fix: resolve AI chat empty body via || fallback + defensive body + Rust strip_frontmatter
Three root causes identified and fixed:
1. JS semantics bug: `activeNoteContent ?? allContent[path]` used nullish
coalescing (`??`) which does NOT fall through on empty string ''. When
handleEditorChange temporarily overwrites tab.content with frontmatter-
only content during async content swaps, activeNoteContent becomes ''
and the fallback to allContent never triggers. Fix: change `??` to `||`.
2. Defence-in-depth: when body is still empty after fallback (Tauri mode
where allContent is {}), but wordCount > 0, the body field now includes
an explicit get_note instruction instead of being empty. This is more
reliable than the preamble instruction that Claude may skip.
3. Rust strip_frontmatter used `rest.find("---")` which matches `---`
anywhere in text (including inside frontmatter values like `title:
foo---bar`). Fixed to use `rest.find("\n---")` for line-boundary
matching. This ensures accurate wordCount for the fallback heuristic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -391,6 +391,37 @@ describe('buildContextSnapshot', () => {
|
||||
expect(json.activeNote.body).toBe('# Just a heading\n\nSome plain content.')
|
||||
})
|
||||
|
||||
it('falls back to allContent when activeNoteContent is empty string (|| fix)', () => {
|
||||
// Regression: `??` does not fall through on empty string '', only on null/undefined.
|
||||
// The `||` fix ensures empty string falls through to allContent.
|
||||
const result = buildContextSnapshot({
|
||||
activeEntry: active,
|
||||
allContent,
|
||||
entries,
|
||||
activeNoteContent: '',
|
||||
})
|
||||
const json = JSON.parse(result.split('```json\n')[1].split('\n```')[0])
|
||||
expect(json.activeNote.body).toContain('Project content.')
|
||||
expect(json.activeNote.body).not.toBe('')
|
||||
})
|
||||
|
||||
it('includes defensive body when body is empty but wordCount > 0', () => {
|
||||
// When body is empty (e.g. timing issue) but note has content on disk,
|
||||
// body field should instruct Claude to use get_note
|
||||
const entryWithWords = makeEntry({
|
||||
path: '/vault/a.md', title: 'Alpha', wordCount: 206,
|
||||
})
|
||||
const result = buildContextSnapshot({
|
||||
activeEntry: entryWithWords,
|
||||
allContent: {},
|
||||
entries,
|
||||
activeNoteContent: '---\ntitle: Alpha\n---\n',
|
||||
})
|
||||
const json = JSON.parse(result.split('```json\n')[1].split('\n```')[0])
|
||||
expect(json.activeNote.body).toContain('get_note')
|
||||
expect(json.activeNote.body).toContain('206 words')
|
||||
})
|
||||
|
||||
it('includes wikilink instruction in preamble', () => {
|
||||
const result = buildContextSnapshot({ activeEntry: active, allContent, entries })
|
||||
expect(result).toContain('[[Note Title]]')
|
||||
|
||||
@@ -105,8 +105,18 @@ const MAX_NOTE_LIST_ITEMS = 100
|
||||
export function buildContextSnapshot(params: ContextSnapshotParams): string {
|
||||
const { activeEntry, allContent, activeNoteContent, openTabs, noteList, noteListFilter, entries, references } = params
|
||||
|
||||
const rawContent = activeNoteContent ?? allContent[activeEntry.path] ?? ''
|
||||
const body = extractBody(rawContent)
|
||||
// Use `||` (not `??`) so empty string '' falls through to allContent.
|
||||
// This handles the case where handleEditorChange temporarily overwrites
|
||||
// tab.content with frontmatter-only content during async content swaps.
|
||||
const rawContent = activeNoteContent || allContent[activeEntry.path] || ''
|
||||
let body = extractBody(rawContent)
|
||||
|
||||
// Defence-in-depth: when body is empty but the note has content on disk,
|
||||
// include an explicit instruction in the body field itself (more reliable
|
||||
// than a preamble instruction that Claude might skip).
|
||||
if (!body && activeEntry.wordCount > 0) {
|
||||
body = `[Content not available in editor context — use get_note("${activeEntry.path}") to read the full note (${activeEntry.wordCount} words)]`
|
||||
}
|
||||
|
||||
const snapshot: Record<string, unknown> = {
|
||||
activeNote: {
|
||||
|
||||
Reference in New Issue
Block a user