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
This commit is contained in:
98
apps/web/src/hooks/useDraftPersistence.ts
Normal file
98
apps/web/src/hooks/useDraftPersistence.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user