- 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
99 lines
2.5 KiB
TypeScript
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 };
|
|
}
|