From 8c0d5bced6c7d88043d62e340b2dff6dac307408 Mon Sep 17 00:00:00 2001 From: Vikranth Reddimasu Date: Sat, 18 Apr 2026 10:32:19 -0400 Subject: [PATCH] fix(appshell): move openSource useCallback above conditional returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-Wave-2, AppShell had a Rules of Hooks violation: openSource's useCallback sat BELOW two early-return branches (showWizard, showWelcome). On the first render — when either of those was true — the hook was never called; once both became false on a subsequent render, it ran, shifting the hook order by one slot and triggering: "React has detected a change in the order of Hooks called by AppShell. ... 66. useRef → useRef, 67. undefined → useCallback" The net effect in production: the renderer threw during render on the transition from welcome → main shell, so the Electron window stayed blank for users who launched the app on an existing notebook. Fix: hoist openSource above the conditional returns so every render evaluates the same hook sequence. No behavior change — the callback captures the same state and serves the same citation-click and source- card-click handlers. Verified by pointing Vite at the fixed build and observing the full app shell render correctly; React console no longer emits the hook-order error after the module reload. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/layout/AppShell.tsx | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/apps/desktop/src/components/layout/AppShell.tsx b/apps/desktop/src/components/layout/AppShell.tsx index c145b77..c9c02ad 100644 --- a/apps/desktop/src/components/layout/AppShell.tsx +++ b/apps/desktop/src/components/layout/AppShell.tsx @@ -187,6 +187,35 @@ export function AppShell() { e.target.value = ''; }; + // Single entry point for opening a source — called from both the source + // panel card click and citation marker click in MessageBubble. Declared + // BEFORE the conditional early returns below so hook order stays stable + // across wizard/welcome/main renders (Rules of Hooks). + const openSource = useCallback( + async (source: SourceChunk) => { + const notebookId = source.notebook_id || activeNotebookId; + if (!notebookId) return; + try { + const url = await getDocumentPreviewUrl(notebookId, source.source_path); + const filename = + source.document_name || + source.source_path.split(/[/\\]/).pop() || + source.source_path; + setResolvedPreviewUrl(url); + setHighlightText(source.preview); + setPreviewDocument({ + filename, + source_path: source.source_path, + chunk_count: 0, + preview: '', + }); + } catch { + showToast('Could not open source', 'error'); + } + }, + [activeNotebookId, setPreviewDocument], + ); + // Show wizard overlay if (showWizard) { return ( @@ -241,26 +270,6 @@ export function AppShell() { ); } - // Single entry point for opening a source — called from both the source - // panel card click and citation marker click in MessageBubble. Keeps the - // fetch/open/highlight dance in one place. - const openSource = useCallback( - async (source: SourceChunk) => { - const notebookId = source.notebook_id || activeNotebookId; - if (!notebookId) return; - try { - const url = await getDocumentPreviewUrl(notebookId, source.source_path); - const filename = source.document_name || source.source_path.split(/[/\\]/).pop() || source.source_path; - setResolvedPreviewUrl(url); - setHighlightText(source.preview); - setPreviewDocument({ filename, source_path: source.source_path, chunk_count: 0, preview: '' }); - } catch { - showToast('Could not open source', 'error'); - } - }, - [activeNotebookId, setPreviewDocument], - ); - return ( <>