fix: AI chat receives note body from open tabs instead of empty allContent

allContent is empty ({}) in Tauri mode because loadVaultData never
populates it — note content only enters allContent on explicit save.
mergeTabContent() enriches allContent with open tab content so the AI
context snapshot always includes the active note's body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-08 12:32:52 +01:00
parent 43629ca62c
commit c47a67168c
3 changed files with 87 additions and 2 deletions

View File

@@ -1,4 +1,4 @@
import { useRef, useEffect, useCallback, memo } from 'react'
import { useRef, useEffect, useCallback, memo, useMemo } from 'react'
import { useEditorTabSwap, getH1TextFromBlocks } from '../hooks/useEditorTabSwap'
import { useHeadingTitleSync } from '../hooks/useHeadingTitleSync'
import { useCreateBlockNote } from '@blocknote/react'
@@ -15,6 +15,7 @@ import { useEditorFocus } from '../hooks/useEditorFocus'
import { EditorRightPanel } from './EditorRightPanel'
import { EditorContent } from './EditorContent'
import { schema } from './editorSchema'
import { mergeTabContent } from '../utils/mergeTabContent'
import './Editor.css'
import './EditorTheme.css'
@@ -172,6 +173,11 @@ export const Editor = memo(function Editor({
diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, diffToggleRef,
})
const enrichedAllContent = useMemo(
() => mergeTabContent(allContent, tabs),
[allContent, tabs],
)
const isLoadingNewTab = activeTabPath !== null && !activeTab
const activeStatus = activeTab ? getNoteStatus?.(activeTab.entry.path) ?? 'clean' : 'clean'
const showDiffToggle = !!(activeTab && (diffMode || activeStatus === 'modified'))
@@ -234,7 +240,7 @@ export const Editor = memo(function Editor({
inspectorEntry={inspectorEntry}
inspectorContent={inspectorContent}
entries={entries}
allContent={allContent}
allContent={enrichedAllContent}
gitHistory={gitHistory}
vaultPath={vaultPath ?? ''}
openTabs={tabs.map(t => t.entry)}

View File

@@ -0,0 +1,61 @@
import { describe, it, expect } from 'vitest'
import { mergeTabContent } from './mergeTabContent'
describe('mergeTabContent', () => {
it('adds tab content when allContent is empty', () => {
const result = mergeTabContent({}, [
{ entry: { path: '/vault/a.md' }, content: '# Hello\nBody text' },
])
expect(result['/vault/a.md']).toBe('# Hello\nBody text')
})
it('overrides allContent with tab content (editor state is fresher)', () => {
const result = mergeTabContent(
{ '/vault/a.md': 'old saved content' },
[{ entry: { path: '/vault/a.md' }, content: 'current editor content' }],
)
expect(result['/vault/a.md']).toBe('current editor content')
})
it('preserves allContent for paths without open tabs', () => {
const result = mergeTabContent(
{ '/vault/b.md': 'other note content' },
[{ entry: { path: '/vault/a.md' }, content: '# Hello' }],
)
expect(result['/vault/b.md']).toBe('other note content')
expect(result['/vault/a.md']).toBe('# Hello')
})
it('returns original object when no tabs have content to merge', () => {
const original = { '/vault/a.md': 'content' }
expect(mergeTabContent(original, [])).toBe(original)
})
it('skips tabs with empty content', () => {
const original = { '/vault/a.md': 'content' }
const result = mergeTabContent(original, [
{ entry: { path: '/vault/b.md' }, content: '' },
])
expect(result).toBe(original)
expect(result['/vault/b.md']).toBeUndefined()
})
it('handles multiple tabs', () => {
const result = mergeTabContent({}, [
{ entry: { path: '/vault/a.md' }, content: 'Note A' },
{ entry: { path: '/vault/b.md' }, content: 'Note B' },
])
expect(result['/vault/a.md']).toBe('Note A')
expect(result['/vault/b.md']).toBe('Note B')
})
it('does not mutate the original allContent object', () => {
const original = { '/vault/a.md': 'old' }
const result = mergeTabContent(original, [
{ entry: { path: '/vault/a.md' }, content: 'new' },
])
expect(original['/vault/a.md']).toBe('old')
expect(result['/vault/a.md']).toBe('new')
expect(result).not.toBe(original)
})
})

View File

@@ -0,0 +1,18 @@
/**
* Merge open tab content into the allContent dictionary.
* Tab content takes priority (it reflects the current editor state).
* Returns the original object if no changes are needed (stable reference for useMemo).
*/
export function mergeTabContent(
allContent: Record<string, string>,
tabs: ReadonlyArray<{ entry: { path: string }; content: string }>,
): Record<string, string> {
let merged: Record<string, string> | null = null
for (const tab of tabs) {
if (!tab.content) continue
if (allContent[tab.entry.path] === tab.content) continue
if (!merged) merged = { ...allContent }
merged[tab.entry.path] = tab.content
}
return merged ?? allContent
}