feat: wikilink pills in message bubbles, noteList context injection

- Render [[wikilink]] reference pills inside sent message bubbles
  with type-colored badges; clicking a pill opens the note
- Add noteList (filtered note list titles, max 100) and noteListFilter
  to the structured context snapshot sent to the AI
- Thread noteList/noteListFilter from App → Editor → EditorRightPanel → AiPanel
- Store references in AiAgentMessage for display in chat history
- Add tests for reference pill rendering and noteList context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-04 19:53:46 +01:00
parent 55ff9e6f5d
commit cfb047cb22
9 changed files with 186 additions and 13 deletions

View File

@@ -294,6 +294,43 @@ describe('buildContextSnapshot', () => {
expect(json.referencedNotes).toBeUndefined()
})
it('includes noteList when provided', () => {
const noteList = [
{ path: '/vault/a.md', title: 'Alpha', type: 'Project' },
{ path: '/vault/b.md', title: 'Beta', type: 'Person' },
]
const result = buildContextSnapshot({
activeEntry: active, allContent, entries,
noteList,
})
const json = JSON.parse(result.split('```json\n')[1].split('\n```')[0])
expect(json.noteList).toHaveLength(2)
expect(json.noteList[0].title).toBe('Alpha')
expect(json.noteList[1].type).toBe('Person')
})
it('truncates noteList at 100 items', () => {
const noteList = Array.from({ length: 150 }, (_, i) => ({
path: `/vault/note-${i}.md`, title: `Note ${i}`, type: 'Note',
}))
const result = buildContextSnapshot({
activeEntry: active, allContent, entries,
noteList,
})
const json = JSON.parse(result.split('```json\n')[1].split('\n```')[0])
expect(json.noteList).toHaveLength(100)
expect(json.noteListTruncated).toEqual({ shown: 100, total: 150 })
})
it('omits noteList when empty', () => {
const result = buildContextSnapshot({
activeEntry: active, allContent, entries,
noteList: [],
})
const json = JSON.parse(result.split('```json\n')[1].split('\n```')[0])
expect(json.noteList).toBeUndefined()
})
it('includes belongsTo and relatedTo in frontmatter', () => {
const entryWithRels = makeEntry({
path: '/vault/a.md', title: 'Alpha',

View File

@@ -61,11 +61,19 @@ export interface NoteReference {
type: string | null
}
/** Lightweight note summary for the context snapshot. */
export interface NoteListItem {
path: string
title: string
type: string
}
/** Parameters for building the structured context snapshot. */
export interface ContextSnapshotParams {
activeEntry: VaultEntry
allContent: Record<string, string>
openTabs?: VaultEntry[]
noteList?: NoteListItem[]
noteListFilter?: { type: string | null; query: string }
entries: VaultEntry[]
references?: NoteReference[]
@@ -82,9 +90,11 @@ function entryFrontmatter(e: VaultEntry): Record<string, unknown> {
return fm
}
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, noteListFilter, entries, references } = params
const { activeEntry, allContent, openTabs, noteList, noteListFilter, entries, references } = params
const snapshot: Record<string, unknown> = {
activeNote: {
@@ -106,6 +116,14 @@ export function buildContextSnapshot(params: ContextSnapshotParams): string {
}))
}
if (noteList && noteList.length > 0) {
const items = noteList.slice(0, MAX_NOTE_LIST_ITEMS)
snapshot.noteList = items
if (noteList.length > MAX_NOTE_LIST_ITEMS) {
snapshot.noteListTruncated = { shown: MAX_NOTE_LIST_ITEMS, total: noteList.length }
}
}
if (noteListFilter && (noteListFilter.type || noteListFilter.query)) {
snapshot.noteListFilter = noteListFilter
}