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:
@@ -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)}
|
||||
|
||||
61
src/utils/mergeTabContent.test.ts
Normal file
61
src/utils/mergeTabContent.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
18
src/utils/mergeTabContent.ts
Normal file
18
src/utils/mergeTabContent.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user