@aexol/spectral 0.7.6 → 0.7.8

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 (57) hide show
  1. package/dist/agent/index.js +16 -140
  2. package/dist/cli.js +25 -220
  3. package/dist/extensions/spectral-vision-fallback.js +188 -0
  4. package/dist/memory/commands/status.js +5 -5
  5. package/dist/memory/commands/view.js +16 -14
  6. package/dist/memory/compaction.js +31 -3
  7. package/dist/memory/prompts.js +5 -5
  8. package/dist/memory/tools/recall-observation.js +2 -2
  9. package/dist/pi/coding-agent/config.js +0 -11
  10. package/dist/pi/coding-agent/core/agent-session.js +3 -17
  11. package/dist/pi/coding-agent/core/extensions/loader.js +0 -6
  12. package/dist/pi/coding-agent/core/extensions/runner.js +7 -1
  13. package/dist/pi/coding-agent/core/keybindings.js +129 -2
  14. package/dist/pi/coding-agent/core/settings-manager.js +20 -0
  15. package/dist/pi/coding-agent/core/tools/bash.js +17 -63
  16. package/dist/pi/coding-agent/core/tools/edit.js +4 -141
  17. package/dist/pi/coding-agent/core/tools/find.js +0 -11
  18. package/dist/pi/coding-agent/core/tools/grep.js +0 -11
  19. package/dist/pi/coding-agent/core/tools/ls.js +0 -11
  20. package/dist/pi/coding-agent/core/tools/read.js +0 -12
  21. package/dist/pi/coding-agent/core/tools/render-utils.js +1 -14
  22. package/dist/pi/coding-agent/core/tools/write.js +2 -97
  23. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +1 -1
  24. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +6 -12
  25. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1 -2
  26. package/dist/relay/models-fetch.js +13 -1
  27. package/dist/server/pi-bridge.js +57 -4
  28. package/dist/server/session-stream.js +7 -1
  29. package/package.json +1 -1
  30. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +0 -248
  31. package/dist/pi/coding-agent/core/export-html/index.js +0 -225
  32. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +0 -107
  33. package/dist/pi/tui/autocomplete.js +0 -631
  34. package/dist/pi/tui/components/box.js +0 -103
  35. package/dist/pi/tui/components/cancellable-loader.js +0 -34
  36. package/dist/pi/tui/components/editor.js +0 -1915
  37. package/dist/pi/tui/components/image.js +0 -88
  38. package/dist/pi/tui/components/input.js +0 -425
  39. package/dist/pi/tui/components/loader.js +0 -68
  40. package/dist/pi/tui/components/markdown.js +0 -633
  41. package/dist/pi/tui/components/select-list.js +0 -158
  42. package/dist/pi/tui/components/settings-list.js +0 -184
  43. package/dist/pi/tui/components/spacer.js +0 -22
  44. package/dist/pi/tui/components/text.js +0 -88
  45. package/dist/pi/tui/components/truncated-text.js +0 -50
  46. package/dist/pi/tui/editor-component.js +0 -1
  47. package/dist/pi/tui/fuzzy.js +0 -109
  48. package/dist/pi/tui/index.js +0 -31
  49. package/dist/pi/tui/keybindings.js +0 -173
  50. package/dist/pi/tui/keys.js +0 -1172
  51. package/dist/pi/tui/kill-ring.js +0 -43
  52. package/dist/pi/tui/stdin-buffer.js +0 -360
  53. package/dist/pi/tui/terminal-image.js +0 -335
  54. package/dist/pi/tui/terminal.js +0 -324
  55. package/dist/pi/tui/tui.js +0 -1076
  56. package/dist/pi/tui/undo-stack.js +0 -24
  57. package/dist/pi/tui/utils.js +0 -1016
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Spectral Vision Extension
3
+ *
4
+ * Automatically describes images using a vision-capable model and replaces
5
+ * raw image data with text descriptions. This saves context for the main agent
6
+ * and allows non-vision models to "see" images.
7
+ *
8
+ * Logic:
9
+ * - ALWAYS intercepts images (even when main model supports vision),
10
+ * replacing them with text descriptions to save main agent context.
11
+ * - Uses the main model for vision descriptions if it supports images.
12
+ * - Falls back to the admin-configured default vision model (isVisionDefault
13
+ * flag from backend via SettingsManager).
14
+ * - If no admin default is configured, falls back to the first available
15
+ * vision-capable model by provider priority.
16
+ *
17
+ * Hooks into the `context` event to intercept ALL images before they reach
18
+ * the LLM (covers user-attached images and tool-result images alike).
19
+ */
20
+ import { streamSimple } from "../pi/ai/index.js";
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+ /** Find the best available vision-capable model from the registry */
25
+ function findVisionModel(ctx) {
26
+ const allModels = ctx.modelRegistry.getAll();
27
+ const availableModels = allModels.filter((m) => ctx.modelRegistry.hasConfiguredAuth(m));
28
+ // Filter to models that support images
29
+ const visionModels = availableModels.filter((m) => m.input.includes("image"));
30
+ if (visionModels.length === 0)
31
+ return undefined;
32
+ // 1. Try admin-configured default vision model from settings
33
+ const settings = ctx.settingsManager;
34
+ if (settings) {
35
+ const defaultVisionProvider = settings.getDefaultVisionProvider();
36
+ const defaultVisionModel = settings.getDefaultVisionModel();
37
+ if (defaultVisionProvider && defaultVisionModel) {
38
+ const match = visionModels.find((m) => m.provider === defaultVisionProvider && m.id === defaultVisionModel);
39
+ if (match) {
40
+ process.stderr.write(`[spectral-vision] Using admin-configured default: ${match.provider}/${match.id}\n`);
41
+ return match;
42
+ }
43
+ }
44
+ }
45
+ // 2. Fall back to provider priority
46
+ const providerPriority = ["anthropic", "openai", "google", "openrouter"];
47
+ for (const provider of providerPriority) {
48
+ const match = visionModels.find((m) => m.provider === provider);
49
+ if (match)
50
+ return match;
51
+ }
52
+ return visionModels[0];
53
+ }
54
+ /** Check if any content block in an array is an image */
55
+ function hasImageContent(content) {
56
+ return Array.isArray(content) && content.some((c) => c?.type === "image");
57
+ }
58
+ /** Count images in a message */
59
+ function countImages(msg) {
60
+ if (!Array.isArray(msg.content))
61
+ return 0;
62
+ return msg.content.filter((c) => c.type === "image").length;
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // Core: call vision model to describe images
66
+ // ---------------------------------------------------------------------------
67
+ async function describeImages(visionModel, content, contextText, ctx) {
68
+ const userContent = [
69
+ {
70
+ type: "text",
71
+ text: `Please describe the following image(s). ` +
72
+ (contextText ? `Context: ${contextText}. ` : "") +
73
+ "Focus on what is visually visible — text content, UI elements, diagrams, " +
74
+ "code structure, layout, colors, etc. Be concise but thorough. " +
75
+ "If multiple images are provided, describe each one separately with a heading.",
76
+ },
77
+ ];
78
+ const textPrefixBlocks = [];
79
+ let imageCount = 0;
80
+ for (const block of content) {
81
+ if (block.type === "image") {
82
+ userContent.push(block);
83
+ imageCount++;
84
+ }
85
+ else if (block.type === "text") {
86
+ textPrefixBlocks.push(block);
87
+ }
88
+ }
89
+ if (imageCount === 0)
90
+ return content;
91
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(visionModel);
92
+ if (!auth.ok) {
93
+ process.stderr.write(`[spectral-vision] No API key for vision model ${visionModel.provider}/${visionModel.id}\n`);
94
+ return content;
95
+ }
96
+ try {
97
+ const result = await streamSimple(visionModel, {
98
+ systemPrompt: "You are an image description assistant. Describe images accurately and concisely.",
99
+ messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
100
+ tools: [],
101
+ }, {
102
+ apiKey: auth.apiKey,
103
+ headers: auth.headers,
104
+ });
105
+ let description = "";
106
+ for await (const event of result) {
107
+ if (event.type === "text_delta") {
108
+ description += event.delta;
109
+ }
110
+ }
111
+ const textNote = description.trim()
112
+ ? `[Image description from ${visionModel.provider}/${visionModel.id} (${imageCount} image(s)):\n${description}\n]`
113
+ : `[${imageCount} image(s) — vision model returned empty description]`;
114
+ return [
115
+ ...textPrefixBlocks,
116
+ { type: "text", text: textNote },
117
+ ];
118
+ }
119
+ catch (err) {
120
+ const msg = err instanceof Error ? err.message : String(err);
121
+ process.stderr.write(`[spectral-vision] Vision call failed: ${msg}\n`);
122
+ return [
123
+ ...textPrefixBlocks,
124
+ {
125
+ type: "text",
126
+ text: `[${imageCount} image(s) omitted — vision fallback failed: ${msg}]`,
127
+ },
128
+ ];
129
+ }
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // Extension entry point
133
+ // ---------------------------------------------------------------------------
134
+ export default function spectralVisionExtension(pi) {
135
+ let visionModel;
136
+ pi.on("session_start", (_event, ctx) => {
137
+ visionModel = findVisionModel(ctx);
138
+ if (visionModel) {
139
+ process.stderr.write(`[spectral-vision] Ready — using ${visionModel.provider}/${visionModel.id} for image descriptions.\n`);
140
+ }
141
+ else {
142
+ process.stderr.write("[spectral-vision] No vision model with auth configured. Image description disabled.\n");
143
+ }
144
+ });
145
+ pi.on("context", async (event, ctx) => {
146
+ const messages = event.messages;
147
+ // Check if any message contains images
148
+ let totalImages = 0;
149
+ for (const msg of messages) {
150
+ totalImages += countImages(msg);
151
+ }
152
+ if (totalImages === 0)
153
+ return;
154
+ // Resolve vision model
155
+ const currentModel = ctx.model;
156
+ // If main model supports images, use it for vision (saves auth setup)
157
+ if (currentModel?.input.includes("image")) {
158
+ visionModel = currentModel;
159
+ }
160
+ else {
161
+ // Refresh vision model
162
+ if (!visionModel ||
163
+ !ctx.modelRegistry.hasConfiguredAuth(visionModel)) {
164
+ visionModel = findVisionModel(ctx);
165
+ }
166
+ }
167
+ if (!visionModel) {
168
+ process.stderr.write("[spectral-vision] No vision model available, images will be omitted\n");
169
+ return;
170
+ }
171
+ process.stderr.write(`[spectral-vision] Describing ${totalImages} image(s) with ${visionModel.provider}/${visionModel.id}...\n`);
172
+ // Process each message
173
+ const processed = await Promise.all(messages.map(async (msg) => {
174
+ if (msg.role !== "user" || !Array.isArray(msg.content) || !hasImageContent(msg.content)) {
175
+ return msg;
176
+ }
177
+ // Extract context from preceding text blocks
178
+ const textBlocks = msg.content
179
+ .filter((c) => c.type === "text")
180
+ .map((c) => c.text)
181
+ .join(" ");
182
+ const contextText = textBlocks.substring(0, 500); // limit context
183
+ const described = await describeImages(visionModel, msg.content, contextText, ctx);
184
+ return { ...msg, content: described };
185
+ }));
186
+ return { messages: processed };
187
+ });
188
+ }
@@ -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
  });
@@ -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
  }
@@ -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
 
@@ -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 = [];
@@ -297,17 +297,6 @@ export function getThemesDir() {
297
297
  /**
298
298
  * Get path to HTML export template directory (shipped with package)
299
299
  * - For Bun binary: export-html/ next to executable
300
- * - For Node.js (dist/): dist/core/export-html/
301
- * - For tsx (src/): src/core/export-html/
302
- */
303
- export function getExportTemplateDir() {
304
- if (isBunBinary) {
305
- return join(getPackageDir(), "export-html");
306
- }
307
- const packageDir = getPackageDir();
308
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
309
- return join(packageDir, srcOrDist, "core", "export-html");
310
- }
311
300
  /** Get path to package.json */
312
301
  export function getPackageJsonPath() {
313
302
  return join(getPackageDir(), "package.json");
@@ -15,7 +15,6 @@
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname } from "node:path";
17
17
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "../../ai/index.js";
18
- import { theme } from "../modes/interactive/theme/theme.js";
19
18
  import { stripFrontmatter } from "../utils/frontmatter.js";
20
19
  import { resolvePath } from "../utils/paths.js";
21
20
  import { sleep } from "../utils/sleep.js";
@@ -23,8 +22,6 @@ import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth
23
22
  import { executeBashWithOperations } from "./bash-executor.js";
24
23
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
25
24
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
26
- import { exportSessionToHtml } from "./export-html/index.js";
27
- import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
28
25
  import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
29
26
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
30
27
  import { expandPromptTemplate } from "./prompt-templates.js";
@@ -1889,7 +1886,7 @@ export class AgentSession {
1889
1886
  extensionsResult.runtime.flagValues.set(name, value);
1890
1887
  }
1891
1888
  }
1892
- this._extensionRunner = new ExtensionRunner(extensionsResult.extensions, extensionsResult.runtime, this._cwd, this.sessionManager, this._modelRegistry);
1889
+ this._extensionRunner = new ExtensionRunner(extensionsResult.extensions, extensionsResult.runtime, this._cwd, this.sessionManager, this._modelRegistry, this.settingsManager);
1893
1890
  if (this._extensionRunnerRef) {
1894
1891
  this._extensionRunnerRef.current = this._extensionRunner;
1895
1892
  }
@@ -2398,19 +2395,8 @@ export class AgentSession {
2398
2395
  * @param outputPath Optional output path (defaults to session directory)
2399
2396
  * @returns Path to exported file
2400
2397
  */
2401
- async exportToHtml(outputPath) {
2402
- const themeName = this.settingsManager.getTheme();
2403
- // Create tool renderer if we have an extension runner (for custom tool HTML rendering)
2404
- const toolRenderer = createToolHtmlRenderer({
2405
- getToolDefinition: (name) => this.getToolDefinition(name),
2406
- theme,
2407
- cwd: this.sessionManager.getCwd(),
2408
- });
2409
- return await exportSessionToHtml(this.sessionManager, this.state, {
2410
- outputPath,
2411
- themeName,
2412
- toolRenderer,
2413
- });
2398
+ async exportToHtml(_outputPath) {
2399
+ throw new Error("HTML export has been removed. Use spectral serve instead.");
2414
2400
  }
2415
2401
  /**
2416
2402
  * Export the current session branch to a JSONL file.
@@ -9,7 +9,6 @@ import { fileURLToPath } from "node:url";
9
9
  import * as _bundledPiAgentCore from "../../../agent-core/index.js";
10
10
  import * as _bundledPiAi from "../../../ai/index.js";
11
11
  import * as _bundledPiAiOauth from "../../../ai/oauth.js";
12
- import * as _bundledPiTui from "../../../tui/index.js";
13
12
  import { createJiti } from "@mariozechner/jiti";
14
13
  // Static imports of packages that extensions may use.
15
14
  // These MUST be static so Bun bundles them into the compiled binary.
@@ -34,12 +33,10 @@ const VIRTUAL_MODULES = {
34
33
  "@sinclair/typebox/compile": _bundledTypeboxCompile,
35
34
  "@sinclair/typebox/value": _bundledTypeboxValue,
36
35
  "../../../agent-core/index.ts": _bundledPiAgentCore,
37
- "../../../tui/index.ts": _bundledPiTui,
38
36
  "../../../ai/index.ts": _bundledPiAi,
39
37
  "../../../ai/oauth.ts": _bundledPiAiOauth,
40
38
  "../../index.ts": _bundledPiCodingAgent,
41
39
  "@mariozechner/pi-agent": _bundledPiAgentCore,
42
- "@mariozechner/pi-tui": _bundledPiTui,
43
40
  "@mariozechner/pi-ai": _bundledPiAi,
44
41
  "@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
45
42
  "@mariozechner/pi-coding-agent": _bundledPiCodingAgent,
@@ -68,18 +65,15 @@ function getAliases() {
68
65
  };
69
66
  const piCodingAgentEntry = packageIndex;
70
67
  const piAgentCoreEntry = resolveWorkspaceOrImport("agent/dist/index.js", "../../../agent-core/index.ts");
71
- const piTuiEntry = resolveWorkspaceOrImport("tui/dist/index.js", "../../../tui/index.ts");
72
68
  const piAiEntry = resolveWorkspaceOrImport("ai/dist/index.js", "../../../ai/index.ts");
73
69
  const piAiOauthEntry = resolveWorkspaceOrImport("ai/dist/oauth.js", "../../../ai/oauth.ts");
74
70
  _aliases = {
75
71
  "../../index.ts": piCodingAgentEntry,
76
72
  "../../../agent-core/index.ts": piAgentCoreEntry,
77
- "../../../tui/index.ts": piTuiEntry,
78
73
  "../../../ai/index.ts": piAiEntry,
79
74
  "../../../ai/oauth.ts": piAiOauthEntry,
80
75
  "@mariozechner/pi-coding-agent": piCodingAgentEntry,
81
76
  "@mariozechner/pi-agent": piAgentCoreEntry,
82
- "@mariozechner/pi-tui": piTuiEntry,
83
77
  "@mariozechner/pi-ai": piAiEntry,
84
78
  "@mariozechner/pi-ai/oauth": piAiOauthEntry,
85
79
  typebox: typeboxEntry,
@@ -113,14 +113,16 @@ export class ExtensionRunner {
113
113
  shutdownHandler = () => { };
114
114
  shortcutDiagnostics = [];
115
115
  commandDiagnostics = [];
116
+ settingsManager;
116
117
  staleMessage;
117
- constructor(extensions, runtime, cwd, sessionManager, modelRegistry) {
118
+ constructor(extensions, runtime, cwd, sessionManager, modelRegistry, settingsManager) {
118
119
  this.extensions = extensions;
119
120
  this.runtime = runtime;
120
121
  this.uiContext = noOpUIContext;
121
122
  this.cwd = cwd;
122
123
  this.sessionManager = sessionManager;
123
124
  this.modelRegistry = modelRegistry;
125
+ this.settingsManager = settingsManager;
124
126
  }
125
127
  bindCore(actions, contextActions, providerActions) {
126
128
  // Copy actions into the shared runtime (all extension APIs reference this)
@@ -399,6 +401,10 @@ export class ExtensionRunner {
399
401
  runner.assertActive();
400
402
  return runner.modelRegistry;
401
403
  },
404
+ get settingsManager() {
405
+ runner.assertActive();
406
+ return runner.settingsManager;
407
+ },
402
408
  get model() {
403
409
  runner.assertActive();
404
410
  return getModel();