From 9b2585b2bd060a40ca7ecd995ca53c8fe60ed20b Mon Sep 17 00:00:00 2001 From: murashit Date: Sat, 18 Jul 2026 10:52:25 +0900 Subject: [PATCH] refactor(context): model Obsidian excerpts as references --- docs/design.md | 2 + src/app-server/protocol/request-input.ts | 7 +- src/domain/chat/context-manifest.ts | 30 ++++++- .../application/composer/wikilink-context.ts | 82 +++++++++++-------- .../format/context-attachments.ts | 5 +- tests/app-server/request-input.test.ts | 33 +++++++- .../app-server/mappers/thread-stream.test.ts | 47 ++++++++++- .../chat/app-server/transports.test.ts | 3 +- .../application/composer/suggestions.test.ts | 2 +- .../composer/wikilink-context.test.ts | 15 ++-- .../chat/panel/composer-controller.test.ts | 10 ++- 11 files changed, 177 insertions(+), 59 deletions(-) diff --git a/docs/design.md b/docs/design.md index 558d1527..bd3f489a 100644 --- a/docs/design.md +++ b/docs/design.md @@ -14,6 +14,8 @@ The panel may acquire prompt context through an explicit user action. `/web` fet Panel-acquired context remains reference material rather than current user authorization. Send its bounded payload as untrusted app-server context, while persisting only a small content-free descriptor needed to reconstruct panel display and archive metadata. Apply physical byte and part limits at the app-server boundary; keep source-specific selection and truncation policy with the context producer. +Vault references must remain model-visible in that explicit context. App-server `mention` items with ordinary Vault paths are retained as panel display metadata, but Codex does not expose those arbitrary paths to the model as file context. + Panel settings should store only panel-specific preferences. Do not mirror Codex configuration in Obsidian settings just to display or inspect it. ## Sources of Truth diff --git a/src/app-server/protocol/request-input.ts b/src/app-server/protocol/request-input.ts index c4314109..8f6b6577 100644 --- a/src/app-server/protocol/request-input.ts +++ b/src/app-server/protocol/request-input.ts @@ -75,7 +75,9 @@ export function appServerTurnInputFromCodexInput(input: readonly CodexInputItem[ ].join("\n"), }; }); - manifest.push(manifestEntry(item, id, partCount, sourceBytes, split.includedBytes)); + if (item.attachment || split.includedBytes < sourceBytes) { + manifest.push(manifestEntry(item, id, partCount, sourceBytes, split.includedBytes)); + } } return { input: toAppServerUserInput(input, manifest), @@ -129,6 +131,9 @@ function manifestEntry( includedBytes, truncated: includedBytes < sourceBytes, } as const; + if (item.attachment?.kind === "obsidian") { + return { ...common, kind: "obsidian", inlineExcerpts: item.attachment.inlineExcerpts }; + } if (item.attachment?.kind !== "referencedThread") return common; return { ...common, diff --git a/src/domain/chat/context-manifest.ts b/src/domain/chat/context-manifest.ts index ffb9efde..a6532037 100644 --- a/src/domain/chat/context-manifest.ts +++ b/src/domain/chat/context-manifest.ts @@ -12,7 +12,7 @@ export type TurnContextAttachment = truncated: boolean; } | { kind: "web" } - | { kind: "obsidian" }; + | { kind: "obsidian"; inlineExcerpts: number }; export interface TurnContextManifestEntry { kind: TurnContextAttachment["kind"]; @@ -25,6 +25,7 @@ export interface TurnContextManifestEntry { includedTurns?: number; turnLimit?: number; omittedTurns?: number; + inlineExcerpts?: number; } export interface TurnContextManifest { @@ -88,7 +89,15 @@ export function turnContextSubmissionId(value: string): string { function manifestMatchesClientId(manifest: TurnContextManifest, clientId: string | null): boolean { if (!clientId || !/^local-(?:user|steer)-\d+-[A-Za-z0-9_-]+-[a-z0-9]+-[a-z0-9]+$/.test(clientId)) return false; const submissionId = turnContextSubmissionId(clientId); - return manifest.contexts.every((context, index) => context.id === `${submissionId}.${String(index).padStart(2, "0")}`); + const contextIds = manifest.contexts.map((context) => context.id); + return ( + new Set(contextIds).size === contextIds.length && + contextIds.every((contextId) => new RegExp(`^${escapeRegExp(submissionId)}\\.\\d{2}$`).test(contextId)) + ); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function referencedThreadFromManifest(manifest: TurnContextManifest | null): ReferencedThreadMetadata | null { @@ -117,6 +126,19 @@ function manifestEntryFromUnknown(input: unknown): TurnContextManifestEntry | nu const truncated = value["truncated"]; if (!kind || !id || parts === null || sourceBytes === null || includedBytes === null || typeof truncated !== "boolean") return null; + if (kind === "obsidian") { + const inlineExcerpts = optionalNonNegativeInteger(value["inlineExcerpts"]); + if (inlineExcerpts === null) return null; + return { + kind, + id, + parts, + sourceBytes, + includedBytes, + truncated, + ...(inlineExcerpts === undefined ? {} : { inlineExcerpts }), + }; + } if (kind !== "referencedThread") return { kind, id, parts, sourceBytes, includedBytes, truncated }; const threadId = stringValue(value["threadId"]); const includedTurns = nonNegativeInteger(value["includedTurns"]); @@ -149,6 +171,10 @@ function nonNegativeInteger(value: unknown): number | null { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null; } +function optionalNonNegativeInteger(value: unknown): number | null | undefined { + return value === undefined ? undefined : nonNegativeInteger(value); +} + function positiveInteger(value: unknown): number | null { return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null; } diff --git a/src/features/chat/application/composer/wikilink-context.ts b/src/features/chat/application/composer/wikilink-context.ts index 866a698c..5c9d5d67 100644 --- a/src/features/chat/application/composer/wikilink-context.ts +++ b/src/features/chat/application/composer/wikilink-context.ts @@ -10,7 +10,6 @@ import { import { type ActiveNoteContextReference, type ComposerContextReferences, - formatComposerContextRange, type SelectionContextReference, selectionContextReferenceMarker, } from "./context-references"; @@ -22,6 +21,12 @@ interface ParsedWikiLink { display: string; } +interface ObsidianReference { + marker: string; + path: string; + excerpt?: string; +} + export type WikiLinkMentionResolver = (target: string) => { name: string; path: string } | null; const OBSIDIAN_CONTEXT_ADDITIONAL_CONTEXT_KEY = "codex_panel_obsidian_context"; @@ -64,7 +69,7 @@ export function preparedUserInputWithWikiLinkMentionsSkillsAndContext( const contextReplacement = textWithContextReferences(text, contextReferences); const resolvedText = contextReplacement.text; const mentions: RequestMention[] = []; - const wikilinkMappings: string[] = []; + const wikilinkReferences: ObsidianReference[] = []; const seenPaths = new Set(); const activeNoteSnapshots = contextReferences.activeNoteSnapshots ?? []; @@ -73,7 +78,7 @@ export function preparedUserInputWithWikiLinkMentionsSkillsAndContext( if (!mention || seenPaths.has(mention.path)) continue; seenPaths.add(mention.path); mentions.push(mention); - wikilinkMappings.push(`- [[${link.raw}]] -> ${mention.path}`); + wikilinkReferences.push({ marker: `[[${link.raw}]]`, path: mention.path }); } for (const selection of contextReplacement.selections) { @@ -101,7 +106,7 @@ export function preparedUserInputWithWikiLinkMentionsSkillsAndContext( resolvedText, mentions, resolvedSkills, - additionalContext(wikilinkMappings, contextReplacement.selections, attachedActiveNote), + additionalContext(wikilinkReferences, contextReplacement.selections, attachedActiveNote), ), }; } @@ -134,63 +139,68 @@ function selectionsReferencedByText(text: string, snapshots: readonly SelectionC } function additionalContext( - wikilinkMappings: readonly string[], + wikilinkReferences: readonly ObsidianReference[], selections: readonly SelectionContextReference[], activeNote: ActiveNoteContextReference | null, ): RequestAdditionalContext[] { - return obsidianContextAdditionalContext(wikilinkMappings, selections, activeNote); + return obsidianContextAdditionalContext(obsidianReferences(wikilinkReferences, selections, activeNote)); } -function obsidianContextAdditionalContext( - wikilinkMappings: readonly string[], +function obsidianReferences( + wikilinkReferences: readonly ObsidianReference[], selections: readonly SelectionContextReference[], activeNote: ActiveNoteContextReference | null, -): RequestAdditionalContext[] { - if (wikilinkMappings.length === 0 && selections.length === 0 && !activeNote) return []; +): ObsidianReference[] { + const selectionReferences = selections.map((selection) => ({ + marker: selectionContextReferenceMarker(selection), + path: selection.path, + excerpt: selection.text, + })); + const selectedWikilinks = new Set(selections.map((selection) => referenceKey(`[[${selection.linktext}]]`, selection.path))); + return [ + ...wikilinkReferences.filter((reference) => !selectedWikilinks.has(referenceKey(reference.marker, reference.path))), + ...selectionReferences, + ...(activeNote ? [{ marker: ACTIVE_FILE_MENTION_NAME, path: activeNote.path }] : []), + ]; +} + +function referenceKey(marker: string, path: string): string { + return `${marker}\u0000${path}`; +} + +function obsidianContextAdditionalContext(references: readonly ObsidianReference[]): RequestAdditionalContext[] { + if (references.length === 0) return []; + const inlineExcerpts = references.filter((reference) => reference.excerpt !== undefined).length; return [ { key: OBSIDIAN_CONTEXT_ADDITIONAL_CONTEXT_KEY, kind: "untrusted", - value: obsidianContextValue(wikilinkMappings, selections, activeNote), + value: obsidianContextValue(references), + ...(inlineExcerpts > 0 ? { attachment: { kind: "obsidian" as const, inlineExcerpts } } : {}), }, ]; } -function obsidianContextValue( - wikilinkMappings: readonly string[], - selections: readonly SelectionContextReference[], - activeNote: ActiveNoteContextReference | null, -): string { - const sections: string[][] = []; - if (wikilinkMappings.length > 0) sections.push(["Resolved wikilinks:", ...wikilinkMappings]); - if (selections.length > 0) { - sections.push(["Referenced selections:", ...selectionReferenceLines(selections), "", ...selectionBodyLines(selections)]); - } - if (activeNote) sections.push(activeNoteContextLines(activeNote)); +function obsidianContextValue(references: readonly ObsidianReference[]): string { + const excerpts = references.filter((reference): reference is ObsidianReference & { excerpt: string } => reference.excerpt !== undefined); return [ - "Obsidian context for the current user input:", - ...sections.flatMap((section, index) => (index === 0 ? section : ["", ...section])), + "Obsidian references for the current user input:", + ...references.map(referenceLine), + ...(excerpts.length > 0 ? ["", "Inline excerpts:", ...excerptLines(excerpts)] : []), ].join("\n"); } -function selectionReferenceLines(selections: readonly SelectionContextReference[]): string[] { - return selections.map((selection) => { - const location = `${selection.path} ${formatComposerContextRange(selection.range)}`; - return `- ${selectionContextReferenceMarker(selection)} -> ${location}`; - }); +function referenceLine(reference: ObsidianReference): string { + return `- ${reference.marker} -> ${reference.path}${reference.excerpt === undefined ? "" : " (inline excerpt below)"}`; } -function selectionBodyLines(selections: readonly SelectionContextReference[]): string[] { - return selections.flatMap((selection, index) => { +function excerptLines(references: readonly (ObsidianReference & { excerpt: string })[]): string[] { + return references.flatMap((reference, index) => { const prefix = index === 0 ? [] : [""]; - return [...prefix, `${selectionContextReferenceMarker(selection)}:`, selection.text]; + return [...prefix, `${reference.marker}:`, reference.excerpt]; }); } -function activeNoteContextLines(activeNote: ActiveNoteContextReference): string[] { - return ["Referenced active file:", `- ${ACTIVE_FILE_MENTION_NAME} -> ${activeNote.path}`]; -} - function parsedSkillReferences(text: string): string[] { const references: string[] = []; const seen = new Set(); diff --git a/src/features/chat/domain/thread-stream/format/context-attachments.ts b/src/features/chat/domain/thread-stream/format/context-attachments.ts index 56545bad..9a47ae26 100644 --- a/src/features/chat/domain/thread-stream/format/context-attachments.ts +++ b/src/features/chat/domain/thread-stream/format/context-attachments.ts @@ -19,8 +19,9 @@ export function contextAttachmentsFromManifest(manifest: TurnContextManifest | n const source = visibleWebSource(visibleText); attachments.push({ label: web.truncated ? "Web page (truncated)" : "Web page", ...(source ? { detail: source } : {}) }); } - if (manifest?.contexts.some((context) => context.kind === "obsidian" && context.truncated)) { - attachments.push({ label: "Obsidian context (truncated)" }); + const obsidian = manifest?.contexts.find((context) => context.kind === "obsidian" && context.truncated); + if (obsidian) { + attachments.push({ label: obsidian.inlineExcerpts ? "Obsidian excerpt (truncated)" : "Obsidian context (truncated)" }); } return attachments; } diff --git a/tests/app-server/request-input.test.ts b/tests/app-server/request-input.test.ts index 16c0f4c5..f5a5fbcf 100644 --- a/tests/app-server/request-input.test.ts +++ b/tests/app-server/request-input.test.ts @@ -109,6 +109,37 @@ describe("app-server request input", () => { expect(manifestInput?.type === "text" ? manifestInput.text : "").not.toContain("本文です"); }); + it("keeps Obsidian reference metadata ahead of a truncated inline excerpt", () => { + const reference = "[[Note]] (L2:C1-L3:C1) -> Note.md"; + const value = [ + "Obsidian references for the current user input:", + `- ${reference} (inline excerpt below)`, + "", + "Inline excerpts:", + "[[Note]] (L2:C1-L3:C1):", + "本文".repeat(20_000), + ].join("\n"); + const prepared = appServerTurnInputFromCodexInput( + [ + { type: "text", text: "review it" }, + { + type: "additionalContext", + key: "codex_panel_obsidian_context", + kind: "untrusted", + value, + attachment: { kind: "obsidian", inlineExcerpts: 1 }, + }, + ], + "local-user", + ); + + const firstPart = Object.values(prepared.additionalContext ?? {})[0]; + expect(firstPart?.value).toContain(reference); + const manifestInput = prepared.input.at(-1); + const manifest = manifestInput?.type === "text" ? turnContextManifestFromText(manifestInput.text) : null; + expect(manifest?.contexts).toEqual([expect.objectContaining({ kind: "obsidian", inlineExcerpts: 1, truncated: true })]); + }); + it("namespaces identical explicit context by submission", () => { const input: CodexInput = [ { type: "text", text: "read it" }, @@ -134,6 +165,6 @@ describe("app-server request input", () => { expect(entries.some(([key, entry]) => key.includes(".selection.") && entry.value.includes("selected text"))).toBe(true); const manifestInput = prepared.input.at(-1); const manifest = manifestInput?.type === "text" ? turnContextManifestFromText(manifestInput.text) : null; - expect(manifest?.contexts.map((context) => context.parts)).toEqual([7, 1]); + expect(manifest?.contexts.map((context) => context.parts)).toEqual([7]); }); }); diff --git a/tests/features/chat/app-server/mappers/thread-stream.test.ts b/tests/features/chat/app-server/mappers/thread-stream.test.ts index 0930275e..24b983dc 100644 --- a/tests/features/chat/app-server/mappers/thread-stream.test.ts +++ b/tests/features/chat/app-server/mappers/thread-stream.test.ts @@ -176,6 +176,41 @@ describe("turn item conversion preserves app-server semantics", () => { }); }); + it("accepts a persisted attachment descriptor after an unpersisted reference context", () => { + const clientId = "local-user-1-seed-1-1"; + const prepared = appServerTurnInputFromCodexInput( + [ + { type: "text", text: "https://example.com/ compare with [[Note]]" }, + { + type: "additionalContext", + key: "codex_panel_obsidian_context", + kind: "untrusted", + value: "- [[Note]] -> Note.md", + }, + { + type: "additionalContext", + key: "codex_panel_web_context", + kind: "untrusted", + value: "Source: https://example.com/\n\npage", + attachment: { kind: "web" }, + }, + ], + clientId, + ); + + expect( + threadStreamItemFromTurnItem({ + type: "userMessage", + id: "u1", + clientId, + content: prepared.input, + }), + ).toMatchObject({ + text: "https://example.com/ compare with [[Note]]", + contextAttachments: [{ label: "Web page", detail: "https://example.com/" }], + }); + }); + it("does not hide a user-authored manifest-like message", () => { const text = '[Codex Panel context v2]{"version":2,"contexts":[{"kind":"web","id":"fake","parts":1,"sourceBytes":1,"includedBytes":1,"truncated":false}]}'; @@ -205,12 +240,18 @@ describe("turn item conversion preserves app-server semantics", () => { expect(projected).not.toHaveProperty("contextAttachments"); }); - it("surfaces truncated Obsidian context from the persisted manifest", () => { + it("surfaces a truncated Obsidian excerpt from the persisted manifest", () => { const clientId = "local-user-1-seed-1-1"; const prepared = appServerTurnInputFromCodexInput( [ { type: "text", text: "review the selection" }, - { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", value: "x".repeat(30_000) }, + { + type: "additionalContext", + key: "codex_panel_obsidian_context", + kind: "untrusted", + value: "x".repeat(30_000), + attachment: { kind: "obsidian", inlineExcerpts: 1 }, + }, ], clientId, ); @@ -224,7 +265,7 @@ describe("turn item conversion preserves app-server semantics", () => { }), ).toMatchObject({ text: "review the selection", - contextAttachments: [{ label: "Obsidian context (truncated)" }], + contextAttachments: [{ label: "Obsidian excerpt (truncated)" }], }); }); diff --git a/tests/features/chat/app-server/transports.test.ts b/tests/features/chat/app-server/transports.test.ts index 0eab48ff..829f85c5 100644 --- a/tests/features/chat/app-server/transports.test.ts +++ b/tests/features/chat/app-server/transports.test.ts @@ -134,7 +134,6 @@ describe("chat app-server transports", () => { { type: "text", text, text_elements: [] }, { type: "mention", name: "Alpha", path: "notes/Alpha.md" }, { type: "mention", name: "", path: "notes/Alpha.md" }, - { type: "text", text: expect.stringContaining("[Codex Panel context v2]"), text_elements: [] }, ], clientUserMessageId: "local-user", }); @@ -142,7 +141,7 @@ describe("chat app-server transports", () => { "codex_panel.local-user.00.codex_panel_obsidian_context.part_01_of_01": { kind: "untrusted", value: - "Codex Panel context part 1/1.\nSource: codex_panel_obsidian_context\n\nObsidian context for the current user input:\nResolved wikilinks:\n- [[Alpha]] -> notes/Alpha.md\n\nReferenced active file:\n- -> notes/Alpha.md", + "Codex Panel context part 1/1.\nSource: codex_panel_obsidian_context\n\nObsidian references for the current user input:\n- [[Alpha]] -> notes/Alpha.md\n- -> notes/Alpha.md", }, }); }); diff --git a/tests/features/chat/application/composer/suggestions.test.ts b/tests/features/chat/application/composer/suggestions.test.ts index 747620da..3df91be8 100644 --- a/tests/features/chat/application/composer/suggestions.test.ts +++ b/tests/features/chat/application/composer/suggestions.test.ts @@ -199,7 +199,7 @@ describe("composer suggestions", () => { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", - value: "Obsidian context for the current user input:\nResolved wikilinks:\n- [[Assets/Diagram.png]] -> Assets/Diagram.png", + value: "Obsidian references for the current user input:\n- [[Assets/Diagram.png]] -> Assets/Diagram.png", }, ]); }); diff --git a/tests/features/chat/application/composer/wikilink-context.test.ts b/tests/features/chat/application/composer/wikilink-context.test.ts index 82bd5291..0eed30f4 100644 --- a/tests/features/chat/application/composer/wikilink-context.test.ts +++ b/tests/features/chat/application/composer/wikilink-context.test.ts @@ -12,10 +12,10 @@ const obsidianContext = (...sections: string[]) => ({ type: "additionalContext" as const, key: "codex_panel_obsidian_context", kind: "untrusted" as const, - value: ["Obsidian context for the current user input:", ...sections].join("\n"), + value: ["Obsidian references for the current user input:", ...sections].join("\n"), }); -const wikilinkContext = (...mappings: string[]) => obsidianContext("Resolved wikilinks:", ...mappings); +const wikilinkContext = (...mappings: string[]) => obsidianContext(...mappings); function userInputWithWikiLinkMentionsAndSkills( text: string, @@ -226,7 +226,7 @@ describe("wikilink context", () => { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", - value: "Obsidian context for the current user input:\nReferenced active file:\n- -> notes/Alpha.md", + value: "Obsidian references for the current user input:\n- -> notes/Alpha.md", }, ]); }); @@ -253,8 +253,7 @@ describe("wikilink context", () => { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", - value: - "Obsidian context for the current user input:\nResolved wikilinks:\n- [[Beta]] -> notes/Beta.md\n\nReferenced active file:\n- -> notes/Alpha.md", + value: "Obsidian references for the current user input:\n- [[Beta]] -> notes/Beta.md\n- -> notes/Alpha.md", }, ]); }); @@ -294,8 +293,9 @@ describe("wikilink context", () => { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", + attachment: { kind: "obsidian", inlineExcerpts: 1 }, value: - "Obsidian context for the current user input:\nResolved wikilinks:\n- [[Beta]] -> notes/Beta.md\n- [[Gamma]] -> notes/Gamma.md\n\nReferenced selections:\n- [[Gamma]] (L2:C1-L2:C6) -> notes/Gamma.md L2:C1-L2:C6\n\n[[Gamma]] (L2:C1-L2:C6):\nselected gamma\n\nReferenced active file:\n- -> notes/Alpha.md", + "Obsidian references for the current user input:\n- [[Beta]] -> notes/Beta.md\n- [[Gamma]] (L2:C1-L2:C6) -> notes/Gamma.md (inline excerpt below)\n- -> notes/Alpha.md\n\nInline excerpts:\n[[Gamma]] (L2:C1-L2:C6):\nselected gamma", }, ]); }); @@ -331,8 +331,9 @@ describe("wikilink context", () => { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", + attachment: { kind: "obsidian", inlineExcerpts: 1 }, value: - "Obsidian context for the current user input:\nResolved wikilinks:\n- [[notes/Alpha]] -> notes/Alpha.md\n\nReferenced selections:\n- [[notes/Alpha]] (L42:C5-L47:C1) -> notes/Alpha.md L42:C5-L47:C1\n\n[[notes/Alpha]] (L42:C5-L47:C1):\ncompleted selection", + "Obsidian references for the current user input:\n- [[notes/Alpha]] (L42:C5-L47:C1) -> notes/Alpha.md (inline excerpt below)\n\nInline excerpts:\n[[notes/Alpha]] (L42:C5-L47:C1):\ncompleted selection", }); }); }); diff --git a/tests/features/chat/panel/composer-controller.test.ts b/tests/features/chat/panel/composer-controller.test.ts index 0d0cf501..5030f42e 100644 --- a/tests/features/chat/panel/composer-controller.test.ts +++ b/tests/features/chat/panel/composer-controller.test.ts @@ -1148,7 +1148,7 @@ describe("ChatComposerController", () => { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", - value: "Obsidian context for the current user input:\nReferenced active file:\n- -> notes/Alpha.md", + value: "Obsidian references for the current user input:\n- -> notes/Alpha.md", }, ]); }); @@ -1220,8 +1220,9 @@ describe("ChatComposerController", () => { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", + attachment: { kind: "obsidian", inlineExcerpts: 1 }, value: - "Obsidian context for the current user input:\nResolved wikilinks:\n- [[notes/Alpha]] -> notes/Alpha.md\n\nReferenced selections:\n- [[notes/Alpha]] (L42:C5-L47:C1) -> notes/Alpha.md L42:C5-L47:C1\n\n[[notes/Alpha]] (L42:C5-L47:C1):\ninitial selection", + "Obsidian references for the current user input:\n- [[notes/Alpha]] (L42:C5-L47:C1) -> notes/Alpha.md (inline excerpt below)\n\nInline excerpts:\n[[notes/Alpha]] (L42:C5-L47:C1):\ninitial selection", }); controller.setDraft("", { clearSuggestions: true }); @@ -1229,14 +1230,15 @@ describe("ChatComposerController", () => { type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", + attachment: { kind: "obsidian", inlineExcerpts: 1 }, value: - "Obsidian context for the current user input:\nResolved wikilinks:\n- [[notes/Alpha]] -> notes/Alpha.md\n\nReferenced selections:\n- [[notes/Alpha]] (L42:C5-L47:C1) -> notes/Alpha.md L42:C5-L47:C1\n\n[[notes/Alpha]] (L42:C5-L47:C1):\ninitial selection", + "Obsidian references for the current user input:\n- [[notes/Alpha]] (L42:C5-L47:C1) -> notes/Alpha.md (inline excerpt below)\n\nInline excerpts:\n[[notes/Alpha]] (L42:C5-L47:C1):\ninitial selection", }); expect(controller.preparedInput(completedSelectionReference).input).toContainEqual({ type: "additionalContext", key: "codex_panel_obsidian_context", kind: "untrusted", - value: "Obsidian context for the current user input:\nResolved wikilinks:\n- [[notes/Alpha]] -> notes/Alpha.md", + value: "Obsidian references for the current user input:\n- [[notes/Alpha]] -> notes/Alpha.md", }); });