@aexol/spectral 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/dist/agent/agents.js +4 -4
  2. package/dist/agent/index.js +24 -148
  3. package/dist/cli.js +25 -220
  4. package/dist/commands/serve.js +1 -1
  5. package/dist/extensions/spectral-vision-fallback.js +225 -0
  6. package/dist/mcp/agent-dir.js +1 -1
  7. package/dist/mcp/config.js +3 -3
  8. package/dist/mcp/sampling-handler.js +1 -1
  9. package/dist/mcp/server-manager.js +5 -1
  10. package/dist/memory/commands/status.js +6 -6
  11. package/dist/memory/commands/view.js +16 -14
  12. package/dist/memory/compaction.js +33 -5
  13. package/dist/memory/config.js +3 -3
  14. package/dist/memory/debug-log.js +1 -1
  15. package/dist/memory/observer.js +2 -2
  16. package/dist/memory/prompts.js +5 -5
  17. package/dist/memory/tokens.js +1 -1
  18. package/dist/memory/tools/read-project-observations.js +2 -2
  19. package/dist/memory/tools/recall-observation.js +4 -4
  20. package/dist/relay/auto-research.js +23 -23
  21. package/dist/relay/dispatcher.js +28 -2
  22. package/dist/relay/models-fetch.js +15 -3
  23. package/dist/{pi → sdk}/coding-agent/cli/args.js +4 -4
  24. package/dist/{pi → sdk}/coding-agent/config.js +9 -20
  25. package/dist/{pi → sdk}/coding-agent/core/agent-session.js +5 -17
  26. package/dist/{pi → sdk}/coding-agent/core/compaction/compaction.js +161 -5
  27. package/dist/{pi → sdk}/coding-agent/core/extensions/loader.js +0 -6
  28. package/dist/{pi → sdk}/coding-agent/core/extensions/runner.js +7 -1
  29. package/dist/{pi → sdk}/coding-agent/core/keybindings.js +129 -2
  30. package/dist/{pi → sdk}/coding-agent/core/model-registry.js +11 -4
  31. package/dist/{pi → sdk}/coding-agent/core/package-manager.js +5 -5
  32. package/dist/{pi → sdk}/coding-agent/core/sdk.js +1 -1
  33. package/dist/{pi → sdk}/coding-agent/core/session-manager.js +4 -4
  34. package/dist/{pi → sdk}/coding-agent/core/settings-manager.js +20 -0
  35. package/dist/{pi → sdk}/coding-agent/core/telemetry.js +1 -1
  36. package/dist/{pi → sdk}/coding-agent/core/tools/bash.js +17 -63
  37. package/dist/{pi → sdk}/coding-agent/core/tools/edit.js +4 -141
  38. package/dist/{pi → sdk}/coding-agent/core/tools/find.js +0 -11
  39. package/dist/{pi → sdk}/coding-agent/core/tools/grep.js +0 -11
  40. package/dist/{pi → sdk}/coding-agent/core/tools/ls.js +0 -11
  41. package/dist/{pi → sdk}/coding-agent/core/tools/read.js +0 -12
  42. package/dist/{pi → sdk}/coding-agent/core/tools/render-utils.js +1 -14
  43. package/dist/{pi → sdk}/coding-agent/core/tools/write.js +2 -97
  44. package/dist/{pi → sdk}/coding-agent/migrations.js +3 -3
  45. package/dist/{pi → sdk}/coding-agent/modes/interactive/components/keybinding-hints.js +1 -1
  46. package/dist/sdk/coding-agent/modes/interactive/components/visual-truncate.js +26 -0
  47. package/dist/{pi → sdk}/coding-agent/modes/interactive/theme/theme.js +1 -2
  48. package/dist/{pi → sdk}/coding-agent/utils/tools-manager.js +1 -1
  49. package/dist/{pi → sdk}/coding-agent/utils/version-check.js +2 -2
  50. package/dist/{pi → sdk}/coding-agent/utils/windows-self-update.js +1 -1
  51. package/dist/server/{pi-bridge.js → agent-bridge.js} +158 -89
  52. package/dist/server/handlers/sessions.js +21 -0
  53. package/dist/server/session-stream.js +12 -6
  54. package/package.json +6 -3
  55. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +0 -248
  56. package/dist/pi/coding-agent/core/export-html/index.js +0 -225
  57. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +0 -107
  58. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +0 -32
  59. package/dist/pi/tui/autocomplete.js +0 -631
  60. package/dist/pi/tui/components/box.js +0 -103
  61. package/dist/pi/tui/components/cancellable-loader.js +0 -34
  62. package/dist/pi/tui/components/editor.js +0 -1915
  63. package/dist/pi/tui/components/image.js +0 -88
  64. package/dist/pi/tui/components/input.js +0 -425
  65. package/dist/pi/tui/components/loader.js +0 -68
  66. package/dist/pi/tui/components/markdown.js +0 -633
  67. package/dist/pi/tui/components/select-list.js +0 -158
  68. package/dist/pi/tui/components/settings-list.js +0 -184
  69. package/dist/pi/tui/components/spacer.js +0 -22
  70. package/dist/pi/tui/components/text.js +0 -88
  71. package/dist/pi/tui/components/truncated-text.js +0 -50
  72. package/dist/pi/tui/editor-component.js +0 -1
  73. package/dist/pi/tui/fuzzy.js +0 -109
  74. package/dist/pi/tui/index.js +0 -31
  75. package/dist/pi/tui/keybindings.js +0 -173
  76. package/dist/pi/tui/keys.js +0 -1172
  77. package/dist/pi/tui/kill-ring.js +0 -43
  78. package/dist/pi/tui/stdin-buffer.js +0 -360
  79. package/dist/pi/tui/terminal-image.js +0 -335
  80. package/dist/pi/tui/terminal.js +0 -324
  81. package/dist/pi/tui/tui.js +0 -1076
  82. package/dist/pi/tui/undo-stack.js +0 -24
  83. package/dist/pi/tui/utils.js +0 -1016
  84. /package/dist/{pi → sdk}/agent-core/agent-loop.js +0 -0
  85. /package/dist/{pi → sdk}/agent-core/agent.js +0 -0
  86. /package/dist/{pi → sdk}/agent-core/harness/agent-harness.js +0 -0
  87. /package/dist/{pi → sdk}/agent-core/harness/compaction/branch-summarization.js +0 -0
  88. /package/dist/{pi → sdk}/agent-core/harness/compaction/compaction.js +0 -0
  89. /package/dist/{pi → sdk}/agent-core/harness/compaction/utils.js +0 -0
  90. /package/dist/{pi → sdk}/agent-core/harness/env/nodejs.js +0 -0
  91. /package/dist/{pi → sdk}/agent-core/harness/messages.js +0 -0
  92. /package/dist/{pi → sdk}/agent-core/harness/prompt-templates.js +0 -0
  93. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-repo.js +0 -0
  94. /package/dist/{pi → sdk}/agent-core/harness/session/jsonl-storage.js +0 -0
  95. /package/dist/{pi → sdk}/agent-core/harness/session/memory-repo.js +0 -0
  96. /package/dist/{pi → sdk}/agent-core/harness/session/memory-storage.js +0 -0
  97. /package/dist/{pi → sdk}/agent-core/harness/session/repo-utils.js +0 -0
  98. /package/dist/{pi → sdk}/agent-core/harness/session/session.js +0 -0
  99. /package/dist/{pi → sdk}/agent-core/harness/session/uuid.js +0 -0
  100. /package/dist/{pi → sdk}/agent-core/harness/skills.js +0 -0
  101. /package/dist/{pi → sdk}/agent-core/harness/system-prompt.js +0 -0
  102. /package/dist/{pi → sdk}/agent-core/harness/types.js +0 -0
  103. /package/dist/{pi → sdk}/agent-core/harness/utils/shell-output.js +0 -0
  104. /package/dist/{pi → sdk}/agent-core/harness/utils/truncate.js +0 -0
  105. /package/dist/{pi → sdk}/agent-core/index.js +0 -0
  106. /package/dist/{pi → sdk}/agent-core/node.js +0 -0
  107. /package/dist/{pi → sdk}/agent-core/proxy.js +0 -0
  108. /package/dist/{pi → sdk}/agent-core/types.js +0 -0
  109. /package/dist/{pi → sdk}/ai/api-registry.js +0 -0
  110. /package/dist/{pi → sdk}/ai/cli.js +0 -0
  111. /package/dist/{pi → sdk}/ai/env-api-keys.js +0 -0
  112. /package/dist/{pi → sdk}/ai/image-models.generated.js +0 -0
  113. /package/dist/{pi → sdk}/ai/image-models.js +0 -0
  114. /package/dist/{pi → sdk}/ai/images-api-registry.js +0 -0
  115. /package/dist/{pi → sdk}/ai/images.js +0 -0
  116. /package/dist/{pi → sdk}/ai/index.js +0 -0
  117. /package/dist/{pi → sdk}/ai/models.generated.js +0 -0
  118. /package/dist/{pi → sdk}/ai/models.js +0 -0
  119. /package/dist/{pi → sdk}/ai/oauth.js +0 -0
  120. /package/dist/{pi → sdk}/ai/providers/anthropic.js +0 -0
  121. /package/dist/{pi → sdk}/ai/providers/faux.js +0 -0
  122. /package/dist/{pi → sdk}/ai/providers/github-copilot-headers.js +0 -0
  123. /package/dist/{pi → sdk}/ai/providers/openai-completions.js +0 -0
  124. /package/dist/{pi → sdk}/ai/providers/openai-prompt-cache.js +0 -0
  125. /package/dist/{pi → sdk}/ai/providers/register-builtins.js +0 -0
  126. /package/dist/{pi → sdk}/ai/providers/simple-options.js +0 -0
  127. /package/dist/{pi → sdk}/ai/providers/transform-messages.js +0 -0
  128. /package/dist/{pi → sdk}/ai/session-resources.js +0 -0
  129. /package/dist/{pi → sdk}/ai/stream.js +0 -0
  130. /package/dist/{pi → sdk}/ai/types.js +0 -0
  131. /package/dist/{pi → sdk}/ai/utils/diagnostics.js +0 -0
  132. /package/dist/{pi → sdk}/ai/utils/event-stream.js +0 -0
  133. /package/dist/{pi → sdk}/ai/utils/hash.js +0 -0
  134. /package/dist/{pi → sdk}/ai/utils/headers.js +0 -0
  135. /package/dist/{pi → sdk}/ai/utils/json-parse.js +0 -0
  136. /package/dist/{pi → sdk}/ai/utils/node-http-proxy.js +0 -0
  137. /package/dist/{pi → sdk}/ai/utils/oauth/anthropic.js +0 -0
  138. /package/dist/{pi → sdk}/ai/utils/oauth/device-code.js +0 -0
  139. /package/dist/{pi → sdk}/ai/utils/oauth/github-copilot.js +0 -0
  140. /package/dist/{pi → sdk}/ai/utils/oauth/index.js +0 -0
  141. /package/dist/{pi → sdk}/ai/utils/oauth/oauth-page.js +0 -0
  142. /package/dist/{pi → sdk}/ai/utils/oauth/openai-codex.js +0 -0
  143. /package/dist/{pi → sdk}/ai/utils/oauth/pkce.js +0 -0
  144. /package/dist/{pi → sdk}/ai/utils/oauth/types.js +0 -0
  145. /package/dist/{pi → sdk}/ai/utils/overflow.js +0 -0
  146. /package/dist/{pi → sdk}/ai/utils/sanitize-unicode.js +0 -0
  147. /package/dist/{pi → sdk}/ai/utils/typebox-helpers.js +0 -0
  148. /package/dist/{pi → sdk}/ai/utils/validation.js +0 -0
  149. /package/dist/{pi → sdk}/coding-agent/bun/cli.js +0 -0
  150. /package/dist/{pi → sdk}/coding-agent/bun/restore-sandbox-env.js +0 -0
  151. /package/dist/{pi → sdk}/coding-agent/cli/file-processor.js +0 -0
  152. /package/dist/{pi → sdk}/coding-agent/cli/initial-message.js +0 -0
  153. /package/dist/{pi → sdk}/coding-agent/cli.js +0 -0
  154. /package/dist/{pi → sdk}/coding-agent/core/agent-session-runtime.js +0 -0
  155. /package/dist/{pi → sdk}/coding-agent/core/agent-session-services.js +0 -0
  156. /package/dist/{pi → sdk}/coding-agent/core/auth-guidance.js +0 -0
  157. /package/dist/{pi → sdk}/coding-agent/core/auth-storage.js +0 -0
  158. /package/dist/{pi → sdk}/coding-agent/core/bash-executor.js +0 -0
  159. /package/dist/{pi → sdk}/coding-agent/core/compaction/branch-summarization.js +0 -0
  160. /package/dist/{pi → sdk}/coding-agent/core/compaction/index.js +0 -0
  161. /package/dist/{pi → sdk}/coding-agent/core/compaction/utils.js +0 -0
  162. /package/dist/{pi → sdk}/coding-agent/core/defaults.js +0 -0
  163. /package/dist/{pi → sdk}/coding-agent/core/diagnostics.js +0 -0
  164. /package/dist/{pi → sdk}/coding-agent/core/event-bus.js +0 -0
  165. /package/dist/{pi → sdk}/coding-agent/core/exec.js +0 -0
  166. /package/dist/{pi → sdk}/coding-agent/core/extensions/index.js +0 -0
  167. /package/dist/{pi → sdk}/coding-agent/core/extensions/types.js +0 -0
  168. /package/dist/{pi → sdk}/coding-agent/core/extensions/wrapper.js +0 -0
  169. /package/dist/{pi → sdk}/coding-agent/core/footer-data-provider.js +0 -0
  170. /package/dist/{pi → sdk}/coding-agent/core/http-dispatcher.js +0 -0
  171. /package/dist/{pi → sdk}/coding-agent/core/index.js +0 -0
  172. /package/dist/{pi → sdk}/coding-agent/core/messages.js +0 -0
  173. /package/dist/{pi → sdk}/coding-agent/core/model-resolver.js +0 -0
  174. /package/dist/{pi → sdk}/coding-agent/core/output-guard.js +0 -0
  175. /package/dist/{pi → sdk}/coding-agent/core/prompt-templates.js +0 -0
  176. /package/dist/{pi → sdk}/coding-agent/core/provider-display-names.js +0 -0
  177. /package/dist/{pi → sdk}/coding-agent/core/resolve-config-value.js +0 -0
  178. /package/dist/{pi → sdk}/coding-agent/core/resource-loader.js +0 -0
  179. /package/dist/{pi → sdk}/coding-agent/core/session-cwd.js +0 -0
  180. /package/dist/{pi → sdk}/coding-agent/core/skills.js +0 -0
  181. /package/dist/{pi → sdk}/coding-agent/core/slash-commands.js +0 -0
  182. /package/dist/{pi → sdk}/coding-agent/core/source-info.js +0 -0
  183. /package/dist/{pi → sdk}/coding-agent/core/system-prompt.js +0 -0
  184. /package/dist/{pi → sdk}/coding-agent/core/timings.js +0 -0
  185. /package/dist/{pi → sdk}/coding-agent/core/tools/edit-diff.js +0 -0
  186. /package/dist/{pi → sdk}/coding-agent/core/tools/file-mutation-queue.js +0 -0
  187. /package/dist/{pi → sdk}/coding-agent/core/tools/index.js +0 -0
  188. /package/dist/{pi → sdk}/coding-agent/core/tools/output-accumulator.js +0 -0
  189. /package/dist/{pi → sdk}/coding-agent/core/tools/path-utils.js +0 -0
  190. /package/dist/{pi → sdk}/coding-agent/core/tools/tool-definition-wrapper.js +0 -0
  191. /package/dist/{pi → sdk}/coding-agent/core/tools/truncate.js +0 -0
  192. /package/dist/{pi → sdk}/coding-agent/index.js +0 -0
  193. /package/dist/{pi → sdk}/coding-agent/main.js +0 -0
  194. /package/dist/{pi → sdk}/coding-agent/modes/index.js +0 -0
  195. /package/dist/{pi → sdk}/coding-agent/modes/interactive/components/diff.js +0 -0
  196. /package/dist/{pi → sdk}/coding-agent/modes/interactive/interactive-mode.js +0 -0
  197. /package/dist/{pi → sdk}/coding-agent/modes/print-mode.js +0 -0
  198. /package/dist/{pi → sdk}/coding-agent/modes/rpc/jsonl.js +0 -0
  199. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-client.js +0 -0
  200. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-mode.js +0 -0
  201. /package/dist/{pi → sdk}/coding-agent/modes/rpc/rpc-types.js +0 -0
  202. /package/dist/{pi → sdk}/coding-agent/utils/ansi.js +0 -0
  203. /package/dist/{pi → sdk}/coding-agent/utils/changelog.js +0 -0
  204. /package/dist/{pi → sdk}/coding-agent/utils/child-process.js +0 -0
  205. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-image.js +0 -0
  206. /package/dist/{pi → sdk}/coding-agent/utils/clipboard-native.js +0 -0
  207. /package/dist/{pi → sdk}/coding-agent/utils/clipboard.js +0 -0
  208. /package/dist/{pi → sdk}/coding-agent/utils/exif-orientation.js +0 -0
  209. /package/dist/{pi → sdk}/coding-agent/utils/frontmatter.js +0 -0
  210. /package/dist/{pi → sdk}/coding-agent/utils/fs-watch.js +0 -0
  211. /package/dist/{pi → sdk}/coding-agent/utils/git.js +0 -0
  212. /package/dist/{pi → sdk}/coding-agent/utils/html.js +0 -0
  213. /package/dist/{pi → sdk}/coding-agent/utils/image-convert.js +0 -0
  214. /package/dist/{pi → sdk}/coding-agent/utils/image-resize.js +0 -0
  215. /package/dist/{pi → sdk}/coding-agent/utils/mime.js +0 -0
  216. /package/dist/{pi → sdk}/coding-agent/utils/paths.js +0 -0
  217. /package/dist/{pi → sdk}/coding-agent/utils/photon.js +0 -0
  218. /package/dist/{pi → sdk}/coding-agent/utils/pi-user-agent.js +0 -0
  219. /package/dist/{pi → sdk}/coding-agent/utils/shell.js +0 -0
  220. /package/dist/{pi → sdk}/coding-agent/utils/sleep.js +0 -0
  221. /package/dist/{pi → sdk}/coding-agent/utils/syntax-highlight.js +0 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Spectral Vision Extension
3
+ *
4
+ * Automatically switches to a vision-capable model for image processing
5
+ * when images are attached. This allows non-vision models to "see" images
6
+ * and saves main agent context by using a potentially smaller/cheaper
7
+ * vision model for image understanding.
8
+ *
9
+ * Logic:
10
+ * - ALWAYS intercepts images (even when main model supports vision),
11
+ * replacing them with text descriptions.
12
+ * - Prefers the admin-configured default vision model (isVisionDefault
13
+ * flag from backend via SettingsManager) for image descriptions.
14
+ * - Falls back to the main model for vision if no admin default is
15
+ * configured and the main model supports images.
16
+ * - If neither is available, falls back to the first available
17
+ * vision-capable model by provider priority.
18
+ *
19
+ * Hooks into the `context` event to intercept ALL images before they reach
20
+ * the LLM (covers user-attached images and tool-result images alike).
21
+ */
22
+ import { streamSimple } from "../sdk/ai/index.js";
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Check if the SettingsManager has an admin-configured default vision model
28
+ * with working auth. Returns the model if found and auth-ready, or undefined.
29
+ */
30
+ function getAdminVisionModel(ctx) {
31
+ const settings = ctx.settingsManager;
32
+ if (!settings)
33
+ return undefined;
34
+ const defaultVisionProvider = settings.getDefaultVisionProvider();
35
+ const defaultVisionModel = settings.getDefaultVisionModel();
36
+ if (!defaultVisionProvider || !defaultVisionModel)
37
+ return undefined;
38
+ const allModels = ctx.modelRegistry.getAll();
39
+ const match = allModels.find((m) => m.provider === defaultVisionProvider && m.id === defaultVisionModel);
40
+ if (!match || !match.input.includes("image"))
41
+ return undefined;
42
+ if (!ctx.modelRegistry.hasConfiguredAuth(match))
43
+ return undefined;
44
+ return match;
45
+ }
46
+ /**
47
+ * Resolve the best vision model to use for image descriptions.
48
+ *
49
+ * Priority:
50
+ * 1. Admin-configured default vision model (isVisionDefault from backend)
51
+ * 2. Current main model if it supports images
52
+ * 3. Best available vision model by provider priority
53
+ * 4. First available vision model
54
+ */
55
+ function resolveVisionModel(ctx) {
56
+ // 1. Admin-configured default vision model (always preferred)
57
+ const adminModel = getAdminVisionModel(ctx);
58
+ if (adminModel) {
59
+ process.stderr.write(`[spectral-vision] Using admin-configured default: ${adminModel.provider}/${adminModel.id}\n`);
60
+ return adminModel;
61
+ }
62
+ // 2. Current main model if it supports images
63
+ const currentModel = ctx.model;
64
+ if (currentModel?.input.includes("image") && ctx.modelRegistry.hasConfiguredAuth(currentModel)) {
65
+ process.stderr.write(`[spectral-vision] Using main model for vision: ${currentModel.provider}/${currentModel.id}\n`);
66
+ return currentModel;
67
+ }
68
+ // 3. Fall back to best available vision model by provider priority
69
+ const allModels = ctx.modelRegistry.getAll();
70
+ const visionModels = allModels.filter((m) => m.input.includes("image") && ctx.modelRegistry.hasConfiguredAuth(m));
71
+ if (visionModels.length === 0)
72
+ return undefined;
73
+ const providerPriority = ["anthropic", "openai", "google", "openrouter"];
74
+ for (const provider of providerPriority) {
75
+ const match = visionModels.find((m) => m.provider === provider);
76
+ if (match) {
77
+ process.stderr.write(`[spectral-vision] Using fallback vision model: ${match.provider}/${match.id}\n`);
78
+ return match;
79
+ }
80
+ }
81
+ // 4. First available vision model
82
+ const fallback = visionModels[0];
83
+ process.stderr.write(`[spectral-vision] Using first available vision model: ${fallback.provider}/${fallback.id}\n`);
84
+ return fallback;
85
+ }
86
+ /** Check if any content block in an array is an image */
87
+ function hasImageContent(content) {
88
+ return Array.isArray(content) && content.some((c) => c?.type === "image");
89
+ }
90
+ /** Count images in a message */
91
+ function countImages(msg) {
92
+ if (!Array.isArray(msg.content))
93
+ return 0;
94
+ return msg.content.filter((c) => c.type === "image").length;
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // Core: call vision model to describe images
98
+ // ---------------------------------------------------------------------------
99
+ async function describeImages(visionModel, content, contextText, ctx) {
100
+ const userContent = [
101
+ {
102
+ type: "text",
103
+ text: `Please describe the following image(s). ` +
104
+ (contextText ? `Context: ${contextText}. ` : "") +
105
+ "Focus on what is visually visible — text content, UI elements, diagrams, " +
106
+ "code structure, layout, colors, etc. Be concise but thorough. " +
107
+ "If multiple images are provided, describe each one separately with a heading.",
108
+ },
109
+ ];
110
+ const textPrefixBlocks = [];
111
+ let imageCount = 0;
112
+ for (const block of content) {
113
+ if (block.type === "image") {
114
+ userContent.push(block);
115
+ imageCount++;
116
+ }
117
+ else if (block.type === "text") {
118
+ textPrefixBlocks.push(block);
119
+ }
120
+ }
121
+ if (imageCount === 0)
122
+ return content;
123
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(visionModel);
124
+ if (!auth.ok) {
125
+ process.stderr.write(`[spectral-vision] No API key for vision model ${visionModel.provider}/${visionModel.id}\n`);
126
+ return content;
127
+ }
128
+ try {
129
+ const result = await streamSimple(visionModel, {
130
+ systemPrompt: "You are an image description assistant. Describe images accurately and concisely.",
131
+ messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
132
+ tools: [],
133
+ }, {
134
+ apiKey: auth.apiKey,
135
+ headers: auth.headers,
136
+ });
137
+ let description = "";
138
+ for await (const event of result) {
139
+ if (event.type === "text_delta") {
140
+ description += event.delta;
141
+ }
142
+ }
143
+ const textNote = description.trim()
144
+ ? `[Image description from ${visionModel.provider}/${visionModel.id} (${imageCount} image(s)):\n${description}\n]`
145
+ : `[${imageCount} image(s) — vision model returned empty description]`;
146
+ // Notify the frontend that the vision extension analyzed the image(s)
147
+ try {
148
+ ctx.ui.notify(`[spectral-vision] Analyzed ${imageCount} image(s) with ${visionModel.provider}/${visionModel.id}`, "info");
149
+ }
150
+ catch {
151
+ // UI context may not be available in headless modes
152
+ }
153
+ return [
154
+ ...textPrefixBlocks,
155
+ { type: "text", text: textNote },
156
+ ];
157
+ }
158
+ catch (err) {
159
+ const msg = err instanceof Error ? err.message : String(err);
160
+ process.stderr.write(`[spectral-vision] Vision call failed: ${msg}\n`);
161
+ // Notify frontend about the failure
162
+ try {
163
+ ctx.ui.notify(`[spectral-vision] Failed to analyze ${imageCount} image(s): ${msg}`, "warning");
164
+ }
165
+ catch {
166
+ // UI context may not be available in headless modes
167
+ }
168
+ return [
169
+ ...textPrefixBlocks,
170
+ {
171
+ type: "text",
172
+ text: `[${imageCount} image(s) omitted — vision fallback failed: ${msg}]`,
173
+ },
174
+ ];
175
+ }
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // Extension entry point
179
+ // ---------------------------------------------------------------------------
180
+ export default function spectralVisionExtension(pi) {
181
+ let visionModel;
182
+ pi.on("session_start", (_event, ctx) => {
183
+ visionModel = resolveVisionModel(ctx);
184
+ if (visionModel) {
185
+ process.stderr.write(`[spectral-vision] Ready — using ${visionModel.provider}/${visionModel.id} for image descriptions.\n`);
186
+ }
187
+ else {
188
+ process.stderr.write("[spectral-vision] No vision model with auth configured. Image description disabled.\n");
189
+ }
190
+ });
191
+ pi.on("context", async (event, ctx) => {
192
+ const messages = event.messages;
193
+ // Check if any message contains images
194
+ let totalImages = 0;
195
+ for (const msg of messages) {
196
+ totalImages += countImages(msg);
197
+ }
198
+ if (totalImages === 0)
199
+ return;
200
+ // Re-resolve vision model each time images are encountered.
201
+ // This ensures automatic switching to the admin-configured default
202
+ // vision model even when the main model supports vision.
203
+ visionModel = resolveVisionModel(ctx);
204
+ if (!visionModel) {
205
+ process.stderr.write("[spectral-vision] No vision model available, images will be omitted\n");
206
+ return;
207
+ }
208
+ process.stderr.write(`[spectral-vision] Describing ${totalImages} image(s) with ${visionModel.provider}/${visionModel.id}...\n`);
209
+ // Process each message
210
+ const processed = await Promise.all(messages.map(async (msg) => {
211
+ if (msg.role !== "user" || !Array.isArray(msg.content) || !hasImageContent(msg.content)) {
212
+ return msg;
213
+ }
214
+ // Extract context from preceding text blocks
215
+ const textBlocks = msg.content
216
+ .filter((c) => c.type === "text")
217
+ .map((c) => c.text)
218
+ .join(" ");
219
+ const contextText = textBlocks.substring(0, 500); // limit context
220
+ const described = await describeImages(visionModel, msg.content, contextText, ctx);
221
+ return { ...msg, content: described };
222
+ }));
223
+ return { messages: processed };
224
+ });
225
+ }
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  export function getAgentDir() {
4
4
  const configured = process.env.PI_CODING_AGENT_DIR?.trim();
5
5
  if (!configured) {
6
- return join(homedir(), ".pi", "agent");
6
+ return join(homedir(), ".spectral", "agent");
7
7
  }
8
8
  if (configured === "~") {
9
9
  return homedir();
@@ -5,7 +5,7 @@ import { dirname, join, resolve } from "node:path";
5
5
  import { getAgentPath } from "./agent-dir.js";
6
6
  const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
7
7
  const PROJECT_CONFIG_NAME = ".mcp.json";
8
- const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
8
+ const PROJECT_MCP_CONFIG_NAME = ".spectral/mcp.json";
9
9
  const REPOPROMPT_BINARY_CANDIDATES = [
10
10
  join(homedir(), "RepoPrompt", "repoprompt_cli"),
11
11
  "/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp",
@@ -32,7 +32,7 @@ export function getProjectConfigPath(cwd = process.cwd()) {
32
32
  return resolve(cwd, PROJECT_CONFIG_NAME);
33
33
  }
34
34
  export function getProjectPiConfigPath(cwd = process.cwd()) {
35
- return resolve(cwd, PROJECT_PI_CONFIG_NAME);
35
+ return resolve(cwd, PROJECT_MCP_CONFIG_NAME);
36
36
  }
37
37
  export function getConfigDiscoveryPaths(overridePath) {
38
38
  return getConfigSources(overridePath).map((source) => ({
@@ -368,7 +368,7 @@ function findProjectRoot(cwd = process.cwd()) {
368
368
  if (existsSync(join(current, ".git"))
369
369
  || existsSync(join(current, "package.json"))
370
370
  || existsSync(join(current, PROJECT_CONFIG_NAME))
371
- || existsSync(join(current, ".pi"))) {
371
+ || existsSync(join(current, ".spectral"))) {
372
372
  return current;
373
373
  }
374
374
  const parent = dirname(current);
@@ -1,4 +1,4 @@
1
- import { complete } from "../pi/ai/index.js";
1
+ import { complete } from "../sdk/ai/index.js";
2
2
  import { truncateAtWord } from "./utils.js";
3
3
  import { CreateMessageRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  export function registerSamplingHandler(client, options) {
@@ -96,6 +96,8 @@ export class McpServerManager {
96
96
  }
97
97
  // Check for UnauthorizedError — server requires OAuth.
98
98
  // This can fire from createHttpTransport probe or client.connect.
99
+ // transport may be undefined if createHttpTransport threw during the probe
100
+ // before returning a transport; close() handles this safely.
99
101
  if (error instanceof UnauthorizedError && supportsOAuth(definition)) {
100
102
  return {
101
103
  client,
@@ -239,7 +241,9 @@ export class McpServerManager {
239
241
  connection.status = "closed";
240
242
  this.connections.delete(name);
241
243
  await connection.client.close().catch(() => { });
242
- await connection.transport.close().catch(() => { });
244
+ if (connection.transport) {
245
+ await connection.transport.close().catch(() => { });
246
+ }
243
247
  }
244
248
  async closeAll() {
245
249
  const names = [...this.connections.keys()];
@@ -1,4 +1,4 @@
1
- import { SettingsManager } from "../../pi/coding-agent/index.js";
1
+ import { SettingsManager } from "../../sdk/coding-agent/index.js";
2
2
  import { getMemoryState, rawTokensSinceLastBound, rawTokensSinceLastCompaction, } from "../branch.js";
3
3
  import { observationPoolTokens as estimateObservationPoolTokens } from "../compaction.js";
4
4
  import { countByRelevance, formatRelevanceHistogram } from "../relevance.js";
@@ -39,14 +39,14 @@ export function registerStatusCommand(pi, runtime) {
39
39
  const pObsLabel = pendingObsCount === 1 ? "observation" : "observations";
40
40
  const passiveLines = runtime.config.passive === true
41
41
  ? [
42
- "── Mode ──",
42
+ "### Mode",
43
43
  "Passive: proactive observation and compaction triggers disabled; compaction hook remains active",
44
44
  "",
45
45
  ]
46
46
  : [];
47
47
  const activityLines = runtime.config.passive === true
48
48
  ? [
49
- "── Activity ──",
49
+ "### Activity",
50
50
  `Observation trigger: passive (~${sinceBound.toLocaleString()} / ${obsThreshold.toLocaleString()} tokens, ${obsPct}%)`,
51
51
  " → proactive observation is disabled; manual/Pi compaction can still run sync catch-up observation",
52
52
  `Compaction trigger: passive (~${sinceCompaction.toLocaleString()} / ${compThreshold.toLocaleString()} tokens, ${compPct}%)`,
@@ -56,7 +56,7 @@ export function registerStatusCommand(pi, runtime) {
56
56
  ` distilled from them and redundant observations are pruned away`,
57
57
  ]
58
58
  : [
59
- "── Activity ──",
59
+ "### Activity",
60
60
  `Next observation: ~${sinceBound.toLocaleString()} / ${obsThreshold.toLocaleString()} tokens (${obsPct}%)`,
61
61
  ` → at ${obsThreshold.toLocaleString()} tokens, recent conversation is compressed into new observations`,
62
62
  `Next compaction: ~${sinceCompaction.toLocaleString()} / ${compThreshold.toLocaleString()} tokens (${compPct}%)`,
@@ -68,7 +68,7 @@ export function registerStatusCommand(pi, runtime) {
68
68
  ];
69
69
  const lines = [
70
70
  ...passiveLines,
71
- "── Memory ──",
71
+ "### Memory",
72
72
  `Reflections: ~${committedRefsTokens.toLocaleString()} tokens (${committedRefsCount} ${refLabel}) — durable insights`,
73
73
  `Observations:`,
74
74
  ` committed ~${committedObsTokens.toLocaleString()} tokens (${committedObsCount} ${cObsLabel}) — folded into last compaction`,
@@ -79,7 +79,7 @@ export function registerStatusCommand(pi, runtime) {
79
79
  ];
80
80
  if (runtime.observerInFlight || runtime.compactInFlight) {
81
81
  lines.push("");
82
- lines.push("── In flight ──");
82
+ lines.push("### In Flight");
83
83
  if (runtime.observerInFlight)
84
84
  lines.push("Observer: running");
85
85
  if (runtime.compactInFlight)
@@ -1,8 +1,8 @@
1
1
  import { getMemoryState } from "../branch.js";
2
- import { observationPoolTokens as estimateObservationPoolTokens } from "../compaction.js";
2
+ import { observationPoolTokens as estimateObservationPoolTokens, observationsToMarkdownList, reflectionsToMarkdownList, } from "../compaction.js";
3
3
  import { countByRelevance, formatRelevanceHistogram } from "../relevance.js";
4
4
  import { estimateStringTokens } from "../tokens.js";
5
- import { reflectionContent, reflectionToPromptLine } from "../types.js";
5
+ import { reflectionContent } from "../types.js";
6
6
  export function registerViewCommand(pi, runtime) {
7
7
  pi.registerCommand("om-view", {
8
8
  description: "Print observational memory details (reflections + observations)",
@@ -22,39 +22,41 @@ export function registerViewCommand(pi, runtime) {
22
22
  const totalTokens = committedRefTokens + totalObsTokens;
23
23
  const relevanceHistogram = countByRelevance([...committedObs, ...pendingObs]);
24
24
  const plural = (n, singular, plural) => (n === 1 ? singular : plural);
25
- const renderObs = (r) => `[${r.id}] ${r.timestamp} [${r.relevance}] ${r.content}`;
26
25
  const sections = [];
27
- sections.push(`Memory: ${committedRefCount} ${plural(committedRefCount, "reflection", "reflections")} · ` +
26
+ sections.push(`## Memory Overview\n\n` +
27
+ `${committedRefCount} ${plural(committedRefCount, "reflection", "reflections")} · ` +
28
28
  `${totalObsCount} ${plural(totalObsCount, "observation", "observations")} ` +
29
29
  `(${committedObsCount} committed, ${pendingObsCount} pending) · ` +
30
30
  `~${totalTokens.toLocaleString()} tokens · ` +
31
31
  `relevance ${formatRelevanceHistogram(relevanceHistogram)}`);
32
+ sections.push(`## Reflections (${committedRefCount} ${plural(committedRefCount, "entry", "entries")}, ~${committedRefTokens.toLocaleString()} tokens)`);
32
33
  sections.push("");
33
- sections.push(`── Reflections (${committedRefCount} ${plural(committedRefCount, "entry", "entries")}, ~${committedRefTokens.toLocaleString()} tokens) ──`);
34
34
  if (committedRefItems.length > 0) {
35
- sections.push(committedRefItems.map(reflectionToPromptLine).join("\n\n"));
35
+ sections.push(reflectionsToMarkdownList(committedRefItems));
36
36
  }
37
37
  else {
38
- sections.push("(none)");
38
+ sections.push("*(none)*");
39
39
  }
40
40
  sections.push("");
41
- sections.push(`── Observations committed (${committedObsCount} ${plural(committedObsCount, "observation", "observations")}, ~${committedObsTokens.toLocaleString()} tokens) ──`);
41
+ sections.push(`## Observations (committed: ${committedObsCount} ${plural(committedObsCount, "observation", "observations")}, ~${committedObsTokens.toLocaleString()} tokens)`);
42
+ sections.push("");
42
43
  if (committedObs.length > 0) {
43
- sections.push(committedObs.map(renderObs).join("\n"));
44
+ sections.push(observationsToMarkdownList(committedObs));
44
45
  }
45
46
  else {
46
- sections.push("(none)");
47
+ sections.push("*(none)*");
47
48
  }
48
49
  sections.push("");
49
- sections.push(`── Observations pending (${pendingObsCount} ${plural(pendingObsCount, "observation", "observations")}, ~${pendingObsTokens.toLocaleString()} tokens) ──`);
50
+ sections.push(`## Observations (pending: ${pendingObsCount} ${plural(pendingObsCount, "observation", "observations")}, ~${pendingObsTokens.toLocaleString()} tokens)`);
51
+ sections.push("");
50
52
  if (pendingObs.length > 0) {
51
- sections.push(pendingObs.map(renderObs).join("\n"));
53
+ sections.push(observationsToMarkdownList(pendingObs));
52
54
  }
53
55
  else {
54
- sections.push("(none)");
56
+ sections.push("*(none)*");
55
57
  }
56
58
  sections.push("");
57
- sections.push("Tip: use /tree to browse the raw messages still live in the session.");
59
+ sections.push("*Tip: use /tree to browse the raw messages still live in the session.*");
58
60
  ctx.ui.notify(sections.join("\n"), "info");
59
61
  },
60
62
  });
@@ -1,5 +1,5 @@
1
- import { agentLoop } from "../pi/agent-core/index.js";
2
- import { Type } from "../pi/ai/index.js";
1
+ import { agentLoop } from "../sdk/agent-core/index.js";
2
+ import { Type } from "../sdk/ai/index.js";
3
3
  import { debugLog, isDebugLogEnabled } from "./debug-log.js";
4
4
  import { hashId } from "./ids.js";
5
5
  import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "./model-budget.js";
@@ -798,16 +798,44 @@ export async function runPruner(args, reflections, observations, budgetTokens, o
798
798
  });
799
799
  return result;
800
800
  }
801
+ /**
802
+ * Format an observation for Markdown-safe display.
803
+ * Wraps the ID in backticks so it renders as inline code in Markdown
804
+ * rather than being misinterpreted as a link reference.
805
+ */
806
+ export function observationToMarkdown(record) {
807
+ return `[\`${record.id}\`] ${record.timestamp} [${record.relevance}] ${record.content}`;
808
+ }
809
+ /**
810
+ * Format a reflection for Markdown-safe display.
811
+ * Wraps the ID in backticks so it renders as inline code in Markdown.
812
+ */
813
+ export function reflectionToMarkdown(reflection) {
814
+ if (typeof reflection === "string")
815
+ return reflection;
816
+ return `[\`${reflection.id}\`] ${reflection.content}`;
817
+ }
818
+ /**
819
+ * Format observations as a Markdown bullet list for display.
820
+ */
821
+ export function observationsToMarkdownList(observations) {
822
+ return observations.map((o) => `- ${observationToMarkdown(o)}`).join("\n");
823
+ }
824
+ /**
825
+ * Format reflections as a Markdown bullet list for display.
826
+ */
827
+ export function reflectionsToMarkdownList(reflections) {
828
+ return reflections.map((r) => `- ${reflectionToMarkdown(r)}`).join("\n");
829
+ }
801
830
  export function renderSummary(reflections, observations) {
802
831
  if (reflections.length === 0 && observations.length === 0)
803
832
  return "";
804
833
  const parts = [CONTEXT_USAGE_INSTRUCTIONS];
805
834
  if (reflections.length > 0) {
806
- parts.push(`## Reflections\n${reflections.map(reflectionToPromptLine).join("\n")}`);
835
+ parts.push(`## Reflections\n${reflectionsToMarkdownList(reflections)}`);
807
836
  }
808
837
  if (observations.length > 0) {
809
- const body = observationsToPromptLines(observations).join("\n");
810
- parts.push(`## Observations\n${body}`);
838
+ parts.push(`## Observations\n${observationsToMarkdownList(observations)}`);
811
839
  }
812
840
  return parts.join("\n\n");
813
841
  }
@@ -1,10 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { getAgentDir } from "../pi/coding-agent/index.js";
3
+ import { getAgentDir } from "../sdk/coding-agent/index.js";
4
4
  export const DEFAULTS = {
5
5
  observationThresholdTokens: 1_000,
6
6
  compactionThresholdTokens: 50_000,
7
- reflectionThresholdTokens: 30_000,
7
+ reflectionThresholdTokens: 10_000,
8
8
  passive: false,
9
9
  debugLog: false,
10
10
  observerMaxChunkTokens: 30_000,
@@ -72,7 +72,7 @@ function readNamespacedConfig(path) {
72
72
  }
73
73
  export function loadConfig(cwd, env = process.env) {
74
74
  const globalPath = join(getAgentDir(), "settings.json");
75
- const projectPath = join(cwd, ".pi", "settings.json");
75
+ const projectPath = join(cwd, ".spectral", "settings.json");
76
76
  return {
77
77
  ...DEFAULTS,
78
78
  ...readNamespacedConfig(globalPath),
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { existsSync, mkdirSync, renameSync, statSync, unlinkSync, appendFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { getAgentDir } from "../pi/coding-agent/index.js";
4
+ import { getAgentDir } from "../sdk/coding-agent/index.js";
5
5
  export const DEBUG_LOG_MAX_BYTES = 10 * 1024 * 1024;
6
6
  export const DEBUG_LOG_RELATIVE_PATH = join("observational-memory", "debug.ndjson");
7
7
  const storage = new AsyncLocalStorage();
@@ -1,5 +1,5 @@
1
- import { agentLoop } from "../pi/agent-core/index.js";
2
- import { Type } from "../pi/ai/index.js";
1
+ import { agentLoop } from "../sdk/agent-core/index.js";
2
+ import { Type } from "../sdk/ai/index.js";
3
3
  import { hashId } from "./ids.js";
4
4
  import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "./model-budget.js";
5
5
  import { OBSERVER_SYSTEM } from "./prompts.js";
@@ -92,7 +92,7 @@ Your job is to compress a chunk of recent conversation into timestamped, rated o
92
92
 
93
93
  You receive:
94
94
  - Current reflections (long-lived facts already crystallized).
95
- - Current observations (already-recorded observations, each shown as "[id] YYYY-MM-DD HH:MM [relevance] content").
95
+ - Current observations (already-recorded observations, each shown as "[\`id\`] YYYY-MM-DD HH:MM [relevance] content").
96
96
  - A new chunk of conversation with source entry labels and inline message timestamps. Each source block starts with "[Source entry id: <id>]" followed by content formatted as "[User @ YYYY-MM-DD HH:MM]:", "[Assistant @ ...]:", "[Tool result for <name> @ ...]:", custom messages, or branch summaries.
97
97
  - A current local time fallback for observations that have no obvious message timestamp.
98
98
 
@@ -141,7 +141,7 @@ Your task is different from the observer's: you are not recording events, you ar
141
141
 
142
142
  You receive:
143
143
  - Current reflections (already-crystallized long-lived facts, one per line). Newer reflections may begin with a bracketed id handle; treat that id as recall metadata, not as part of the reflection prose.
144
- - Current observations (timestamped, relevance-tagged events accumulated over many turns). Each is shown as "[id] YYYY-MM-DD HH:MM [relevance] content".
144
+ - Current observations (timestamped, relevance-tagged events accumulated over many turns). Each is shown as "[\`id\`] YYYY-MM-DD HH:MM [relevance] content".
145
145
 
146
146
  How you work:
147
147
  1. Read current reflections and observations to understand what is already crystallized and what new signal exists in the pool.
@@ -198,7 +198,7 @@ ${RELEVANCE_RUBRIC}
198
198
 
199
199
  You receive:
200
200
  - Current reflections (long-lived facts; they survive regardless — treat them as already captured). Newer reflections may begin with a bracketed id handle; treat that id as recall metadata, not as part of the reflection prose.
201
- - Current observations (timestamped, relevance-tagged events to prune). Each is shown as "[id] YYYY-MM-DD HH:MM [relevance] [coverage: tag] content", where id is the 12-character hex handle you reference when dropping.
201
+ - Current observations (timestamped, relevance-tagged events to prune). Each is shown as "[\`id\`] YYYY-MM-DD HH:MM [relevance] [coverage: tag] content", where id is the 12-character hex handle you reference when dropping.
202
202
  - A pressure line stating pool size, target, tokens still to cut, and the current pass strategy.
203
203
 
204
204
  Coverage tags are pruning signals derived from current provenance-backed reflection support ids. They are strong evidence, not blind commands:
@@ -279,8 +279,8 @@ export function buildPrunerPassGuidance(pass, maxPasses) {
279
279
  }
280
280
  export const CONTEXT_USAGE_INSTRUCTIONS = `These are condensed memories from earlier in this session.
281
281
 
282
- - Reflections: stable, long-lived facts about the user, project, decisions, and constraints. New reflection lines may include ids in brackets.
283
- - Observations: timestamped events from the conversation history, in chronological order. Observation lines include ids in brackets.
282
+ - Reflections: stable, long-lived facts about the user, project, decisions, and constraints. New reflection lines may include ids in brackets wrapped in backticks.
283
+ - Observations: timestamped events from the conversation history, in chronological order. Observation lines include ids in brackets wrapped in backticks.
284
284
 
285
285
  Treat these as past records. When entries conflict, the most recent observation reflects the latest known state. Work that prior observations describe as completed should not be redone unless the user explicitly asks to revisit it.
286
286
 
@@ -1,4 +1,4 @@
1
- import { estimateTokens as estimateMessageTokens } from "../pi/coding-agent/index.js";
1
+ import { estimateTokens as estimateMessageTokens } from "../sdk/coding-agent/index.js";
2
2
  export function estimateStringTokens(text) {
3
3
  return Math.ceil(text.length / 4);
4
4
  }
@@ -1,5 +1,5 @@
1
- import { Type } from "../../pi/ai/index.js";
2
- import { defineTool } from "../../pi/coding-agent/index.js";
1
+ import { Type } from "../../sdk/ai/index.js";
2
+ import { defineTool } from "../../sdk/coding-agent/index.js";
3
3
  import { getProjectObsStore } from "../project-observations-store.js";
4
4
  export const READ_PROJECT_OBSERVATIONS_TOOL_NAME = "read_project_observations";
5
5
  export const readProjectObservationsTool = defineTool({
@@ -1,5 +1,5 @@
1
- import { Type } from "../../pi/ai/index.js";
2
- import { defineTool } from "../../pi/coding-agent/index.js";
1
+ import { Type } from "../../sdk/ai/index.js";
2
+ import { defineTool } from "../../sdk/coding-agent/index.js";
3
3
  import { recallMemorySources, } from "../branch.js";
4
4
  import { renderRecallSourceEntries, renderRecallSourceEntry } from "../serialize.js";
5
5
  import { estimateEntryTokens } from "../tokens.js";
@@ -181,10 +181,10 @@ function friendlySourceUnavailableMessage(match) {
181
181
  return `Observation ${match.observation.id} has source entries associated, but some are unavailable on the current branch or are not source-renderable.${missing}${nonSource}`;
182
182
  }
183
183
  function reflectionLineText(reflection) {
184
- return `[${reflection.id}] ${reflection.content}`;
184
+ return `[\`${reflection.id}\`] ${reflection.content}`;
185
185
  }
186
186
  function observationLineText(observation) {
187
- return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
187
+ return `[\`${observation.id}\`] ${observation.timestamp} [${observation.relevance}] ${observation.content}`;
188
188
  }
189
189
  function renderObservationOnlyTextFromResult(result) {
190
190
  const sections = [];