@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.2

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 (196) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/cli.js +6325 -4420
  3. package/dist/types/cli/bench-cli.d.ts +1 -0
  4. package/dist/types/config/model-resolver.d.ts +6 -3
  5. package/dist/types/config/settings-schema.d.ts +62 -0
  6. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  7. package/dist/types/launch/broker-list-order.test.d.ts +1 -0
  8. package/dist/types/live/attestation.d.ts +2 -0
  9. package/dist/types/live/controller.d.ts +10 -2
  10. package/dist/types/live/protocol.d.ts +1 -1
  11. package/dist/types/live/transport.d.ts +6 -19
  12. package/dist/types/live/visualizer.d.ts +8 -11
  13. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  14. package/dist/types/modes/components/custom-message.d.ts +1 -1
  15. package/dist/types/modes/components/message-frame.d.ts +8 -4
  16. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  18. package/dist/types/modes/interactive-mode.d.ts +3 -6
  19. package/dist/types/modes/types.d.ts +9 -5
  20. package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
  21. package/dist/types/session/agent-session-types.d.ts +8 -1
  22. package/dist/types/session/agent-session.d.ts +20 -1
  23. package/dist/types/session/auth-storage.d.ts +1 -1
  24. package/dist/types/session/eval-runner.d.ts +2 -0
  25. package/dist/types/session/messages.d.ts +2 -0
  26. package/dist/types/session/session-tools.d.ts +15 -0
  27. package/dist/types/session/streaming-output.d.ts +8 -0
  28. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  29. package/dist/types/stt/index.d.ts +0 -2
  30. package/dist/types/stt/stt-controller.d.ts +7 -0
  31. package/dist/types/task/executor.d.ts +3 -1
  32. package/dist/types/task/structured-subagent.d.ts +3 -0
  33. package/dist/types/task/types.d.ts +11 -11
  34. package/dist/types/thinking.d.ts +13 -0
  35. package/dist/types/tiny/title-client.d.ts +10 -0
  36. package/dist/types/tools/builtin-names.d.ts +1 -1
  37. package/dist/types/tools/computer/protocol.d.ts +43 -0
  38. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  39. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  40. package/dist/types/tools/computer/worker.d.ts +15 -0
  41. package/dist/types/tools/computer-renderer.d.ts +22 -0
  42. package/dist/types/tools/computer.d.ts +71 -0
  43. package/dist/types/tools/context.d.ts +2 -0
  44. package/dist/types/tools/default-renderer.d.ts +21 -0
  45. package/dist/types/tools/eval-format/index.d.ts +7 -0
  46. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  47. package/dist/types/tools/eval-format/julia.d.ts +2 -0
  48. package/dist/types/tools/eval-format/python.d.ts +2 -0
  49. package/dist/types/tools/eval-format/ruby.d.ts +2 -0
  50. package/dist/types/tools/index.d.ts +2 -0
  51. package/dist/types/tools/resolve.d.ts +7 -0
  52. package/dist/types/tools/todo.d.ts +4 -1
  53. package/dist/types/tts/streaming-player.d.ts +10 -43
  54. package/dist/types/utils/tools-manager.d.ts +1 -2
  55. package/dist/types/web/search/providers/base.d.ts +16 -0
  56. package/dist/types/web/search/providers/brave.d.ts +2 -0
  57. package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
  58. package/dist/types/web/search/providers/gemini.d.ts +3 -0
  59. package/dist/types/web/search/providers/jina.d.ts +2 -0
  60. package/dist/types/web/search/providers/kagi.d.ts +2 -0
  61. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  62. package/dist/types/web/search/providers/parallel.d.ts +2 -0
  63. package/dist/types/web/search/providers/perplexity.d.ts +3 -0
  64. package/dist/types/web/search/providers/searxng.d.ts +10 -0
  65. package/dist/types/web/search/providers/tavily.d.ts +8 -0
  66. package/dist/types/web/search/query.d.ts +190 -0
  67. package/package.json +12 -12
  68. package/src/cli/args.ts +1 -0
  69. package/src/cli/bench-cli.ts +12 -1
  70. package/src/cli/setup-cli.ts +2 -14
  71. package/src/cli/web-search-cli.ts +7 -0
  72. package/src/cli.ts +8 -0
  73. package/src/config/model-registry.ts +6 -0
  74. package/src/config/model-resolver.ts +17 -6
  75. package/src/config/settings-schema.ts +66 -0
  76. package/src/debug/raw-sse-buffer.ts +157 -30
  77. package/src/eval/executor-base.ts +1 -0
  78. package/src/eval/js/executor.ts +2 -0
  79. package/src/exec/bash-executor.ts +1 -0
  80. package/src/export/share.ts +4 -3
  81. package/src/extensibility/extensions/wrapper.ts +68 -12
  82. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  83. package/src/launch/broker-list-order.test.ts +89 -0
  84. package/src/launch/broker.ts +49 -8
  85. package/src/live/attestation.ts +91 -0
  86. package/src/live/controller.ts +76 -23
  87. package/src/live/protocol.test.ts +3 -3
  88. package/src/live/protocol.ts +1 -1
  89. package/src/live/transport.ts +72 -140
  90. package/src/live/visualizer.ts +114 -134
  91. package/src/modes/components/assistant-message.ts +7 -2
  92. package/src/modes/components/custom-message.ts +4 -1
  93. package/src/modes/components/message-frame.ts +14 -8
  94. package/src/modes/components/session-account-selector.ts +62 -0
  95. package/src/modes/components/tool-execution.ts +17 -110
  96. package/src/modes/controllers/input-controller.ts +55 -47
  97. package/src/modes/controllers/live-command-controller.ts +82 -5
  98. package/src/modes/controllers/selector-controller.ts +62 -0
  99. package/src/modes/interactive-mode.ts +79 -5
  100. package/src/modes/types.ts +10 -4
  101. package/src/modes/utils/ui-helpers.ts +7 -8
  102. package/src/prompts/system/computer-safety.md +14 -0
  103. package/src/prompts/system/resolve-device-reminder.md +1 -1
  104. package/src/prompts/system/workflow-notice.md +1 -1
  105. package/src/prompts/tools/ast-edit.md +1 -0
  106. package/src/prompts/tools/bash.md +1 -0
  107. package/src/prompts/tools/computer.md +26 -0
  108. package/src/prompts/tools/eval.md +2 -2
  109. package/src/prompts/tools/task.md +5 -2
  110. package/src/prompts/tools/web-search.md +2 -0
  111. package/src/sdk.ts +13 -4
  112. package/src/session/agent-session-types.ts +9 -0
  113. package/src/session/agent-session.ts +70 -0
  114. package/src/session/auth-storage.ts +1 -0
  115. package/src/session/eval-runner.ts +5 -0
  116. package/src/session/messages.ts +3 -0
  117. package/src/session/queued-messages.ts +7 -1
  118. package/src/session/session-tools.ts +37 -0
  119. package/src/session/streaming-output.ts +52 -5
  120. package/src/slash-commands/builtin-registry.ts +165 -9
  121. package/src/slash-commands/helpers/session-pin.ts +44 -0
  122. package/src/stt/downloader.ts +0 -2
  123. package/src/stt/index.ts +0 -2
  124. package/src/stt/stt-controller.ts +57 -146
  125. package/src/system-prompt.ts +4 -0
  126. package/src/task/executor.ts +17 -9
  127. package/src/task/index.ts +14 -34
  128. package/src/task/structured-subagent.ts +4 -0
  129. package/src/task/types.ts +15 -13
  130. package/src/thinking.ts +27 -0
  131. package/src/tiny/title-client.ts +22 -0
  132. package/src/tools/ast-edit.ts +4 -1
  133. package/src/tools/bash-interactive.ts +90 -86
  134. package/src/tools/bash.ts +13 -0
  135. package/src/tools/browser/render.ts +2 -1
  136. package/src/tools/builtin-names.ts +1 -0
  137. package/src/tools/computer/protocol.ts +28 -0
  138. package/src/tools/computer/supervisor.ts +258 -0
  139. package/src/tools/computer/worker-entry.ts +25 -0
  140. package/src/tools/computer/worker.ts +135 -0
  141. package/src/tools/computer-renderer.ts +108 -0
  142. package/src/tools/computer.ts +433 -0
  143. package/src/tools/context.ts +2 -0
  144. package/src/tools/default-renderer.ts +139 -0
  145. package/src/tools/essential-tools.ts +1 -0
  146. package/src/tools/eval-format/index.ts +24 -0
  147. package/src/tools/eval-format/javascript.ts +952 -0
  148. package/src/tools/eval-format/julia.ts +446 -0
  149. package/src/tools/eval-format/python.ts +544 -0
  150. package/src/tools/eval-format/ruby.ts +380 -0
  151. package/src/tools/eval-render.ts +12 -6
  152. package/src/tools/index.ts +5 -0
  153. package/src/tools/renderers.ts +2 -0
  154. package/src/tools/resolve.ts +10 -1
  155. package/src/tools/todo.ts +58 -6
  156. package/src/tools/xdev.ts +54 -26
  157. package/src/tts/streaming-player.ts +81 -340
  158. package/src/utils/clipboard.ts +1 -30
  159. package/src/utils/mac-file-urls.applescript +37 -0
  160. package/src/utils/tool-choice.ts +14 -0
  161. package/src/utils/tools-manager.ts +1 -19
  162. package/src/web/search/index.ts +28 -5
  163. package/src/web/search/providers/anthropic.ts +62 -4
  164. package/src/web/search/providers/base.ts +16 -0
  165. package/src/web/search/providers/brave.ts +30 -3
  166. package/src/web/search/providers/codex.ts +14 -1
  167. package/src/web/search/providers/duckduckgo.ts +23 -1
  168. package/src/web/search/providers/ecosia.ts +5 -1
  169. package/src/web/search/providers/exa.ts +28 -1
  170. package/src/web/search/providers/firecrawl.ts +37 -3
  171. package/src/web/search/providers/gemini.ts +12 -2
  172. package/src/web/search/providers/google.ts +2 -1
  173. package/src/web/search/providers/jina.ts +31 -8
  174. package/src/web/search/providers/kagi.ts +10 -1
  175. package/src/web/search/providers/kimi.ts +16 -1
  176. package/src/web/search/providers/mojeek.ts +14 -1
  177. package/src/web/search/providers/parallel.ts +48 -2
  178. package/src/web/search/providers/perplexity.ts +94 -6
  179. package/src/web/search/providers/searxng.ts +145 -9
  180. package/src/web/search/providers/startpage.ts +8 -2
  181. package/src/web/search/providers/synthetic.ts +7 -1
  182. package/src/web/search/providers/tavily.ts +52 -3
  183. package/src/web/search/providers/tinyfish.ts +6 -1
  184. package/src/web/search/providers/xai.ts +49 -2
  185. package/src/web/search/providers/zai.ts +19 -1
  186. package/src/web/search/query.ts +850 -0
  187. package/dist/types/stt/recorder.d.ts +0 -30
  188. package/dist/types/stt/transcriber.d.ts +0 -14
  189. package/dist/types/stt/wav.d.ts +0 -29
  190. package/dist/types/tts/player.d.ts +0 -32
  191. package/src/live/audio-worklet.txt +0 -59
  192. package/src/live/browser-runtime.txt +0 -221
  193. package/src/stt/recorder.ts +0 -551
  194. package/src/stt/transcriber.ts +0 -60
  195. package/src/stt/wav.ts +0 -173
  196. package/src/tts/player.ts +0 -137
@@ -0,0 +1,380 @@
1
+ const INDENT = " ";
2
+
3
+ const OPENING_KEYWORDS: Record<string, true> = {
4
+ begin: true,
5
+ case: true,
6
+ class: true,
7
+ def: true,
8
+ for: true,
9
+ if: true,
10
+ module: true,
11
+ unless: true,
12
+ until: true,
13
+ while: true,
14
+ };
15
+
16
+ const BRANCH_KEYWORDS: Record<string, true> = {
17
+ else: true,
18
+ elsif: true,
19
+ ensure: true,
20
+ rescue: true,
21
+ when: true,
22
+ };
23
+
24
+ const PAIRED_DELIMITERS: Record<string, string> = {
25
+ "(": ")",
26
+ "[": "]",
27
+ "{": "}",
28
+ "<": ">",
29
+ };
30
+
31
+ type Quote = "'" | '"' | "`";
32
+
33
+ interface QuotedContext {
34
+ kind: "quoted";
35
+ quote: Quote;
36
+ interpolated: boolean;
37
+ escaped: boolean;
38
+ }
39
+
40
+ interface PercentContext {
41
+ kind: "percent";
42
+ open: string;
43
+ close: string;
44
+ depth: number;
45
+ interpolated: boolean;
46
+ escaped: boolean;
47
+ }
48
+
49
+ interface InterpolationContext {
50
+ kind: "interpolation";
51
+ braceDepth: number;
52
+ }
53
+
54
+ interface CommentContext {
55
+ kind: "comment";
56
+ }
57
+
58
+ type LexicalContext = QuotedContext | PercentContext | InterpolationContext | CommentContext;
59
+
60
+ interface PercentLiteralStart {
61
+ end: number;
62
+ open: string;
63
+ close: string;
64
+ interpolated: boolean;
65
+ }
66
+
67
+ function isHorizontalWhitespace(character: string): boolean {
68
+ return character === " " || character === "\t" || character === "\f" || character === "\v";
69
+ }
70
+
71
+ function isIdentifierStart(character: string | undefined): boolean {
72
+ if (character === undefined) return false;
73
+ const code = character.charCodeAt(0);
74
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || character === "_";
75
+ }
76
+
77
+ function isIdentifierPart(character: string | undefined): boolean {
78
+ if (character === undefined) return false;
79
+ const code = character.charCodeAt(0);
80
+ return isIdentifierStart(character) || (code >= 48 && code <= 57);
81
+ }
82
+
83
+ function findPercentLiteral(source: string, start: number): PercentLiteralStart | undefined {
84
+ let delimiterIndex = start + 1;
85
+ let type = "";
86
+ const candidateType = source[delimiterIndex];
87
+ if (candidateType !== undefined && "qQwWiIxrs".includes(candidateType)) {
88
+ type = candidateType;
89
+ delimiterIndex++;
90
+ }
91
+
92
+ const open = source[delimiterIndex];
93
+ if (open === undefined || isIdentifierPart(open) || /\s/.test(open)) return undefined;
94
+ return {
95
+ end: delimiterIndex + 1,
96
+ open,
97
+ close: PAIRED_DELIMITERS[open] ?? open,
98
+ interpolated: type === "" || type === "Q" || type === "W" || type === "I" || type === "x" || type === "r",
99
+ };
100
+ }
101
+
102
+ function formatRubyPrefix(source: string): string {
103
+ const output: string[] = [];
104
+ const contexts: LexicalContext[] = [];
105
+ const delimiters: string[] = [];
106
+ let lineParts: string[] = [];
107
+ let lineHasVisibleText = false;
108
+ let preserveLeadingWhitespace = false;
109
+ let pendingSemicolonBreak = false;
110
+ let blockDepth = 0;
111
+ let hasSignificantToken = false;
112
+ let leadingWord: string | null = null;
113
+ let previousToken = "";
114
+ let hasDo = false;
115
+ let endCount = 0;
116
+ let endlessDefinition = false;
117
+
118
+ const append = (text: string): void => {
119
+ lineParts.push(text);
120
+ if (lineHasVisibleText) return;
121
+ for (let index = 0; index < text.length; index++) {
122
+ if (!isHorizontalWhitespace(text[index])) {
123
+ lineHasVisibleText = true;
124
+ return;
125
+ }
126
+ }
127
+ };
128
+
129
+ const observeOtherToken = (value: string): void => {
130
+ if (!hasSignificantToken) {
131
+ hasSignificantToken = true;
132
+ leadingWord = null;
133
+ }
134
+ previousToken = value;
135
+ };
136
+
137
+ const resetLine = (): void => {
138
+ lineParts = [];
139
+ lineHasVisibleText = false;
140
+ preserveLeadingWhitespace = contexts.length > 0 || delimiters.length > 0;
141
+ hasSignificantToken = false;
142
+ leadingWord = null;
143
+ previousToken = "";
144
+ hasDo = false;
145
+ endCount = 0;
146
+ endlessDefinition = false;
147
+ };
148
+
149
+ const flushLine = (ending: string): void => {
150
+ const raw = lineParts.join("");
151
+ const leadingEnd = leadingWord === "end";
152
+ const branch = leadingWord !== null && BRANCH_KEYWORDS[leadingWord] === true;
153
+ const opens =
154
+ !endlessDefinition &&
155
+ ((leadingWord !== null && OPENING_KEYWORDS[leadingWord] === true) || (!leadingEnd && !branch && hasDo));
156
+ const indentation = leadingEnd || branch ? Math.max(0, blockDepth - 1) : blockDepth;
157
+ blockDepth = Math.max(0, blockDepth + (opens ? 1 : 0) - endCount);
158
+
159
+ if (preserveLeadingWhitespace) {
160
+ output.push(raw, ending);
161
+ return;
162
+ }
163
+
164
+ let contentStart = 0;
165
+ while (contentStart < raw.length && isHorizontalWhitespace(raw[contentStart])) contentStart++;
166
+ const content = raw.slice(contentStart);
167
+ output.push(content.length === 0 ? "" : INDENT.repeat(indentation) + content, ending);
168
+ };
169
+
170
+ for (let index = 0; index < source.length; ) {
171
+ const character = source[index];
172
+ if (character === "\n" || character === "\r") {
173
+ const ending = character === "\r" && source[index + 1] === "\n" ? "\r\n" : character;
174
+ const top = contexts[contexts.length - 1];
175
+ if (top?.kind === "comment") {
176
+ contexts.pop();
177
+ } else if (top?.kind === "quoted" || top?.kind === "percent") {
178
+ top.escaped = false;
179
+ }
180
+
181
+ if (pendingSemicolonBreak && !lineHasVisibleText) {
182
+ pendingSemicolonBreak = false;
183
+ resetLine();
184
+ } else {
185
+ flushLine(ending);
186
+ pendingSemicolonBreak = false;
187
+ resetLine();
188
+ }
189
+ index += ending.length;
190
+ continue;
191
+ }
192
+
193
+ const top = contexts[contexts.length - 1];
194
+ if (top?.kind === "comment") {
195
+ append(character);
196
+ index++;
197
+ continue;
198
+ }
199
+
200
+ if (top?.kind === "quoted") {
201
+ append(character);
202
+ if (top.escaped) {
203
+ top.escaped = false;
204
+ index++;
205
+ continue;
206
+ }
207
+ if (character === "\\") {
208
+ top.escaped = true;
209
+ index++;
210
+ continue;
211
+ }
212
+ if (top.interpolated && character === "#" && source[index + 1] === "{") {
213
+ append("{");
214
+ contexts.push({ kind: "interpolation", braceDepth: 1 });
215
+ index += 2;
216
+ continue;
217
+ }
218
+ if (character === top.quote) contexts.pop();
219
+ index++;
220
+ continue;
221
+ }
222
+
223
+ if (top?.kind === "percent") {
224
+ append(character);
225
+ if (top.escaped) {
226
+ top.escaped = false;
227
+ index++;
228
+ continue;
229
+ }
230
+ if (character === "\\") {
231
+ top.escaped = true;
232
+ index++;
233
+ continue;
234
+ }
235
+ if (top.interpolated && character === "#" && source[index + 1] === "{") {
236
+ append("{");
237
+ contexts.push({ kind: "interpolation", braceDepth: 1 });
238
+ index += 2;
239
+ continue;
240
+ }
241
+ if (top.open !== top.close && character === top.open) {
242
+ top.depth++;
243
+ } else if (character === top.close) {
244
+ top.depth--;
245
+ if (top.depth === 0) contexts.pop();
246
+ }
247
+ index++;
248
+ continue;
249
+ }
250
+
251
+ const interpolation = top?.kind === "interpolation" ? top : undefined;
252
+ const inRootCode = interpolation === undefined;
253
+ if (interpolation !== undefined && character === "}") {
254
+ append(character);
255
+ interpolation.braceDepth--;
256
+ if (interpolation.braceDepth === 0) contexts.pop();
257
+ index++;
258
+ continue;
259
+ }
260
+
261
+ if (character === "#") {
262
+ append(character);
263
+ contexts.push({ kind: "comment" });
264
+ index++;
265
+ continue;
266
+ }
267
+
268
+ if (character === "'" || character === '"' || character === "`") {
269
+ if (inRootCode && delimiters.length === 0) observeOtherToken("literal");
270
+ append(character);
271
+ contexts.push({ kind: "quoted", quote: character, interpolated: character !== "'", escaped: false });
272
+ index++;
273
+ continue;
274
+ }
275
+
276
+ if (character === "%") {
277
+ const literal = findPercentLiteral(source, index);
278
+ if (literal !== undefined) {
279
+ if (inRootCode && delimiters.length === 0) observeOtherToken("literal");
280
+ append(source.slice(index, literal.end));
281
+ contexts.push({
282
+ kind: "percent",
283
+ open: literal.open,
284
+ close: literal.close,
285
+ depth: 1,
286
+ interpolated: literal.interpolated,
287
+ escaped: false,
288
+ });
289
+ index = literal.end;
290
+ continue;
291
+ }
292
+ }
293
+
294
+ if (isIdentifierStart(character)) {
295
+ let end = index + 1;
296
+ while (isIdentifierPart(source[end])) end++;
297
+ if (source[end] === "?" || source[end] === "!") end++;
298
+ const word = source.slice(index, end);
299
+ append(word);
300
+
301
+ if (inRootCode && delimiters.length === 0) {
302
+ const blockedByPrefix =
303
+ previousToken === ":" || previousToken === "." || previousToken === "@" || previousToken === "$";
304
+ const label = source[end] === ":" && source[end + 1] !== ":";
305
+ const eligible = !blockedByPrefix && !label;
306
+ if (!hasSignificantToken) {
307
+ hasSignificantToken = true;
308
+ leadingWord = eligible ? word : null;
309
+ }
310
+ if (eligible && word === "do") hasDo = true;
311
+ if (eligible && word === "end") endCount++;
312
+ previousToken = "word";
313
+ }
314
+ index = end;
315
+ continue;
316
+ }
317
+
318
+ if (interpolation !== undefined && character === "{") {
319
+ append(character);
320
+ interpolation.braceDepth++;
321
+ index++;
322
+ continue;
323
+ }
324
+
325
+ if (inRootCode && (character === "(" || character === "[" || character === "{")) {
326
+ if (delimiters.length === 0) observeOtherToken(character);
327
+ append(character);
328
+ delimiters.push(character);
329
+ index++;
330
+ continue;
331
+ }
332
+
333
+ if (inRootCode && (character === ")" || character === "]" || character === "}")) {
334
+ append(character);
335
+ const open = delimiters[delimiters.length - 1];
336
+ if (open !== undefined && PAIRED_DELIMITERS[open] === character) delimiters.pop();
337
+ if (delimiters.length === 0) observeOtherToken(character);
338
+ index++;
339
+ continue;
340
+ }
341
+
342
+ if (character === ";") {
343
+ append(character);
344
+ if (inRootCode && delimiters.length === 0) {
345
+ observeOtherToken(character);
346
+ flushLine("\n");
347
+ pendingSemicolonBreak = true;
348
+ resetLine();
349
+ }
350
+ index++;
351
+ continue;
352
+ }
353
+
354
+ append(character);
355
+ if (inRootCode && delimiters.length === 0 && !isHorizontalWhitespace(character)) {
356
+ if (
357
+ character === "=" &&
358
+ leadingWord === "def" &&
359
+ /\s/.test(source[index - 1] ?? "") &&
360
+ source[index + 1] !== "="
361
+ ) {
362
+ endlessDefinition = true;
363
+ }
364
+ observeOtherToken(character);
365
+ }
366
+ index++;
367
+ }
368
+
369
+ if (lineParts.length > 0 && !(pendingSemicolonBreak && !lineHasVisibleText)) flushLine("");
370
+ return output.join("");
371
+ }
372
+
373
+ /** Formats an arbitrary Ruby source prefix for display without requiring valid syntax. */
374
+ export function formatRubyForDisplay(source: string): string {
375
+ try {
376
+ return formatRubyPrefix(source);
377
+ } catch {
378
+ return source;
379
+ }
380
+ }
@@ -19,6 +19,7 @@ import { formatContextUsage } from "../modes/components/status-line/context-thre
19
19
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
20
20
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
21
21
  import { markFramedBlockComponent, outputBlockContentWidth, renderCodeCell } from "../tui";
22
+ import { formatEvalCodeForDisplay } from "./eval-format";
22
23
  import {
23
24
  JSON_TREE_MAX_DEPTH_COLLAPSED,
24
25
  JSON_TREE_MAX_DEPTH_EXPANDED,
@@ -89,10 +90,11 @@ function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
89
90
  const out: EvalRenderCell[] = [];
90
91
  for (const cell of raw) {
91
92
  if (!cell || typeof cell !== "object") continue;
93
+ const language = normalizeRenderLanguage(typeof cell.language === "string" ? cell.language : undefined);
92
94
  const code = typeof cell.code === "string" ? cell.code : "";
93
95
  out.push({
94
- language: normalizeRenderLanguage(typeof cell.language === "string" ? cell.language : undefined),
95
- code,
96
+ language,
97
+ code: formatEvalCodeForDisplay(code, language),
96
98
  title: typeof cell.title === "string" ? cell.title : undefined,
97
99
  });
98
100
  }
@@ -587,6 +589,10 @@ export const evalToolRenderer = {
587
589
 
588
590
  const cellResults = details?.cells;
589
591
  if (cellResults && cellResults.length > 0) {
592
+ const displayCells = cellResults.map(cell => {
593
+ const language = cell.language ?? details?.language ?? "python";
594
+ return { cell, code: formatEvalCodeForDisplay(cell.code, language), language };
595
+ });
590
596
  let cached: { key: string; width: number; result: string[] } | undefined;
591
597
 
592
598
  return markFramedBlockComponent({
@@ -602,8 +608,8 @@ export const evalToolRenderer = {
602
608
  }
603
609
 
604
610
  const lines: string[] = [];
605
- for (let i = 0; i < cellResults.length; i++) {
606
- const cell = cellResults[i];
611
+ for (let i = 0; i < displayCells.length; i++) {
612
+ const { cell, code, language } = displayCells[i];
607
613
  const allEvents = cell.statusEvents ?? [];
608
614
  const agentEvents = allEvents.filter(e => e.op === "agent");
609
615
  const otherEvents = agentEvents.length > 0 ? allEvents.filter(e => e.op !== "agent") : allEvents;
@@ -623,8 +629,8 @@ export const evalToolRenderer = {
623
629
  }
624
630
  const cellLines = renderCodeCell(
625
631
  {
626
- code: cell.code,
627
- language: languageForHighlighter(cell.language ?? details?.language),
632
+ code,
633
+ language: languageForHighlighter(language),
628
634
  showLanguage: true,
629
635
  index: i,
630
636
  total: cellResults.length,
@@ -41,6 +41,7 @@ import { BashTool } from "./bash";
41
41
  import { BrowserTool } from "./browser";
42
42
  import { type BuiltinToolName, type HiddenToolName, normalizeToolNames } from "./builtin-names";
43
43
  import { type CheckpointState, CheckpointTool, type CompletedRewindState, RewindTool } from "./checkpoint";
44
+ import { ComputerTool } from "./computer";
44
45
  import { DebugTool } from "./debug";
45
46
  import { EvalTool } from "./eval";
46
47
  import { resolveEvalBackends } from "./eval-backends";
@@ -75,6 +76,8 @@ export * from "./ast-grep";
75
76
  export * from "./bash";
76
77
  export * from "./browser";
77
78
  export * from "./checkpoint";
79
+ export * from "./computer";
80
+ export * from "./computer/supervisor";
78
81
  export * from "./debug";
79
82
  export * from "./essential-tools";
80
83
  export * from "./eval";
@@ -399,6 +402,7 @@ export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
399
402
  lsp: LspTool.createIf,
400
403
  inspect_image: s => new InspectImageTool(s),
401
404
  browser: s => new BrowserTool(s),
405
+ computer: s => new ComputerTool(s),
402
406
  checkpoint: CheckpointTool.createIf,
403
407
  rewind: RewindTool.createIf,
404
408
  task: s => TaskTool.create(s),
@@ -555,6 +559,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
555
559
  if (name === "web_search") return session.settings.get("web_search.enabled");
556
560
  if (name === "ask") return session.settings.get("ask.enabled");
557
561
  if (name === "browser") return session.settings.get("browser.enabled");
562
+ if (name === "computer") return session.settings.get("computer.enabled");
558
563
  if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
559
564
  if (name === "hub") {
560
565
  return (
@@ -16,6 +16,7 @@ import { astEditToolRenderer } from "./ast-edit";
16
16
  import { astGrepToolRenderer } from "./ast-grep";
17
17
  import { bashToolRenderer } from "./bash";
18
18
  import { browserToolRenderer } from "./browser/render";
19
+ import { computerToolRenderer } from "./computer-renderer";
19
20
  import { debugToolRenderer } from "./debug";
20
21
  import { evalToolRenderer } from "./eval-render";
21
22
  import { githubToolRenderer } from "./gh-renderer";
@@ -82,6 +83,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
82
83
  ast_edit: astEditToolRenderer as ToolRenderer,
83
84
  bash: bashToolRenderer as ToolRenderer,
84
85
  browser: browserToolRenderer as ToolRenderer,
86
+ computer: computerToolRenderer as ToolRenderer,
85
87
  debug: debugToolRenderer as ToolRenderer,
86
88
  eval: evalToolRenderer as ToolRenderer,
87
89
  edit: editToolRenderer as ToolRenderer,
@@ -17,6 +17,7 @@
17
17
  import type { AgentToolResult, CustomMessage } from "@oh-my-pi/pi-agent-core";
18
18
  import type { Component } from "@oh-my-pi/pi-tui";
19
19
  import { Text } from "@oh-my-pi/pi-tui";
20
+ import { prompt } from "@oh-my-pi/pi-utils";
20
21
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
21
22
  import { parseXdUrl, XD_URL_PREFIX } from "../internal-urls/xd-protocol";
22
23
  import type { Theme } from "../modes/theme/theme";
@@ -36,6 +37,14 @@ export const RESOLVE_DEVICE_PATH = `${XD_URL_PREFIX}${RESOLVE_DEVICE_NAME}`;
36
37
  export const REJECT_DEVICE_PATH = `${XD_URL_PREFIX}${REJECT_DEVICE_NAME}`;
37
38
  export const PROPOSE_DEVICE_PATH = `${XD_URL_PREFIX}${PROPOSE_DEVICE_NAME}`;
38
39
 
40
+ /**
41
+ * Model-visible banner prepended to a staged preview's tool result text. The
42
+ * TUI badge (`⟨proposed⟩`) never reaches the model, and preview diffs are
43
+ * byte-identical to applied-edit output — without this line the model reads
44
+ * the result as an already-applied change.
45
+ */
46
+ export const PREVIEW_PENDING_NOTICE = `Staged as a proposal — files NOT modified yet. To apply: write a one-sentence reason to ${RESOLVE_DEVICE_PATH}. To discard: write to ${REJECT_DEVICE_PATH}.`;
47
+
39
48
  export type ResolutionDeviceName = typeof RESOLVE_DEVICE_NAME | typeof REJECT_DEVICE_NAME | typeof PROPOSE_DEVICE_NAME;
40
49
 
41
50
  /** Whether an xd:// device name is one of the plain-text resolution devices. */
@@ -204,7 +213,7 @@ export function buildResolveReminderMessage(sourceToolName: string): CustomMessa
204
213
  return {
205
214
  role: "custom",
206
215
  customType: "resolve-reminder",
207
- content: resolveReminderPrompt.trim(),
216
+ content: prompt.render(resolveReminderPrompt, { toolName: sourceToolName }).trim(),
208
217
  display: false,
209
218
  details: { toolName: sourceToolName },
210
219
  attribution: "agent",
package/src/tools/todo.ts CHANGED
@@ -524,7 +524,46 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
524
524
  }
525
525
  }
526
526
 
527
- function applyParams(phases: TodoPhase[], params: TodoParams): { phases: TodoPhase[]; errors: string[] } {
527
+ /**
528
+ * Infer a missing `op` from the raw argument shape. Only unambiguous shapes
529
+ * are inferred:
530
+ * - `list` → `init` (list is init-only)
531
+ * - `items` + `phase` → `append` (lazily creates the phase, so the result
532
+ * matches a single-phase init when nothing exists yet)
533
+ * - bare `items` with no existing todos → `init` (nothing to overwrite)
534
+ * Targeting args alone (`task`/`phase`) map to several ops and stay an error.
535
+ */
536
+ function inferTodoOp(args: Record<string, unknown>, hasExistingPhases: boolean): TodoOperation | undefined {
537
+ if (Array.isArray(args.list) && args.list.length > 0) return "init";
538
+ if (Array.isArray(args.items) && args.items.length > 0) {
539
+ if (typeof args.phase === "string" && args.phase) return "append";
540
+ if (!hasExistingPhases) return "init";
541
+ }
542
+ return undefined;
543
+ }
544
+
545
+ /**
546
+ * Validate execute-time arguments, repairing an omitted `op`. The tool sets
547
+ * `lenientArgValidation`, so the agent loop hands `execute()` the raw
548
+ * arguments when schema validation fails; the only failure repaired here is
549
+ * a missing `op` alongside an unambiguous payload (models routinely send
550
+ * `{list:[...]}` with no op). Anything else returns the schema error text
551
+ * for a normal model retry.
552
+ */
553
+ function resolveTodoParams(raw: unknown, hasExistingPhases: boolean): TodoOpEntryValue | string {
554
+ const direct = todoSchema(raw);
555
+ if (!(direct instanceof type.errors)) return direct;
556
+ if (isRecord(raw) && raw.op === undefined) {
557
+ const inferred = inferTodoOp(raw, hasExistingPhases);
558
+ if (inferred) {
559
+ const repaired = todoSchema({ ...raw, op: inferred });
560
+ if (!(repaired instanceof type.errors)) return repaired;
561
+ }
562
+ }
563
+ return `Invalid todo arguments: ${direct.summary}`;
564
+ }
565
+
566
+ function applyParams(phases: TodoPhase[], params: TodoOpEntryValue): { phases: TodoPhase[]; errors: string[] } {
528
567
  const errors: string[] = [];
529
568
  const next = applyEntry(phases, params, errors);
530
569
  normalizeInProgressTask(next);
@@ -534,7 +573,7 @@ function applyParams(phases: TodoPhase[], params: TodoParams): { phases: TodoPha
534
573
  /** Apply an array of `todo`-style ops to existing phases. Used by /todo slash command. */
535
574
  export function applyOpsToPhases(
536
575
  currentPhases: TodoPhase[],
537
- ops: TodoParams[],
576
+ ops: TodoOpEntryValue[],
538
577
  ): { phases: TodoPhase[]; errors: string[] } {
539
578
  const errors: string[] = [];
540
579
  let next = clonePhases(currentPhases);
@@ -731,6 +770,9 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
731
770
  readonly parameters = todoSchema;
732
771
  readonly concurrency = "exclusive";
733
772
  readonly strict = true;
773
+ // Raw args reach execute() on schema failure; resolveTodoParams re-validates
774
+ // and repairs the one recoverable shape (missing `op`, unambiguous payload).
775
+ readonly lenientArgValidation = true;
734
776
 
735
777
  readonly examples: readonly ToolExample<typeof todoSchema.infer>[] = [
736
778
  {
@@ -789,11 +831,22 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
789
831
  _context?: AgentToolContext,
790
832
  ): Promise<AgentToolResult<TodoToolDetails>> {
791
833
  const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
834
+ const storage = this.session.getSessionFile() ? "session" : "memory";
835
+ const resolved = resolveTodoParams(params, previousPhases.length > 0);
836
+ if (typeof resolved === "string") {
837
+ return {
838
+ content: [{ type: "text", text: resolved }],
839
+ details: { phases: previousPhases, storage },
840
+ isError: true,
841
+ };
842
+ }
843
+ const entry = resolved;
844
+ const op = entry.op;
792
845
  // Pure-view calls are reads: no normalization, no state write.
793
- const readOnly = params.op === "view";
846
+ const readOnly = op === "view";
794
847
  const { phases: updated, errors } = readOnly
795
848
  ? { phases: previousPhases, errors: [] as string[] }
796
- : applyParams(clonePhases(previousPhases), params);
849
+ : applyParams(clonePhases(previousPhases), entry);
797
850
  // A batch with any error is discarded wholesale: persisting a
798
851
  // half-applied batch makes the natural retry hit "already exists" for
799
852
  // the ops that did land. State and rendered summary stay at previous.
@@ -801,8 +854,7 @@ export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
801
854
  const effective = failed ? previousPhases : updated;
802
855
  const completedTasks = readOnly || failed ? [] : getCompletionTransitions(previousPhases, updated);
803
856
  if (!readOnly && !failed) this.session.setTodoPhases?.(updated);
804
- const storage = this.session.getSessionFile() ? "session" : "memory";
805
- const details: TodoToolDetails = { op: params.op, phases: effective, storage };
857
+ const details: TodoToolDetails = { op, phases: effective, storage };
806
858
  if (completedTasks.length > 0) details.completedTasks = completedTasks;
807
859
 
808
860
  return {