@oh-my-pi/pi-coding-agent 15.8.3 → 15.9.1

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 (172) hide show
  1. package/CHANGELOG.md +113 -31
  2. package/dist/types/capability/skill.d.ts +7 -0
  3. package/dist/types/cli/update-cli.d.ts +15 -1
  4. package/dist/types/config/append-only-context-mode.d.ts +8 -0
  5. package/dist/types/config/model-registry.d.ts +3 -0
  6. package/dist/types/config/models-config-schema.d.ts +15 -0
  7. package/dist/types/config/settings-schema.d.ts +38 -24
  8. package/dist/types/debug/protocol-probe.d.ts +38 -0
  9. package/dist/types/debug/terminal-info.d.ts +34 -0
  10. package/dist/types/exa/mcp-client.d.ts +2 -1
  11. package/dist/types/export/html/template.generated.d.ts +1 -1
  12. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  13. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  14. package/dist/types/extensibility/shared-events.d.ts +1 -1
  15. package/dist/types/index.d.ts +1 -0
  16. package/dist/types/mcp/json-rpc.d.ts +6 -1
  17. package/dist/types/mcp/tool-bridge.d.ts +4 -0
  18. package/dist/types/mcp/transports/stdio.d.ts +16 -7
  19. package/dist/types/mnemopi/state.d.ts +2 -2
  20. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  21. package/dist/types/modes/components/assistant-message.d.ts +3 -2
  22. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
  23. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  24. package/dist/types/modes/components/plugin-settings.d.ts +40 -8
  25. package/dist/types/modes/components/session-selector.d.ts +8 -3
  26. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  27. package/dist/types/modes/components/todo-reminder.d.ts +1 -1
  28. package/dist/types/modes/components/transcript-container.d.ts +36 -0
  29. package/dist/types/modes/interactive-mode.d.ts +2 -1
  30. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  31. package/dist/types/modes/theme/theme.d.ts +20 -1
  32. package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
  33. package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
  34. package/dist/types/session/agent-session.d.ts +5 -2
  35. package/dist/types/session/history-storage.d.ts +3 -4
  36. package/dist/types/session/indexed-session-storage.d.ts +59 -0
  37. package/dist/types/session/messages.d.ts +1 -0
  38. package/dist/types/session/redis-session-storage.d.ts +12 -85
  39. package/dist/types/session/session-manager.d.ts +21 -0
  40. package/dist/types/session/session-storage.d.ts +5 -7
  41. package/dist/types/session/sql-session-storage.d.ts +16 -85
  42. package/dist/types/slash-commands/types.d.ts +17 -4
  43. package/dist/types/task/executor.d.ts +9 -0
  44. package/dist/types/task/index.d.ts +3 -1
  45. package/dist/types/telemetry-export.d.ts +19 -0
  46. package/dist/types/tiny/compiled-runtime.d.ts +35 -0
  47. package/dist/types/tiny/text.d.ts +17 -0
  48. package/dist/types/tools/ask.d.ts +1 -0
  49. package/dist/types/tools/index.d.ts +4 -2
  50. package/dist/types/tools/path-utils.d.ts +1 -1
  51. package/dist/types/tools/search.d.ts +2 -2
  52. package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
  53. package/dist/types/utils/session-color.d.ts +7 -2
  54. package/dist/types/web/search/index.d.ts +1 -1
  55. package/dist/types/web/search/provider.d.ts +2 -2
  56. package/dist/types/web/search/providers/base.d.ts +14 -0
  57. package/dist/types/web/search/providers/exa.d.ts +9 -0
  58. package/dist/types/web/search/providers/perplexity.d.ts +2 -2
  59. package/dist/types/web/search/types.d.ts +66 -1
  60. package/package.json +15 -9
  61. package/scripts/build-binary.ts +12 -0
  62. package/src/capability/skill.ts +7 -0
  63. package/src/cli/args.ts +1 -1
  64. package/src/cli/session-picker.ts +1 -0
  65. package/src/cli/update-cli.ts +54 -2
  66. package/src/commands/completions.ts +1 -1
  67. package/src/config/append-only-context-mode.ts +37 -0
  68. package/src/config/models-config-schema.ts +1 -0
  69. package/src/config/settings-schema.ts +24 -57
  70. package/src/debug/index.ts +67 -1
  71. package/src/debug/protocol-probe.ts +267 -0
  72. package/src/debug/terminal-info.ts +127 -0
  73. package/src/exa/mcp-client.ts +11 -5
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +3 -3
  76. package/src/extensibility/custom-tools/types.ts +1 -1
  77. package/src/extensibility/extensions/types.ts +11 -0
  78. package/src/extensibility/shared-events.ts +1 -1
  79. package/src/extensibility/skills.ts +3 -3
  80. package/src/index.ts +1 -0
  81. package/src/internal-urls/docs-index.generated.ts +7 -7
  82. package/src/main.ts +16 -2
  83. package/src/mcp/json-rpc.ts +8 -0
  84. package/src/mcp/render.ts +3 -0
  85. package/src/mcp/tool-bridge.ts +10 -2
  86. package/src/mcp/transports/http.ts +33 -16
  87. package/src/mcp/transports/stdio.ts +37 -12
  88. package/src/mnemopi/state.ts +4 -4
  89. package/src/modes/acp/acp-agent.ts +168 -3
  90. package/src/modes/acp/acp-event-mapper.ts +7 -7
  91. package/src/modes/components/agent-dashboard.ts +103 -31
  92. package/src/modes/components/assistant-message.ts +3 -2
  93. package/src/modes/components/extensions/extension-dashboard.ts +56 -10
  94. package/src/modes/components/history-search.ts +128 -14
  95. package/src/modes/components/hook-selector.ts +149 -14
  96. package/src/modes/components/plugin-settings.ts +270 -36
  97. package/src/modes/components/session-selector.ts +81 -19
  98. package/src/modes/components/settings-selector.ts +1 -1
  99. package/src/modes/components/status-line/segments.ts +2 -1
  100. package/src/modes/components/status-line.ts +1 -1
  101. package/src/modes/components/tips.txt +6 -2
  102. package/src/modes/components/todo-reminder.ts +1 -1
  103. package/src/modes/components/tool-execution.ts +9 -4
  104. package/src/modes/components/transcript-container.ts +109 -0
  105. package/src/modes/controllers/command-controller.ts +4 -3
  106. package/src/modes/controllers/event-controller.ts +12 -7
  107. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  108. package/src/modes/controllers/input-controller.ts +10 -5
  109. package/src/modes/controllers/selector-controller.ts +30 -19
  110. package/src/modes/controllers/todo-command-controller.ts +1 -1
  111. package/src/modes/interactive-mode.ts +56 -9
  112. package/src/modes/print-mode.ts +5 -0
  113. package/src/modes/rpc/rpc-types.ts +1 -1
  114. package/src/modes/theme/theme.ts +48 -8
  115. package/src/modes/utils/keybinding-matchers.ts +10 -0
  116. package/src/modes/utils/ui-helpers.ts +1 -0
  117. package/src/plan-mode/plan-handoff.ts +37 -0
  118. package/src/priority.json +4 -0
  119. package/src/prompts/goals/goal-continuation.md +1 -1
  120. package/src/prompts/steering/user-interjection.md +10 -0
  121. package/src/prompts/system/agent-creation-architect.md +1 -26
  122. package/src/prompts/system/eager-todo.md +3 -3
  123. package/src/prompts/system/orchestrate-notice.md +5 -5
  124. package/src/prompts/system/plan-mode-approved.md +14 -17
  125. package/src/prompts/system/plan-mode-reference.md +3 -6
  126. package/src/prompts/system/subagent-system-prompt.md +11 -0
  127. package/src/prompts/system/system-prompt.md +143 -145
  128. package/src/prompts/system/title-system.md +3 -2
  129. package/src/prompts/system/workflow-notice.md +1 -1
  130. package/src/prompts/tools/browser.md +33 -30
  131. package/src/prompts/tools/render-mermaid.md +2 -2
  132. package/src/prompts/tools/search.md +1 -1
  133. package/src/prompts/tools/task.md +3 -1
  134. package/src/prompts/tools/{todo-write.md → todo.md} +5 -5
  135. package/src/sdk.ts +6 -21
  136. package/src/session/agent-session.ts +44 -21
  137. package/src/session/history-storage.ts +11 -18
  138. package/src/session/indexed-session-storage.ts +430 -0
  139. package/src/session/messages.ts +80 -0
  140. package/src/session/redis-session-storage.ts +66 -377
  141. package/src/session/session-manager.ts +109 -23
  142. package/src/session/session-storage.ts +148 -68
  143. package/src/session/sql-session-storage.ts +131 -382
  144. package/src/slash-commands/helpers/todo.ts +2 -2
  145. package/src/slash-commands/types.ts +27 -10
  146. package/src/task/executor.ts +9 -1
  147. package/src/task/index.ts +51 -1
  148. package/src/telemetry-export.ts +126 -0
  149. package/src/tiny/compiled-runtime.ts +179 -0
  150. package/src/tiny/text.ts +112 -1
  151. package/src/tiny/worker.ts +24 -2
  152. package/src/tools/ask.ts +133 -87
  153. package/src/tools/fetch.ts +17 -4
  154. package/src/tools/find.ts +2 -2
  155. package/src/tools/index.ts +6 -4
  156. package/src/tools/memory-recall.ts +1 -1
  157. package/src/tools/memory-reflect.ts +1 -1
  158. package/src/tools/path-utils.ts +16 -7
  159. package/src/tools/renderers.ts +2 -2
  160. package/src/tools/search.ts +6 -3
  161. package/src/tools/ssh.ts +26 -10
  162. package/src/tools/{todo-write.ts → todo.ts} +32 -35
  163. package/src/tools/write.ts +14 -2
  164. package/src/tui/status-line.ts +15 -4
  165. package/src/utils/session-color.ts +39 -14
  166. package/src/utils/title-generator.ts +9 -2
  167. package/src/web/search/index.ts +4 -2
  168. package/src/web/search/provider.ts +19 -35
  169. package/src/web/search/providers/base.ts +17 -0
  170. package/src/web/search/providers/exa.ts +111 -7
  171. package/src/web/search/providers/perplexity.ts +8 -4
  172. package/src/web/search/types.ts +74 -32
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Terminal protocol smoke-test panel for the debug menu.
3
+ *
4
+ * Exercises every "special" escape protocol the renderer can emit so a human
5
+ * can eyeball which ones the active terminal actually honors:
6
+ * - SGR text styling + 24-bit truecolor,
7
+ * - OSC 8 hyperlinks,
8
+ * - OSC 66 text sizing (large text),
9
+ * - inline graphics (Kitty / iTerm2 / Sixel),
10
+ * - OSC 9 / OSC 99 desktop notifications (fired by the caller).
11
+ *
12
+ * The sample image is generated in-process (a deterministic RGB gradient PNG)
13
+ * so the graphics test needs no asset on disk and works across all three image
14
+ * protocols, each of which decodes a standard PNG.
15
+ */
16
+ import * as zlib from "node:zlib";
17
+ import {
18
+ type Component,
19
+ Container,
20
+ encodeTextSized,
21
+ Image,
22
+ type ImageBudget,
23
+ ImageProtocol,
24
+ NotifyProtocol,
25
+ Spacer,
26
+ TERMINAL,
27
+ Text,
28
+ type TextSizingScale,
29
+ } from "@oh-my-pi/pi-tui";
30
+ import { DynamicBorder } from "../modes/components/dynamic-border";
31
+ import { theme } from "../modes/theme/theme";
32
+
33
+ const PNG_SIGNATURE = Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a);
34
+
35
+ /** Frame a PNG chunk: 4-byte big-endian length, type+data, then the CRC-32 of type+data. */
36
+ function pngChunk(type: string, data: Uint8Array): Uint8Array {
37
+ const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
38
+ const out = Buffer.alloc(body.length + 8);
39
+ out.writeUInt32BE(data.length, 0);
40
+ body.copy(out, 4);
41
+ out.writeUInt32BE(Bun.hash.crc32(body) >>> 0, out.length - 4);
42
+ return out;
43
+ }
44
+
45
+ /**
46
+ * Encode raw 8-bit RGB pixels (`width * height * 3` bytes, row-major) as a PNG
47
+ * (color type 2, no interlacing). The IDAT payload is a real zlib stream from
48
+ * {@link zlib.deflateSync}, so the output is a fully valid PNG that every image
49
+ * protocol — including Sixel, which decodes the bytes natively — accepts.
50
+ */
51
+ export function encodeRgbPng(width: number, height: number, rgb: Uint8Array): Uint8Array {
52
+ const ihdr = Buffer.alloc(13);
53
+ ihdr.writeUInt32BE(width, 0);
54
+ ihdr.writeUInt32BE(height, 4);
55
+ ihdr[8] = 8; // bit depth
56
+ ihdr[9] = 2; // color type: truecolor RGB
57
+ // compression (0), filter (0), interlace (0) are already zeroed.
58
+
59
+ const stride = width * 3;
60
+ // Each scanline is prefixed with a filter-type byte (0 = None).
61
+ const raw = Buffer.alloc((stride + 1) * height);
62
+ for (let y = 0; y < height; y++) {
63
+ raw.set(rgb.subarray(y * stride, y * stride + stride), y * (stride + 1) + 1);
64
+ }
65
+ const idat = zlib.deflateSync(raw);
66
+
67
+ return Buffer.concat([
68
+ PNG_SIGNATURE,
69
+ pngChunk("IHDR", ihdr),
70
+ pngChunk("IDAT", idat),
71
+ pngChunk("IEND", new Uint8Array(0)),
72
+ ]);
73
+ }
74
+
75
+ export interface SampleImage {
76
+ base64: string;
77
+ mimeType: string;
78
+ dimensions: { widthPx: number; heightPx: number };
79
+ }
80
+
81
+ /** Build a deterministic RGB gradient PNG (red across, green down, constant blue). */
82
+ export function buildSampleImage(width = 192, height = 128): SampleImage {
83
+ const denomX = Math.max(1, width - 1);
84
+ const denomY = Math.max(1, height - 1);
85
+ const rgb = new Uint8Array(width * height * 3);
86
+ for (let y = 0; y < height; y++) {
87
+ for (let x = 0; x < width; x++) {
88
+ const i = (y * width + x) * 3;
89
+ rgb[i] = Math.round((x / denomX) * 255);
90
+ rgb[i + 1] = Math.round((y / denomY) * 255);
91
+ rgb[i + 2] = 128;
92
+ }
93
+ }
94
+ const png = encodeRgbPng(width, height, rgb);
95
+ return {
96
+ base64: Buffer.from(png).toString("base64"),
97
+ mimeType: "image/png",
98
+ dimensions: { widthPx: width, heightPx: height },
99
+ };
100
+ }
101
+
102
+ const LARGE_TEXT_SAMPLE = "Aa Bb 123";
103
+
104
+ /**
105
+ * OSC 66 text-sizing sample lines, one scaled span per requested scale. Each
106
+ * scaled row is followed by `scale - 1` blank rows that reserve the vertical
107
+ * cells its multi-cell glyphs occupy — mirroring the markdown H1 renderer so
108
+ * the next line does not paint over the bottom of the glyphs.
109
+ */
110
+ export function buildLargeTextLines(scales: readonly TextSizingScale[] = [2, 3]): string[] {
111
+ const lines: string[] = [];
112
+ for (const scale of scales) {
113
+ lines.push(` ${theme.fg("accent", encodeTextSized(`${LARGE_TEXT_SAMPLE} (${scale}x)`, { scale }))}`);
114
+ for (let reserved = 1; reserved < scale; reserved++) lines.push("");
115
+ }
116
+ return lines;
117
+ }
118
+
119
+ /** HSV (h in degrees, s/v in 0..1) to 8-bit RGB, for the truecolor demo bar. */
120
+ function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
121
+ const c = v * s;
122
+ const hp = (((h % 360) + 360) % 360) / 60;
123
+ const x = c * (1 - Math.abs((hp % 2) - 1));
124
+ let r = 0;
125
+ let g = 0;
126
+ let b = 0;
127
+ if (hp < 1) [r, g, b] = [c, x, 0];
128
+ else if (hp < 2) [r, g, b] = [x, c, 0];
129
+ else if (hp < 3) [r, g, b] = [0, c, x];
130
+ else if (hp < 4) [r, g, b] = [0, x, c];
131
+ else if (hp < 5) [r, g, b] = [x, 0, c];
132
+ else [r, g, b] = [c, 0, x];
133
+ const m = v - c;
134
+ const to8 = (n: number) => Math.round((n + m) * 255);
135
+ return [to8(r), to8(g), to8(b)];
136
+ }
137
+
138
+ /** A 24-bit-color hue sweep rendered as background-painted cells (one space each). */
139
+ function truecolorBar(cells: number): string {
140
+ let out = "";
141
+ for (let i = 0; i < cells; i++) {
142
+ const [r, g, b] = hsvToRgb((i / cells) * 360, 0.85, 1);
143
+ out += `\x1b[48;2;${r};${g};${b}m `;
144
+ }
145
+ return `${out}\x1b[0m`;
146
+ }
147
+
148
+ function notifyProtocolLabel(): string {
149
+ switch (TERMINAL.notifyProtocol) {
150
+ case NotifyProtocol.Osc99:
151
+ return "OSC 99 (kitty)";
152
+ case NotifyProtocol.Osc9:
153
+ return "OSC 9 (iTerm2/WezTerm)";
154
+ default:
155
+ return "BEL";
156
+ }
157
+ }
158
+
159
+ function imageProtocolLabel(): string {
160
+ switch (TERMINAL.imageProtocol) {
161
+ case ImageProtocol.Kitty:
162
+ return "Kitty graphics";
163
+ case ImageProtocol.Iterm2:
164
+ return "iTerm2 inline images";
165
+ case ImageProtocol.Sixel:
166
+ return "Sixel";
167
+ default:
168
+ return "none — text fallback";
169
+ }
170
+ }
171
+
172
+ /** Emits a fixed list of lines verbatim — no wrapping or padding. Used for OSC 66 spans. */
173
+ class RawLines implements Component {
174
+ #lines: readonly string[];
175
+ constructor(lines: readonly string[]) {
176
+ this.#lines = lines;
177
+ }
178
+ invalidate(): void {}
179
+ render(): string[] {
180
+ return [...this.#lines];
181
+ }
182
+ }
183
+
184
+ export interface ProtocolProbeOptions {
185
+ image: SampleImage;
186
+ imageBudget: ImageBudget;
187
+ /** Whether the desktop notification was suppressed (e.g. `PI_NOTIFICATIONS=off`). */
188
+ notificationSuppressed: boolean;
189
+ }
190
+
191
+ /**
192
+ * Self-contained panel that renders one sample of every special terminal
193
+ * protocol into the chat transcript.
194
+ */
195
+ export class ProtocolProbeComponent extends Container {
196
+ constructor(options: ProtocolProbeOptions) {
197
+ super();
198
+ const hyperlinksOn = TERMINAL.hyperlinks;
199
+ const sizingOn = TERMINAL.textSizing;
200
+ const yesNo = (on: boolean) => (on ? theme.fg("success", "supported") : theme.fg("muted", "unsupported"));
201
+
202
+ this.addChild(new DynamicBorder());
203
+ this.addChild(new Text(theme.bold(theme.fg("accent", "Terminal Protocol Test")), 1, 0));
204
+
205
+ // Styling: SGR attributes, themed foregrounds, and a truecolor sweep.
206
+ const styling = [
207
+ theme.fg("muted", "Styling (SGR)"),
208
+ ` ${theme.bold("bold")} ${theme.italic("italic")} ${theme.underline("underline")} ${theme.strikethrough("strike")} ${theme.inverse(" inverse ")} ${theme.fg("dim", "dim")}`,
209
+ ` ${theme.fg("accent", "accent")} ${theme.fg("success", "success")} ${theme.fg("warning", "warning")} ${theme.fg("error", "error")}`,
210
+ ` truecolor: ${truecolorBar(32)} (${theme.fg("muted", `24-bit ${TERMINAL.trueColor ? "on" : "off"}`)})`,
211
+ ].join("\n");
212
+ this.addChild(new Text(styling, 1, 0));
213
+ this.addChild(new Spacer(1));
214
+
215
+ // Hyperlinks: OSC 8. Renders as plain text where unsupported.
216
+ this.addChild(
217
+ new Text(
218
+ [
219
+ `${theme.fg("muted", "Hyperlinks (OSC 8)")} — ${yesNo(hyperlinksOn)}`,
220
+ ` \x1b]8;;https://github.com/can1357/oh-my-pi\x07oh-my-pi repo\x1b]8;;\x07`,
221
+ ].join("\n"),
222
+ 1,
223
+ 0,
224
+ ),
225
+ );
226
+ this.addChild(new Spacer(1));
227
+
228
+ // Text sizing: OSC 66.
229
+ this.addChild(new Text(`${theme.fg("muted", "Text sizing (OSC 66)")} — ${yesNo(sizingOn)}`, 1, 0));
230
+ if (sizingOn) {
231
+ this.addChild(new RawLines(buildLargeTextLines()));
232
+ } else {
233
+ this.addChild(
234
+ new Text(theme.fg("dim", " (enable via the tui.textSizing setting on a Kitty terminal)"), 1, 0),
235
+ );
236
+ }
237
+ this.addChild(new Spacer(1));
238
+
239
+ // Graphics: Kitty / iTerm2 / Sixel, with a text fallback baked into Image.
240
+ this.addChild(new Text(`${theme.fg("muted", "Graphics")} — ${theme.fg("dim", imageProtocolLabel())}`, 1, 0));
241
+ this.addChild(
242
+ new Image(
243
+ options.image.base64,
244
+ options.image.mimeType,
245
+ { fallbackColor: (text: string) => theme.fg("toolOutput", text) },
246
+ // Fixed modest caps (not the user's inline-image setting) keep the
247
+ // swatch a crisp, bounded preview rather than an upscaled wall.
248
+ { maxWidthCells: 20, maxHeightCells: 16, budget: options.imageBudget },
249
+ options.image.dimensions,
250
+ ),
251
+ );
252
+ this.addChild(new Spacer(1));
253
+
254
+ // Notifications: fired by the caller; this line reports the outcome.
255
+ const notifyStatus = options.notificationSuppressed
256
+ ? theme.fg("warning", "suppressed (PI_NOTIFICATIONS)")
257
+ : theme.fg("success", "sent — check your desktop / titlebar");
258
+ this.addChild(
259
+ new Text(
260
+ `${theme.fg("muted", "Notification")} (${theme.fg("dim", notifyProtocolLabel())}) — ${notifyStatus}`,
261
+ 1,
262
+ 0,
263
+ ),
264
+ );
265
+ this.addChild(new DynamicBorder());
266
+ }
267
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Terminal state collection for the debug menu.
3
+ *
4
+ * Surfaces the detected terminal, the established subprotocols the renderer
5
+ * negotiated (graphics, desktop notifications, hyperlinks, true color), the
6
+ * scrollback/erase strategy, and the live geometry — the details that decide
7
+ * which escape sequences the renderer emits.
8
+ */
9
+ import {
10
+ getCellDimensions,
11
+ ImageProtocol,
12
+ isOsc99Supported,
13
+ NotifyProtocol,
14
+ TERMINAL,
15
+ TERMINAL_ID,
16
+ } from "@oh-my-pi/pi-tui";
17
+
18
+ /** Live values the debug view reads off the running TUI, not the static capability table. */
19
+ export interface TerminalRuntimeState {
20
+ columns: number;
21
+ rows: number;
22
+ /** Whether DEC 2026 synchronized-output wrappers are currently emitted. */
23
+ synchronizedOutput: boolean;
24
+ }
25
+
26
+ export interface TerminalStateInfo {
27
+ detectedId: string;
28
+ columns: number;
29
+ rows: number;
30
+ cellWidthPx: number;
31
+ cellHeightPx: number;
32
+ trueColor: boolean;
33
+ imageProtocol: string;
34
+ notifyProtocol: string;
35
+ osc99Confirmed: boolean;
36
+ hyperlinks: boolean;
37
+ deccara: boolean;
38
+ screenToScrollback: boolean;
39
+ eagerEraseScrollbackRisk: boolean;
40
+ synchronizedOutput: boolean;
41
+ multiplexer: string | null;
42
+ env: { TERM?: string; TERM_PROGRAM?: string; TERM_PROGRAM_VERSION?: string; COLORTERM?: string };
43
+ }
44
+
45
+ const IMAGE_PROTOCOL_NAMES: Record<ImageProtocol, string> = {
46
+ [ImageProtocol.Kitty]: "Kitty graphics",
47
+ [ImageProtocol.Iterm2]: "iTerm2 inline images",
48
+ [ImageProtocol.Sixel]: "Sixel",
49
+ };
50
+
51
+ const NOTIFY_PROTOCOL_NAMES: Record<NotifyProtocol, string> = {
52
+ [NotifyProtocol.Bell]: "BEL (\\a)",
53
+ [NotifyProtocol.Osc99]: "OSC 99 (kitty desktop notifications)",
54
+ [NotifyProtocol.Osc9]: "OSC 9 (iTerm2/WezTerm)",
55
+ };
56
+
57
+ /** Identify the multiplexer wrapping the session, if any (mirrors the renderer's gate). */
58
+ function detectMultiplexer(env: NodeJS.ProcessEnv): string | null {
59
+ if (env.TMUX) return "tmux";
60
+ if (env.STY) return "screen";
61
+ if (env.ZELLIJ) return "zellij";
62
+ const term = env.TERM?.toLowerCase() ?? "";
63
+ if (term.startsWith("tmux")) return "tmux";
64
+ if (term.startsWith("screen")) return "screen";
65
+ return null;
66
+ }
67
+
68
+ /** Snapshot the active terminal capabilities and the live runtime geometry. */
69
+ export function collectTerminalState(runtime: TerminalRuntimeState): TerminalStateInfo {
70
+ const env = Bun.env;
71
+ const cell = getCellDimensions();
72
+ return {
73
+ detectedId: TERMINAL_ID,
74
+ columns: runtime.columns,
75
+ rows: runtime.rows,
76
+ cellWidthPx: cell.widthPx,
77
+ cellHeightPx: cell.heightPx,
78
+ trueColor: TERMINAL.trueColor,
79
+ imageProtocol: TERMINAL.imageProtocol === null ? "none" : IMAGE_PROTOCOL_NAMES[TERMINAL.imageProtocol],
80
+ notifyProtocol: NOTIFY_PROTOCOL_NAMES[TERMINAL.notifyProtocol],
81
+ osc99Confirmed: isOsc99Supported(),
82
+ hyperlinks: TERMINAL.hyperlinks,
83
+ deccara: TERMINAL.deccara,
84
+ screenToScrollback: TERMINAL.supportsScreenToScrollback,
85
+ eagerEraseScrollbackRisk: TERMINAL.eagerEraseScrollbackRisk,
86
+ synchronizedOutput: runtime.synchronizedOutput,
87
+ multiplexer: detectMultiplexer(env),
88
+ env: {
89
+ TERM: env.TERM,
90
+ TERM_PROGRAM: env.TERM_PROGRAM,
91
+ TERM_PROGRAM_VERSION: env.TERM_PROGRAM_VERSION,
92
+ COLORTERM: env.COLORTERM,
93
+ },
94
+ };
95
+ }
96
+
97
+ const yesNo = (value: boolean): string => (value ? "yes" : "no");
98
+
99
+ /** Format terminal state for display in the debug menu. */
100
+ export function formatTerminalState(info: TerminalStateInfo): string {
101
+ const lines = [
102
+ "Terminal State",
103
+ "━━━━━━━━━━━━━━",
104
+ `Detected: ${info.detectedId}`,
105
+ `Geometry: ${info.columns}x${info.rows} cells · cell ${info.cellWidthPx}x${info.cellHeightPx}px`,
106
+ info.multiplexer ? `Multiplexer: ${info.multiplexer}` : "Multiplexer: none",
107
+ "",
108
+ "Subprotocols",
109
+ ` Graphics: ${info.imageProtocol}`,
110
+ ` Notify: ${info.notifyProtocol}${info.osc99Confirmed ? " · confirmed via DA" : ""}`,
111
+ ` Hyperlinks: ${yesNo(info.hyperlinks)} (OSC 8)`,
112
+ ` True color: ${yesNo(info.trueColor)} (24-bit SGR)`,
113
+ ` DECCARA: ${yesNo(info.deccara)} (rectangular-SGR background fills)`,
114
+ ` Sync output: ${yesNo(info.synchronizedOutput)} (DEC 2026)`,
115
+ "",
116
+ "Scrollback",
117
+ ` Screen->history clear: ${info.screenToScrollback ? "CSI 22 J" : "CSI 2 J (redraw)"}`,
118
+ ` Eager-erase risk: ${yesNo(info.eagerEraseScrollbackRisk)} (ED3 may yank scrolled readers)`,
119
+ "",
120
+ "Detection signals",
121
+ ` TERM: ${info.env.TERM ?? "(unset)"}`,
122
+ ` TERM_PROGRAM: ${info.env.TERM_PROGRAM ?? "(unset)"}`,
123
+ ` TERM_PROGRAM_VERSION: ${info.env.TERM_PROGRAM_VERSION ?? "(unset)"}`,
124
+ ` COLORTERM: ${info.env.COLORTERM ?? "(unset)"}`,
125
+ ];
126
+ return lines.join("\n");
127
+ }
@@ -1,7 +1,7 @@
1
1
  import type { TSchema } from "@oh-my-pi/pi-ai";
2
2
  import { $env, logger } from "@oh-my-pi/pi-utils";
3
3
  import type { CustomTool, CustomToolResult } from "../extensibility/custom-tools/types";
4
- import { callMCP } from "../mcp/json-rpc";
4
+ import { type CallMcpOptions, callMCP } from "../mcp/json-rpc";
5
5
  import type {
6
6
  ExaRenderDetails,
7
7
  ExaSearchResponse,
@@ -105,15 +105,21 @@ export async function callExaTool(
105
105
  toolName: string,
106
106
  args: Record<string, unknown>,
107
107
  apiKey: string | null,
108
+ options?: CallMcpOptions,
108
109
  ): Promise<unknown> {
109
110
  const params = new URLSearchParams();
110
111
  if (apiKey) params.set("exaApiKey", apiKey);
111
112
  params.set("tools", toolName);
112
113
  const url = `https://mcp.exa.ai/mcp?${params.toString()}`;
113
- const response = (await callMCP(url, "tools/call", {
114
- name: toolName,
115
- arguments: args,
116
- })) as MCPCallResponse;
114
+ const response = (await callMCP(
115
+ url,
116
+ "tools/call",
117
+ {
118
+ name: toolName,
119
+ arguments: args,
120
+ },
121
+ options,
122
+ )) as MCPCallResponse;
117
123
 
118
124
  if (response.error) {
119
125
  logger.error("MCP tools/call error", { toolName, args, error: response.error });