From d37c75296ca90b30f4f38a2d5778ccef8722cb38 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 8 Mar 2026 13:42:09 +0100 Subject: [PATCH] fix: pass active note content directly to AI context builder In Tauri mode, allContent is {} (empty) until a note is explicitly saved. The previous mergeTabContent fix enriched allContent with tab content, but the indirection was fragile. This fix passes the active tab's content directly to buildContextSnapshot as activeNoteContent, which takes priority over allContent[path]. This ensures the AI always receives the note body regardless of allContent state. Co-Authored-By: Claude Opus 4.6 --- src/components/AiPanel.tsx | 11 +++++++---- src/components/EditorRightPanel.tsx | 1 + src/utils/ai-context.test.ts | 29 +++++++++++++++++++++++++++++ src/utils/ai-context.ts | 6 ++++-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/components/AiPanel.tsx b/src/components/AiPanel.tsx index 3871a633..9dd99f52 100644 --- a/src/components/AiPanel.tsx +++ b/src/components/AiPanel.tsx @@ -16,6 +16,8 @@ interface AiPanelProps { onVaultChanged?: () => void vaultPath: string activeEntry?: VaultEntry | null + /** Direct content of the active note from the editor tab. */ + activeNoteContent?: string | null entries?: VaultEntry[] allContent?: Record openTabs?: VaultEntry[] @@ -110,7 +112,7 @@ function MessageHistory({ messages, isActive, onOpenNote, onNavigateWikilink, ha ) } -export function AiPanel({ onClose, onOpenNote, onFileCreated, onFileModified, onVaultChanged, vaultPath, activeEntry, entries, allContent, openTabs, noteList, noteListFilter }: AiPanelProps) { +export function AiPanel({ onClose, onOpenNote, onFileCreated, onFileModified, onVaultChanged, vaultPath, activeEntry, activeNoteContent, entries, allContent, openTabs, noteList, noteListFilter }: AiPanelProps) { const [input, setInput] = useState('') const [pendingRefs, setPendingRefs] = useState([]) const inputRef = useRef(null) @@ -122,17 +124,18 @@ export function AiPanel({ onClose, onOpenNote, onFileCreated, onFileModified, on }, [activeEntry, entries]) const contextPrompt = useMemo(() => { - if (!activeEntry || !allContent || !entries) return undefined + if (!activeEntry || !entries) return undefined return buildContextSnapshot({ activeEntry, - allContent, + allContent: allContent ?? {}, + activeNoteContent: activeNoteContent ?? undefined, openTabs, noteList, noteListFilter, entries, references: pendingRefs.length > 0 ? pendingRefs : undefined, }) - }, [activeEntry, allContent, openTabs, noteList, noteListFilter, entries, pendingRefs]) + }, [activeEntry, activeNoteContent, allContent, openTabs, noteList, noteListFilter, entries, pendingRefs]) const fileCallbacks = useMemo(() => ({ onFileCreated, diff --git a/src/components/EditorRightPanel.tsx b/src/components/EditorRightPanel.tsx index 3108c1bb..fbb927d4 100644 --- a/src/components/EditorRightPanel.tsx +++ b/src/components/EditorRightPanel.tsx @@ -51,6 +51,7 @@ export function EditorRightPanel({ onVaultChanged={onVaultChanged} vaultPath={vaultPath} activeEntry={inspectorEntry} + activeNoteContent={inspectorContent} entries={entries} allContent={allContent} openTabs={openTabs} diff --git a/src/utils/ai-context.test.ts b/src/utils/ai-context.test.ts index ba876107..a317fe3a 100644 --- a/src/utils/ai-context.test.ts +++ b/src/utils/ai-context.test.ts @@ -331,6 +331,35 @@ describe('buildContextSnapshot', () => { expect(json.noteList).toBeUndefined() }) + it('uses activeNoteContent for body when allContent is empty (Tauri mode)', () => { + const emptyAllContent: Record = {} + const result = buildContextSnapshot({ + activeEntry: active, + allContent: emptyAllContent, + entries, + activeNoteContent: '---\ntitle: Alpha\n---\n\n# Alpha\nProject content from tab.', + }) + const json = JSON.parse(result.split('```json\n')[1].split('\n```')[0]) + expect(json.activeNote.body).toContain('Project content from tab.') + }) + + it('prefers activeNoteContent over allContent when both present', () => { + const result = buildContextSnapshot({ + activeEntry: active, + allContent, + entries, + activeNoteContent: 'Fresh editor content', + }) + const json = JSON.parse(result.split('```json\n')[1].split('\n```')[0]) + expect(json.activeNote.body).toBe('Fresh editor content') + }) + + it('falls back to allContent when activeNoteContent is undefined', () => { + const result = buildContextSnapshot({ activeEntry: active, allContent, entries }) + const json = JSON.parse(result.split('```json\n')[1].split('\n```')[0]) + expect(json.activeNote.body).toBe('# Alpha\nProject content.') + }) + it('includes wikilink instruction in preamble', () => { const result = buildContextSnapshot({ activeEntry: active, allContent, entries }) expect(result).toContain('[[Note Title]]') diff --git a/src/utils/ai-context.ts b/src/utils/ai-context.ts index 24c19604..f14ed22b 100644 --- a/src/utils/ai-context.ts +++ b/src/utils/ai-context.ts @@ -72,6 +72,8 @@ export interface NoteListItem { export interface ContextSnapshotParams { activeEntry: VaultEntry allContent: Record + /** Direct content of the active note from the editor tab (most reliable source). */ + activeNoteContent?: string openTabs?: VaultEntry[] noteList?: NoteListItem[] noteListFilter?: { type: string | null; query: string } @@ -94,7 +96,7 @@ const MAX_NOTE_LIST_ITEMS = 100 /** Build a structured context snapshot as a system prompt for Claude. */ export function buildContextSnapshot(params: ContextSnapshotParams): string { - const { activeEntry, allContent, openTabs, noteList, noteListFilter, entries, references } = params + const { activeEntry, allContent, activeNoteContent, openTabs, noteList, noteListFilter, entries, references } = params const snapshot: Record = { activeNote: { @@ -102,7 +104,7 @@ export function buildContextSnapshot(params: ContextSnapshotParams): string { title: activeEntry.title, type: activeEntry.isA ?? 'Note', frontmatter: entryFrontmatter(activeEntry), - body: allContent[activeEntry.path] ?? '', + body: activeNoteContent ?? allContent[activeEntry.path] ?? '', }, }