@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.3

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 (163) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3855 -3798
  3. package/dist/types/cli/update-cli.d.ts +1 -0
  4. package/dist/types/cli/update-cli.test.d.ts +1 -0
  5. package/dist/types/commands/update.d.ts +5 -0
  6. package/dist/types/config/config-file.d.ts +1 -1
  7. package/dist/types/config/settings-schema.d.ts +20 -0
  8. package/dist/types/dap/client.d.ts +9 -1
  9. package/dist/types/discovery/helpers.d.ts +2 -0
  10. package/dist/types/edit/file-snapshot-store.d.ts +9 -1
  11. package/dist/types/edit/hashline/execute.d.ts +1 -1
  12. package/dist/types/edit/hashline/params.d.ts +3 -6
  13. package/dist/types/edit/renderer.d.ts +1 -0
  14. package/dist/types/extensibility/plugins/parser.d.ts +2 -1
  15. package/dist/types/hindsight/client.d.ts +15 -11
  16. package/dist/types/hindsight/client.test.d.ts +1 -0
  17. package/dist/types/irc/bus.d.ts +5 -4
  18. package/dist/types/mcp/smithery-registry.d.ts +1 -0
  19. package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
  20. package/dist/types/modes/components/advisor-message.d.ts +4 -2
  21. package/dist/types/modes/components/tool-execution.d.ts +9 -6
  22. package/dist/types/modes/components/transcript-container.d.ts +1 -0
  23. package/dist/types/modes/theme/theme.d.ts +1 -1
  24. package/dist/types/session/agent-session.d.ts +13 -13
  25. package/dist/types/session/indexed-session-storage.d.ts +2 -2
  26. package/dist/types/session/session-storage.d.ts +31 -3
  27. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
  28. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
  29. package/dist/types/ssh/connection-manager.d.ts +19 -0
  30. package/dist/types/ssh/sshfs-mount.d.ts +10 -1
  31. package/dist/types/subprocess/worker-client.d.ts +20 -6
  32. package/dist/types/task/discovery.d.ts +5 -2
  33. package/dist/types/task/renderer.d.ts +1 -0
  34. package/dist/types/tools/ast-grep.d.ts +4 -2
  35. package/dist/types/tools/bash.d.ts +27 -0
  36. package/dist/types/tools/browser/render.d.ts +2 -0
  37. package/dist/types/tools/debug.d.ts +1 -0
  38. package/dist/types/tools/eval-render.d.ts +2 -0
  39. package/dist/types/tools/glob.d.ts +4 -2
  40. package/dist/types/tools/grep.d.ts +4 -3
  41. package/dist/types/tools/path-utils.d.ts +7 -0
  42. package/dist/types/tools/renderers.d.ts +24 -0
  43. package/dist/types/tools/ssh.d.ts +4 -1
  44. package/dist/types/tts/index.d.ts +2 -0
  45. package/dist/types/tts/speakable.d.ts +47 -0
  46. package/dist/types/tts/speech-enhancer.d.ts +46 -0
  47. package/dist/types/tts/streaming-player.d.ts +1 -2
  48. package/dist/types/tts/tts-client.d.ts +11 -10
  49. package/dist/types/tts/tts-protocol.d.ts +7 -0
  50. package/dist/types/tts/vocalizer.d.ts +15 -8
  51. package/dist/types/utils/fetch-timeout.d.ts +4 -0
  52. package/dist/types/utils/git.d.ts +2 -0
  53. package/dist/types/web/search/providers/base.d.ts +1 -0
  54. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  55. package/package.json +12 -12
  56. package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
  57. package/src/advisor/__tests__/advisor.test.ts +1 -1
  58. package/src/cli/gallery-fixtures/fs.ts +2 -2
  59. package/src/cli/gallery-fixtures/search.ts +2 -2
  60. package/src/cli/models-cli.ts +19 -0
  61. package/src/cli/update-cli.test.ts +28 -0
  62. package/src/cli/update-cli.ts +35 -8
  63. package/src/cli.ts +27 -5
  64. package/src/commands/update.ts +8 -2
  65. package/src/config/config-file.ts +6 -6
  66. package/src/config/model-resolver.ts +31 -10
  67. package/src/config/settings-schema.ts +21 -0
  68. package/src/cursor.ts +1 -1
  69. package/src/dap/client.ts +134 -36
  70. package/src/discovery/helpers.ts +8 -0
  71. package/src/edit/file-snapshot-store.ts +12 -1
  72. package/src/edit/hashline/diff.ts +4 -13
  73. package/src/edit/hashline/execute.ts +1 -1
  74. package/src/edit/hashline/params.ts +5 -12
  75. package/src/edit/renderer.ts +4 -2
  76. package/src/edit/streaming.ts +15 -5
  77. package/src/export/html/tool-views.generated.js +34 -34
  78. package/src/extensibility/custom-tools/loader.ts +3 -3
  79. package/src/extensibility/extensions/loader.ts +10 -3
  80. package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
  81. package/src/extensibility/plugins/installer.ts +12 -3
  82. package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
  83. package/src/extensibility/plugins/loader.ts +30 -1
  84. package/src/extensibility/plugins/manager.ts +32 -8
  85. package/src/extensibility/plugins/parser.ts +7 -5
  86. package/src/extensibility/tool-event-input.ts +1 -1
  87. package/src/hindsight/client.test.ts +33 -0
  88. package/src/hindsight/client.ts +42 -22
  89. package/src/internal-urls/docs-index.generated.txt +1 -1
  90. package/src/irc/bus.ts +5 -4
  91. package/src/main.ts +7 -1
  92. package/src/mcp/oauth-flow.ts +93 -4
  93. package/src/mcp/smithery-auth.ts +3 -0
  94. package/src/mcp/smithery-connect.ts +9 -0
  95. package/src/mcp/smithery-registry.test.ts +51 -0
  96. package/src/mcp/smithery-registry.ts +27 -4
  97. package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
  98. package/src/modes/components/advisor-message.ts +13 -10
  99. package/src/modes/components/assistant-message.ts +1 -1
  100. package/src/modes/components/move-overlay.ts +35 -23
  101. package/src/modes/components/status-line/component.ts +11 -4
  102. package/src/modes/components/tool-execution.ts +119 -20
  103. package/src/modes/components/transcript-container.ts +26 -0
  104. package/src/modes/components/tree-selector.ts +10 -3
  105. package/src/modes/controllers/event-controller.ts +23 -3
  106. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  107. package/src/modes/interactive-mode.ts +1 -0
  108. package/src/modes/rpc/rpc-client.ts +29 -16
  109. package/src/modes/theme/shimmer.ts +49 -15
  110. package/src/modes/theme/theme.ts +7 -0
  111. package/src/prompts/goals/goal-mode-context.md +4 -0
  112. package/src/prompts/goals/goal-todo-context.md +10 -2
  113. package/src/prompts/system/speech-rewrite.md +15 -0
  114. package/src/prompts/system/tiny-title-system.md +1 -1
  115. package/src/prompts/system/title-system-marker.md +2 -1
  116. package/src/prompts/system/title-system.md +2 -1
  117. package/src/prompts/tools/ast-grep.md +3 -3
  118. package/src/prompts/tools/glob.md +1 -1
  119. package/src/prompts/tools/grep.md +1 -1
  120. package/src/sdk.ts +8 -5
  121. package/src/session/agent-session.ts +329 -79
  122. package/src/session/indexed-session-storage.ts +40 -3
  123. package/src/session/session-history-format.ts +6 -2
  124. package/src/session/session-manager.ts +83 -14
  125. package/src/session/session-storage.ts +112 -26
  126. package/src/slash-commands/helpers/usage-report.ts +6 -1
  127. package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
  128. package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
  129. package/src/ssh/connection-manager.ts +44 -11
  130. package/src/ssh/sshfs-mount.ts +27 -4
  131. package/src/subprocess/worker-client.ts +161 -10
  132. package/src/task/discovery.ts +25 -2
  133. package/src/task/executor.ts +24 -4
  134. package/src/task/render.ts +26 -12
  135. package/src/task/renderer.ts +1 -0
  136. package/src/task/worktree.ts +144 -16
  137. package/src/tiny/text.ts +109 -17
  138. package/src/tools/ast-grep.ts +20 -17
  139. package/src/tools/bash.ts +46 -9
  140. package/src/tools/browser/render.ts +2 -0
  141. package/src/tools/debug.ts +1 -0
  142. package/src/tools/eval-render.ts +5 -2
  143. package/src/tools/gh-renderer.ts +3 -0
  144. package/src/tools/glob.ts +24 -20
  145. package/src/tools/grep.ts +39 -37
  146. package/src/tools/path-utils.ts +55 -10
  147. package/src/tools/read.ts +23 -6
  148. package/src/tools/renderers.ts +24 -0
  149. package/src/tools/ssh.ts +25 -7
  150. package/src/tts/index.ts +2 -0
  151. package/src/tts/speakable.ts +382 -0
  152. package/src/tts/speech-enhancer.ts +204 -0
  153. package/src/tts/streaming-player.ts +71 -16
  154. package/src/tts/tts-client.ts +11 -10
  155. package/src/tts/tts-protocol.ts +14 -5
  156. package/src/tts/tts-worker.ts +52 -49
  157. package/src/tts/vocalizer.ts +277 -46
  158. package/src/utils/clipboard.ts +49 -12
  159. package/src/utils/fetch-timeout.ts +10 -0
  160. package/src/utils/git.ts +8 -0
  161. package/src/web/search/index.ts +8 -0
  162. package/src/web/search/providers/base.ts +1 -0
  163. package/src/web/search/providers/gemini.ts +23 -6
package/src/tiny/text.ts CHANGED
@@ -152,6 +152,34 @@ const FILLER_TITLE_TOKENS = new Set<string>([
152
152
  ]);
153
153
 
154
154
  const TITLE_WORD = /[\p{L}\p{N}]+/gu;
155
+ const COMMON_TITLE_ACRONYMS = new Set<string>([
156
+ "API",
157
+ "CLI",
158
+ "CPU",
159
+ "CRUD",
160
+ "CSS",
161
+ "DNS",
162
+ "ETL",
163
+ "GPU",
164
+ "HTML",
165
+ "HTTP",
166
+ "HTTPS",
167
+ "ID",
168
+ "JSON",
169
+ "LLM",
170
+ "REST",
171
+ "SDK",
172
+ "SSH",
173
+ "TCP",
174
+ "TLS",
175
+ "TUI",
176
+ "UI",
177
+ "URI",
178
+ "URL",
179
+ "UX",
180
+ "XML",
181
+ "YAML",
182
+ ]);
155
183
 
156
184
  /**
157
185
  * True when a first user message is too low-signal to title (greeting, ack,
@@ -191,51 +219,115 @@ export function normalizeGeneratedTitle(value: string | null | undefined, source
191
219
  * Reconcile a generated title's casing against the user's own message.
192
220
  *
193
221
  * The title prompt asks for sentence case, but small title models still mangle
194
- * casing two ways: they sprout stray interior capitals on ordinary words
195
- * (`daemon` → `dAemon`) and they flatten proper nouns the user cares about
196
- * (`TinyVMM` → `tinyvmm`). The user's message is the source of truth, so per
197
- * title token:
222
+ * casing three ways: they sprout stray interior capitals on ordinary words
223
+ * (`daemon` → `dAemon`), they flatten proper nouns the user cased distinctively
224
+ * (`TinyVMM` → `tinyvmm`), and they title-case ALL-CAPS acronyms as if they
225
+ * were sentence words (`CNPG` → `Cnpg`). The user's message is the source of
226
+ * truth, so per title token:
198
227
  * 1. typed verbatim in the message → keep it (the user established the casing);
199
228
  * 2. else the message has the same word with *distinctive* mixed casing
200
229
  * (`TinyVMM`, `iOS`, `IDs`) → adopt the user's casing (restoration);
201
- * 3. else it's a camelCase artifact (lowercase word + stray interior capital,
230
+ * 3. else the model produced a plain title-cased artifact (`Cnpg`) whose
231
+ * lowercased form is a likely ALL-CAPS acronym in a non-shouty source
232
+ * (`CNPG`, `API`, `ETL`) → restore the source acronym;
233
+ * 4. else it's a camelCase artifact (lowercase word + stray interior capital,
202
234
  * `dAemon`) the user never wrote → lowercase it;
203
- * 4. else leave it — preserves model-cased proper nouns like `GitHub`, `OAuth`.
235
+ * 5. else leave it — preserves model-cased proper nouns like `GitHub`, `OAuth`.
204
236
  *
205
- * Restoration is limited to distinctively *mixed*-cased source tokens: a sentence
206
- * that merely *starts* with `For` can't force a mid-title `for` to `For`, and
207
- * emphatic all-caps (`ALL ERROR HANDLING`) is never re-shouted over sentence case.
237
+ * Restoration is limited to avoid three failure modes: a sentence that merely
238
+ * *starts* with `For` can't force a mid-title `for` to `For` (distinctive
239
+ * requires interior mixed casing); emphatic all-caps input (`ALL ERROR
240
+ * HANDLING`, `FIX the BUG NOW`) is never re-shouted — see {@link isShoutySource};
241
+ * and ordinary all-caps English words (`FIX`, `WORK`, `BUG`) are not treated as
242
+ * restorable acronyms unless they carry a stronger acronym signal.
208
243
  */
209
244
  function reconcileTitleCasing(title: string, sourceText: string): string {
210
245
  const verbatim = new Set<string>();
211
246
  const distinctive = new Map<string, string>();
247
+ const acronyms = new Map<string, string>();
248
+ const shouty = isShoutySource(sourceText);
212
249
  for (const [token] of sourceText.matchAll(TITLE_WORD)) {
213
250
  verbatim.add(token);
214
251
  if (isDistinctiveCasing(token)) {
215
252
  const lower = token.toLowerCase();
216
253
  if (!distinctive.has(lower)) distinctive.set(lower, token);
254
+ } else if (!shouty && isAllCapsAcronym(token)) {
255
+ const lower = token.toLowerCase();
256
+ if (!acronyms.has(lower)) acronyms.set(lower, token);
217
257
  }
218
258
  }
219
259
  return title.replace(TITLE_WORD, token => {
220
260
  if (verbatim.has(token)) return token;
221
- const restored = distinctive.get(token.toLowerCase());
261
+ const lower = token.toLowerCase();
262
+ const restored = distinctive.get(lower);
222
263
  if (restored) return restored;
223
- return isCamelArtifact(token) ? token.toLowerCase() : token;
264
+ if (isTitleCasedArtifact(token)) {
265
+ const acronym = acronyms.get(lower);
266
+ if (acronym) return acronym;
267
+ }
268
+ return isCamelArtifact(token) ? lower : token;
224
269
  });
225
270
  }
226
271
 
227
272
  /** Mixed-case identifier the user cased deliberately (`TinyVMM`, `iOS`, `IDs`):
228
273
  * an interior/repeated capital plus at least one lowercase letter. Only these
229
- * are restored when the model flattens them.
230
- *
231
- * Pure all-caps is intentionally excluded. The model preserves its own acronyms
232
- * verbatim regardless, so restoring all-caps from the source would only ever
233
- * re-shout emphatic input (`ALL ERROR HANDLING`, `FIX THE BUG`) over the
234
- * sentence case the prompt asks for. */
274
+ * are restored when the model flattens them. */
235
275
  function isDistinctiveCasing(token: string): boolean {
236
276
  return /\p{Ll}/u.test(token) && /\p{L}\p{Lu}/u.test(token);
237
277
  }
238
278
 
279
+ /** Multi-letter ALL-CAPS source token with a stronger acronym signal than a
280
+ * plain emphasized word. Consonant-only tokens (`CNPG`, `SQL`, `JWT`) are
281
+ * restored, digit-bearing identifiers are restored, and common technical
282
+ * acronyms (`API`, `JSON`, `URL`) are allowlisted. Ordinary emphasized words
283
+ * (`FIX`, `WORK`, `BUG`) contain vowels and are not restored from source. */
284
+ function isAllCapsAcronym(token: string): boolean {
285
+ if (!isAllCapsWord(token)) return false;
286
+ const upper = token.toUpperCase();
287
+ if (COMMON_TITLE_ACRONYMS.has(upper)) return true;
288
+ if (/\p{N}/u.test(token)) return true;
289
+ return !/[AEIOU]/.test(upper);
290
+ }
291
+
292
+ /** Multi-letter ALL-CAPS word in the source. Used for shout detection, not for
293
+ * acronym restoration — shouted English words (`FIX`, `WORK`) still count as
294
+ * shouty even though they are not restorable acronyms. Requires an actual
295
+ * uppercase letter so caseless scripts (CJK) never register as shouting and
296
+ * disable acronym restoration for those messages. */
297
+ function isAllCapsWord(token: string): boolean {
298
+ const letters = token.match(/\p{L}/gu);
299
+ if (!letters || letters.length < 2) return false;
300
+ return /\p{Lu}/u.test(token) && !/\p{Ll}/u.test(token);
301
+ }
302
+
303
+ /** Plain title-cased word (`Cnpg`, `Etl`): starts uppercase, has one-or-more
304
+ * lowercase letters, no interior uppercase. This is the artifact a title model
305
+ * produces when it sentence-cases an unfamiliar ALL-CAPS acronym; PascalCase
306
+ * proper nouns like `GitHub`/`OAuth` have an interior capital and are
307
+ * excluded so we don't misidentify them. */
308
+ function isTitleCasedArtifact(token: string): boolean {
309
+ if (!/^\p{Lu}/u.test(token)) return false;
310
+ if (!/\p{Ll}/u.test(token)) return false;
311
+ return !/\p{Lu}/u.test(token.slice(1));
312
+ }
313
+
314
+ /** True when the source text is shouting — ≥2 consecutive multi-letter
315
+ * ALL-CAPS tokens (`FIX the BUG NOW` has `BUG NOW`; `ALL ERROR HANDLING`
316
+ * has all three adjacent). Acronym restoration is disabled for shouty input
317
+ * so we don't re-shout emphatic prose the model correctly de-shouted. */
318
+ function isShoutySource(sourceText: string): boolean {
319
+ let run = 0;
320
+ for (const [token] of sourceText.matchAll(TITLE_WORD)) {
321
+ if (isAllCapsWord(token)) {
322
+ run += 1;
323
+ if (run >= 2) return true;
324
+ } else {
325
+ run = 0;
326
+ }
327
+ }
328
+ return false;
329
+ }
330
+
239
331
  /** A lowercase word carrying a stray interior capital (`dAemon`, `cReate`): the
240
332
  * model-mangled shape we flatten when the user never wrote it. PascalCase proper
241
333
  * nouns (`GitHub`, `OAuth`) start uppercase and are left untouched. */
@@ -19,7 +19,7 @@ import { createFileRecorder, formatResultPath } from "./file-recorder";
19
19
  import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
20
20
  import { formatMatchLine } from "./match-line-format";
21
21
  import type { OutputMeta } from "./output-meta";
22
- import { resolveToolSearchScope } from "./path-utils";
22
+ import { resolveToolSearchScope, toPathList } from "./path-utils";
23
23
  import {
24
24
  appendParseErrorsBulletList,
25
25
  capParseErrors,
@@ -37,11 +37,9 @@ import { toolResult } from "./tool-result";
37
37
 
38
38
  const astGrepSchema = type({
39
39
  pat: type("string").describe("ast pattern"),
40
- paths: type("string")
41
- .describe("file, directory, glob, or internal URL to search")
42
- .array()
43
- .atLeastLength(1)
44
- .describe("files, directories, globs, or internal URLs to search"),
40
+ "path?": type("string").describe(
41
+ 'file, directory, glob, or internal URL to search; pass several as a semicolon-delimited list ("src; tests"). Omitted -> searches the workspace root (".")',
42
+ ),
45
43
  "skip?": type("number").describe("matches to skip"),
46
44
  });
47
45
 
@@ -159,23 +157,23 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
159
157
  readonly examples: readonly ToolExample<typeof astGrepSchema.inferIn>[] = [
160
158
  {
161
159
  caption: "Search TypeScript files under src",
162
- call: { pat: "console.log($$$)", paths: ["src/**/*.ts"] },
160
+ call: { pat: "console.log($$$)", path: "src/**/*.ts" },
163
161
  },
164
162
  {
165
163
  caption: "Named imports from a specific package",
166
- call: { pat: 'import { $$$IMPORTS } from "react"', paths: ["src/**/*.ts"] },
164
+ call: { pat: 'import { $$$IMPORTS } from "react"', path: "src/**/*.ts" },
167
165
  },
168
166
  {
169
167
  caption: "Arrow functions assigned to a const",
170
- call: { pat: "const $NAME = ($$$ARGS) => $BODY", paths: ["src/utils/**/*.ts"] },
168
+ call: { pat: "const $NAME = ($$$ARGS) => $BODY", path: "src/utils/**/*.ts" },
171
169
  },
172
170
  {
173
171
  caption: "Method call on any object, ignoring method name with `$_`",
174
- call: { pat: "logger.$_($$$ARGS)", paths: ["src/**/*.ts"] },
172
+ call: { pat: "logger.$_($$$ARGS)", path: "src/**/*.ts" },
175
173
  },
176
174
  {
177
175
  caption: "Loosest existence check for a symbol in one file",
178
- call: { pat: "processItems", paths: ["src/worker.ts"] },
176
+ call: { pat: "processItems", path: "src/worker.ts" },
179
177
  },
180
178
  ];
181
179
  readonly loadMode = "discoverable";
@@ -201,8 +199,10 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
201
199
  if (!Number.isFinite(skip) || skip < 0) {
202
200
  throw new ToolError("skip must be a non-negative number");
203
201
  }
202
+ const scopedPaths = toPathList(params.path);
203
+ const rawPaths = scopedPaths.length > 0 ? scopedPaths : ["."];
204
204
  const scope = await resolveToolSearchScope({
205
- rawPaths: params.paths,
205
+ rawPaths,
206
206
  cwd: this.session.cwd,
207
207
  internalUrlAction: "search",
208
208
  settings: this.session.settings,
@@ -275,7 +275,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
275
275
 
276
276
  if (result.matches.length === 0) {
277
277
  const noMatchMessage = cappedParseErrors.length
278
- ? "No matches found. Parse issues mean the query may be mis-scoped; narrow `paths` before concluding absence."
278
+ ? "No matches found. Parse issues mean the query may be mis-scoped; narrow `path` before concluding absence."
279
279
  : "No matches found";
280
280
  const parseMessage = cappedParseErrors.length
281
281
  ? `\n${formatParseErrors(cappedParseErrors, parseErrorsTotal).join("\n")}`
@@ -375,7 +375,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
375
375
  displayContent: displayLines.join("\n"),
376
376
  };
377
377
  if (result.limitReached) {
378
- outputLines.push("", "Result limit reached; narrow paths or increase limit.");
378
+ outputLines.push("", "Result limit reached; narrow path or increase limit.");
379
379
  }
380
380
  if (cappedParseErrors.length) {
381
381
  outputLines.push("", ...formatParseErrors(cappedParseErrors, parseErrorsTotal));
@@ -392,6 +392,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
392
392
 
393
393
  interface AstGrepRenderArgs {
394
394
  pat?: string;
395
+ path?: string | string[];
396
+ /** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
395
397
  paths?: string[];
396
398
  skip?: number;
397
399
  }
@@ -402,7 +404,8 @@ export const astGrepToolRenderer = {
402
404
  inline: true,
403
405
  renderCall(args: AstGrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
404
406
  const meta: string[] = [];
405
- if (args.paths?.length) meta.push(`in ${args.paths.join(", ")}`);
407
+ const scopePaths = toPathList(args.path ?? args.paths);
408
+ if (scopePaths.length) meta.push(`in ${scopePaths.join(", ")}`);
406
409
  if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
407
410
 
408
411
  const description = args.pat ?? "?";
@@ -436,7 +439,7 @@ export const astGrepToolRenderer = {
436
439
  const header = renderStatusLine({ icon: "warning", title: "AST Grep", description, meta }, uiTheme);
437
440
  const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
438
441
  if (details?.parseErrors?.length) {
439
- lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `paths` before concluding absence"));
442
+ lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `path` before concluding absence"));
440
443
  appendParseErrorsBulletList(lines, details.parseErrors, uiTheme, details.parseErrorsTotal);
441
444
  }
442
445
  return new Text(lines.join("\n"), 0, 0);
@@ -487,7 +490,7 @@ export const astGrepToolRenderer = {
487
490
 
488
491
  const extraLines: string[] = [];
489
492
  if (limitReached) {
490
- extraLines.push(uiTheme.fg("warning", "limit reached; narrow paths or increase limit"));
493
+ extraLines.push(uiTheme.fg("warning", "limit reached; narrow path or increase limit"));
491
494
  }
492
495
  if (details?.parseErrors?.length) {
493
496
  extraLines.push(
package/src/tools/bash.ts CHANGED
@@ -46,6 +46,33 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
46
46
  const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
47
47
  const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
48
48
 
49
+ /**
50
+ * Shape a shell command line for an ACP-conformant `terminal/create` request.
51
+ *
52
+ * ACP's `command` field is documented as the executable and `args` as its
53
+ * argv tail (see https://agentclientprotocol.com/protocol/v1/terminals), so a
54
+ * spec-conformant client `spawn(command, args)`s them directly — no implicit
55
+ * shell. A raw `bash` tool line ("git status && echo x | head") therefore has
56
+ * to be wrapped in an explicit shell invocation, otherwise the client tries
57
+ * to spawn the whole line as argv[0] and fails with `ENOENT` for anything
58
+ * containing a space, pipe, `&&`, redirect, or `$(...)`.
59
+ *
60
+ * The wrap reuses the same shell binary + args the local `bash-executor` would
61
+ * pick via `settings.getShellConfig()` — Git Bash / `bash.exe` on Windows,
62
+ * `$SHELL` (bash/zsh) with the `sh` fallback on POSIX — so the ACP path
63
+ * preserves `bash` tool semantics (`$VAR`, `$(...)`, `source`, POSIX quoting,
64
+ * `-l`) instead of dropping to `cmd.exe` on Windows. The agent host's shell
65
+ * path is used as a proxy for the client's, matching the near-universal
66
+ * ACP deployment shape of an editor spawning omp as a co-hosted subprocess.
67
+ */
68
+ export function wrapShellLineForClientTerminal(
69
+ line: string,
70
+ shellConfig: { shell: string; args: string[]; prefix?: string | undefined },
71
+ ): { command: string; args: string[] } {
72
+ const finalLine = shellConfig.prefix ? `${shellConfig.prefix} ${line}` : line;
73
+ return { command: shellConfig.shell, args: [...shellConfig.args, finalLine] };
74
+ }
75
+
49
76
  /**
50
77
  * Bash patterns flagged as safety critical for approval policy.
51
78
  *
@@ -842,8 +869,10 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
842
869
  // Skip when pty=true (PTY needs the local terminal UI).
843
870
  if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
844
871
  const bridgeWallTimeStart = performance.now();
872
+ const shellSpawn = wrapShellLineForClientTerminal(command, this.session.settings.getShellConfig());
845
873
  const handle = await clientBridge.createTerminal({
846
- command,
874
+ command: shellSpawn.command,
875
+ args: shellSpawn.args,
847
876
  cwd: commandCwd,
848
877
  env: resolvedEnv
849
878
  ? Object.entries(resolvedEnv).map(([name, value]) => ({ name, value: value as string }))
@@ -1151,22 +1180,30 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1151
1180
  renderCall(args: TArgs, options: RenderResultOptions, uiTheme: Theme): Component {
1152
1181
  const renderArgs = toBashRenderArgs(args, config);
1153
1182
  const cmdLines = formatBashCommandLines(renderArgs, uiTheme);
1154
- const header =
1155
- config.showHeader === false
1156
- ? undefined
1157
- : renderStatusLine({ icon: "pending", title: config.resolveTitle(args, options) }, uiTheme);
1158
1183
  const outputBlock = new CachedOutputBlock();
1159
1184
  return markFramedBlockComponent({
1160
- render: (width: number): readonly string[] =>
1161
- outputBlock.render(
1185
+ render: (width: number): readonly string[] => {
1186
+ const header =
1187
+ config.showHeader === false
1188
+ ? undefined
1189
+ : renderStatusLine(
1190
+ {
1191
+ icon: options.spinnerFrame !== undefined ? "running" : "pending",
1192
+ spinnerFrame: options.spinnerFrame,
1193
+ title: config.resolveTitle(args, options),
1194
+ },
1195
+ uiTheme,
1196
+ );
1197
+ return outputBlock.render(
1162
1198
  {
1163
1199
  header,
1164
- state: "pending",
1200
+ state: options.spinnerFrame !== undefined ? "running" : "pending",
1165
1201
  sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
1166
1202
  width,
1167
1203
  },
1168
1204
  uiTheme,
1169
- ),
1205
+ );
1206
+ },
1170
1207
  invalidate: () => {
1171
1208
  outputBlock.invalidate();
1172
1209
  },
@@ -187,6 +187,8 @@ function extractTextOutput(content: Array<{ type: string; text?: string }> | und
187
187
  }
188
188
 
189
189
  export const browserToolRenderer = {
190
+ animatedPendingPreview: (args: unknown) => (args as BrowserRenderArgs).action === "run",
191
+ animatedPartialResult: (args: unknown) => (args as BrowserRenderArgs).action === "run",
190
192
  renderCall(args: BrowserRenderArgs, options: RenderResultOptions, theme: Theme): Component {
191
193
  const action = args.action;
192
194
  if (action === "run") {
@@ -583,6 +583,7 @@ function summarizeDebugCall(args: DebugRenderArgs): string {
583
583
  }
584
584
 
585
585
  export const debugToolRenderer = {
586
+ animatedPartialResult: true,
586
587
  renderCall(args: DebugRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
587
588
  const text = renderStatusLine({ icon: "pending", title: "Debug", description: summarizeDebugCall(args) }, theme);
588
589
  return new Text(text, 0, 0);
@@ -489,6 +489,8 @@ function formatCellOutputLines(
489
489
  }
490
490
 
491
491
  export const evalToolRenderer = {
492
+ animatedPendingPreview: true,
493
+ animatedPartialResult: true,
492
494
  renderCall(args: EvalRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
493
495
  const cells = getRenderCells(args);
494
496
 
@@ -502,7 +504,7 @@ export const evalToolRenderer = {
502
504
 
503
505
  return markFramedBlockComponent({
504
506
  render: (width: number): readonly string[] => {
505
- const key = `${options.expanded ? 1 : 0}|${previewWindowRows()}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
507
+ const key = `${options.expanded ? 1 : 0}|${options.spinnerFrame ?? "-"}|${previewWindowRows()}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
506
508
  if (cached && cached.key === key && cached.width === width) {
507
509
  return cached.result;
508
510
  }
@@ -518,7 +520,8 @@ export const evalToolRenderer = {
518
520
  index: i,
519
521
  total: cells.length,
520
522
  title: cell.title,
521
- status: "pending",
523
+ status: options.spinnerFrame !== undefined ? "running" : "pending",
524
+ spinnerFrame: options.spinnerFrame,
522
525
  width,
523
526
  // Viewport-sized tail window following the newest streamed code
524
527
  // line; renderResult keeps the same cap so the cell never snaps
@@ -416,6 +416,9 @@ function renderWatchCall(args: GithubToolRenderArgs, options: RenderResultOption
416
416
  }
417
417
 
418
418
  export const githubToolRenderer = {
419
+ // No animatedPendingPreview: renderCall materializes plain Text components
420
+ // once per display rebuild (no render closure), so a live spinner interval
421
+ // would request 30fps repaints while the visible glyph stays frozen.
419
422
  renderCall(args: GithubToolRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
420
423
  const op = typeof args.op === "string" && args.op.trim().length > 0 ? args.op.trim() : undefined;
421
424
  if (op === "run_watch") {
package/src/tools/glob.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  partitionExistingPaths,
27
27
  resolveExplicitFindPatterns,
28
28
  resolveToCwd,
29
+ toPathList,
29
30
  } from "./path-utils";
30
31
  import {
31
32
  createCachedComponent,
@@ -38,11 +39,9 @@ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
38
39
  import { toolResult } from "./tool-result";
39
40
 
40
41
  const findSchema = type({
41
- paths: type("string")
42
- .describe("glob including search path")
43
- .array()
44
- .atLeastLength(1)
45
- .describe("globs including search paths"),
42
+ "path?": type("string").describe(
43
+ 'glob, file, or directory to search — a single path or a semicolon-delimited list ("src/**/*.ts; test/**/*.ts"). Omitted -> searches the workspace root (".")',
44
+ ),
46
45
  "hidden?": type("boolean").describe("include hidden files"),
47
46
  "gitignore?": type("boolean").describe("respect gitignore"),
48
47
  "limit?": type("number").describe("max results"),
@@ -110,19 +109,19 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
110
109
  readonly examples: readonly ToolExample<typeof findSchema.infer>[] = [
111
110
  {
112
111
  caption: "Glob files",
113
- call: { paths: ["src/**/*.ts"] },
112
+ call: { path: "src/**/*.ts" },
114
113
  },
115
114
  {
116
- caption: "Multiple targets — separate array elements",
117
- call: { paths: ["src/**/*.ts", "test/**/*.ts"] },
115
+ caption: "Multiple targets — semicolon-delimited list",
116
+ call: { path: "src/**/*.ts; test/**/*.ts" },
118
117
  },
119
118
  {
120
119
  caption: "Glob gitignored files like .env",
121
- call: { paths: [".env*"], gitignore: false },
120
+ call: { path: ".env*", gitignore: false },
122
121
  },
123
122
  {
124
123
  caption: "Glob directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
125
- call: { paths: ["**/tests"] },
124
+ call: { path: "**/tests" },
126
125
  },
127
126
  ];
128
127
  readonly strict = true;
@@ -144,13 +143,15 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
144
143
  onUpdate?: AgentToolUpdateCallback<GlobToolDetails>,
145
144
  _context?: AgentToolContext,
146
145
  ): Promise<AgentToolResult<GlobToolDetails>> {
147
- const { paths, limit, hidden, gitignore } = params;
146
+ const { path: pathInput, limit, hidden, gitignore } = params;
148
147
 
149
148
  return untilAborted(signal, async () => {
150
149
  const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
150
+ const scopedPaths = toPathList(pathInput);
151
+ const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
151
152
  const rawPatternInputs = this.#customOps
152
- ? paths
153
- : await expandDelimitedPathEntries(paths, this.session.cwd, { splitter: parseFindPattern });
153
+ ? effectivePaths
154
+ : await expandDelimitedPathEntries(effectivePaths, this.session.cwd, { splitter: parseFindPattern });
154
155
  const rawPatterns = rawPatternInputs.map(input => normalizePathLikeInput(input).replace(/\\/g, "/"));
155
156
  const internalRouter = InternalUrlRouter.instance();
156
157
  const normalizedPatterns: string[] = [];
@@ -180,7 +181,7 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
180
181
  normalizedPatterns.push(resource.sourcePath);
181
182
  }
182
183
  if (normalizedPatterns.some(pattern => pattern.length === 0)) {
183
- throw new ToolError("`paths` must contain non-empty globs or paths");
184
+ throw new ToolError("`path` must contain non-empty globs or paths");
184
185
  }
185
186
 
186
187
  // Tolerate missing entries in a multi-path call: skip ones whose base
@@ -468,12 +469,15 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
468
469
  // =============================================================================
469
470
 
470
471
  interface GlobRenderArgs {
472
+ path?: string | string[];
473
+ /** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
471
474
  paths?: string | string[];
472
475
  limit?: number;
473
476
  }
474
477
 
475
- function formatGlobRenderPaths(paths: GlobRenderArgs["paths"]): string | undefined {
476
- return Array.isArray(paths) ? paths.join(", ") : paths;
478
+ function formatGlobRenderPaths(args: GlobRenderArgs | undefined): string | undefined {
479
+ const list = toPathList(args?.path ?? args?.paths);
480
+ return list.length > 0 ? list.join(", ") : undefined;
477
481
  }
478
482
 
479
483
  const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
@@ -493,7 +497,7 @@ export const globToolRenderer = {
493
497
  icon: "pending",
494
498
  title: "Glob",
495
499
  titleColor: "toolTitle",
496
- description: formatGlobRenderPaths(args.paths) || "*",
500
+ description: formatGlobRenderPaths(args) || "*",
497
501
  meta,
498
502
  },
499
503
  uiTheme,
@@ -533,7 +537,7 @@ export const globToolRenderer = {
533
537
  iconOverride: globStatusIcon(uiTheme),
534
538
  title: "Glob",
535
539
  titleColor: "toolTitle",
536
- description: formatGlobRenderPaths(args?.paths),
540
+ description: formatGlobRenderPaths(args),
537
541
  meta: [formatCount("file", lines.length)],
538
542
  },
539
543
  uiTheme,
@@ -573,7 +577,7 @@ export const globToolRenderer = {
573
577
  icon: "warning",
574
578
  title: "Glob",
575
579
  titleColor: "toolTitle",
576
- description: formatGlobRenderPaths(args?.paths),
580
+ description: formatGlobRenderPaths(args),
577
581
  meta: ["0 files"],
578
582
  },
579
583
  uiTheme,
@@ -590,7 +594,7 @@ export const globToolRenderer = {
590
594
  ...(truncated ? { icon: "warning" as const } : { iconOverride: globStatusIcon(uiTheme) }),
591
595
  title: "Glob",
592
596
  titleColor: "toolTitle",
593
- description: formatGlobRenderPaths(args?.paths),
597
+ description: formatGlobRenderPaths(args),
594
598
  meta,
595
599
  },
596
600
  uiTheme,