Files
boocode/apps/web/src/hooks/useDraftPersistence.ts
indifferentketchup 50de80ee75 feat(web): workspace components — ComparePane, Memory page, McpDialog, error boundaries, message-parts
- Add ComparePane.tsx: side-by-side AI response comparison
- Add Memory.tsx: memory management page with CRUD UI
- Add McpPermissionDialog.tsx: MCP tool permission approval dialog
- Add McpResponseDisplay.tsx: MCP response visualization
- Add MessageBoundary.tsx + MessageListErrorBoundary.tsx: error resilience
- Add EmptyState.tsx: contextual empty state component
- Add KeyboardShortcutsDialog.tsx: keyboard shortcut reference
- Add message-parts/: ActionRow, CompactCard, MistakeRecoverySentinel, ReasoningBlock, SendToTerminalMenu, StatsLine, SummaryCard
- Add useDraftPersistence.ts: draft message persistence hook
- Add useTerminals.ts: terminal session management hook
- Add keyboard-shortcuts.ts + tool-utils.ts: shared utilities
- Extend components: ChatInput, MessageBubble, MessageList, Workspace, panes
- Extend hooks: useTerminalSocket, useSessionStream test suite
- Update pages: Home, Project — workspace layout and session flow
2026-06-08 03:49:22 +00:00

99 lines
2.5 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
const STORAGE_PREFIX = 'boocode_draft_';
const SAVE_DEBOUNCE_MS = 500;
function getKey(chatId: string): string {
return `${STORAGE_PREFIX}${chatId}`;
}
function readDraft(key: string): string {
if (typeof window === 'undefined') return '';
try {
return localStorage.getItem(key) ?? '';
} catch {
return '';
}
}
function writeDraft(key: string, text: string): void {
if (typeof window === 'undefined') return;
try {
if (text) {
localStorage.setItem(key, text);
} else {
localStorage.removeItem(key);
}
} catch {
// storage full or unavailable — silently ignore
}
}
function removeDraft(key: string): void {
if (typeof window === 'undefined') return;
try {
localStorage.removeItem(key);
} catch {
// silently ignore
}
}
export interface DraftPersistenceResult {
/** Current draft state, initialized from localStorage on mount. */
draft: string;
/** Update draft with 500ms debounced persistence to localStorage. */
setDraft: (text: string) => void;
/** Clear draft state and remove localStorage entry immediately. */
clearDraft: () => void;
/** Re-read from localStorage, update state, and return saved value. */
restoreDraft: () => string;
}
export function useDraftPersistence(chatId: string | undefined): DraftPersistenceResult {
const key = chatId ? getKey(chatId) : null;
const [draft, setDraftState] = useState(() => (key ? readDraft(key) : ''));
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const keyRef = useRef(key);
keyRef.current = key;
// Cleanup timer on unmount
useEffect(() => {
return () => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
};
}, []);
const setDraft = useCallback((text: string) => {
setDraftState(text);
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
const k = keyRef.current;
if (k) writeDraft(k, text);
}, SAVE_DEBOUNCE_MS);
}, []);
const clearDraft = useCallback(() => {
setDraftState('');
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
const k = keyRef.current;
if (k) removeDraft(k);
}, []);
const restoreDraft = useCallback((): string => {
const k = keyRef.current;
if (!k) return '';
const saved = readDraft(k);
setDraftState(saved);
return saved;
}, []);
return { draft, setDraft, clearDraft, restoreDraft };
}