@oh-my-pi/pi-coding-agent 16.1.22 → 16.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CHANGELOG.md +99 -1
  2. package/dist/cli.js +4387 -4164
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +2 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/cli/gc-cli.d.ts +58 -0
  11. package/dist/types/collab/display-name.d.ts +3 -0
  12. package/dist/types/collab/host.d.ts +0 -2
  13. package/dist/types/commands/gc.d.ts +37 -0
  14. package/dist/types/commands/worktree.d.ts +0 -3
  15. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  16. package/dist/types/config/model-discovery.d.ts +6 -1
  17. package/dist/types/config/model-registry.d.ts +6 -2
  18. package/dist/types/config/model-resolver.d.ts +12 -0
  19. package/dist/types/config/models-config-schema.d.ts +29 -2
  20. package/dist/types/config/models-config.d.ts +22 -1
  21. package/dist/types/config/settings-schema.d.ts +145 -21
  22. package/dist/types/config/settings.d.ts +13 -0
  23. package/dist/types/dap/config.d.ts +1 -1
  24. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  25. package/dist/types/edit/renderer.d.ts +4 -0
  26. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  27. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  28. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  29. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  30. package/dist/types/internal-urls/index.d.ts +1 -0
  31. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  32. package/dist/types/internal-urls/router.d.ts +1 -1
  33. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  34. package/dist/types/internal-urls/types.d.ts +19 -2
  35. package/dist/types/irc/bus.d.ts +6 -0
  36. package/dist/types/mcp/transports/stdio.d.ts +25 -1
  37. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  38. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  39. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  40. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  41. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  42. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  43. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  44. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  45. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  46. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  49. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  51. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  52. package/dist/types/modes/interactive-mode.d.ts +10 -1
  53. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  54. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  55. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +1 -0
  56. package/dist/types/modes/theme/theme.d.ts +2 -1
  57. package/dist/types/modes/types.d.ts +9 -1
  58. package/dist/types/sdk.d.ts +2 -2
  59. package/dist/types/session/agent-session.d.ts +2 -1
  60. package/dist/types/session/session-listing.d.ts +10 -1
  61. package/dist/types/session/session-manager.d.ts +13 -0
  62. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  63. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  64. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  65. package/dist/types/ssh/connection-manager.d.ts +2 -0
  66. package/dist/types/ssh/file-transfer.d.ts +79 -0
  67. package/dist/types/ssh/utils.d.ts +6 -0
  68. package/dist/types/system-prompt.d.ts +5 -0
  69. package/dist/types/task/executor.d.ts +16 -0
  70. package/dist/types/tiny/text.d.ts +1 -1
  71. package/dist/types/tools/builtin-names.d.ts +5 -1
  72. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  73. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  74. package/dist/types/tools/index.d.ts +3 -3
  75. package/dist/types/tools/path-utils.d.ts +29 -0
  76. package/dist/types/tools/plan-mode-guard.d.ts +7 -0
  77. package/dist/types/tools/read.d.ts +2 -2
  78. package/dist/types/tools/render-utils.d.ts +8 -0
  79. package/dist/types/tools/renderers.d.ts +11 -0
  80. package/dist/types/tools/ssh.d.ts +2 -0
  81. package/dist/types/tools/todo.d.ts +0 -16
  82. package/dist/types/tools/write.d.ts +2 -2
  83. package/dist/types/utils/active-repo-context.d.ts +8 -0
  84. package/dist/types/utils/image-resize.d.ts +1 -0
  85. package/dist/types/utils/markit-cache.d.ts +23 -0
  86. package/dist/types/utils/markit.d.ts +5 -1
  87. package/dist/types/utils/prompt-path.d.ts +1 -0
  88. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  89. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  90. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  91. package/dist/types/web/search/providers/xai.d.ts +13 -0
  92. package/dist/types/web/search/types.d.ts +18 -2
  93. package/package.json +30 -15
  94. package/scripts/bench-guard.ts +1 -1
  95. package/scripts/build-binary.ts +9 -9
  96. package/scripts/bundle-dist.ts +2 -2
  97. package/src/advisor/__tests__/advisor.test.ts +40 -4
  98. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  99. package/src/advisor/advise-tool.ts +1 -1
  100. package/src/advisor/emission-guard.ts +172 -0
  101. package/src/advisor/index.ts +1 -0
  102. package/src/advisor/runtime.ts +11 -0
  103. package/src/advisor/watchdog.ts +12 -1
  104. package/src/cli/args.ts +5 -2
  105. package/src/cli/auth-broker-cli.ts +17 -0
  106. package/src/cli/config-cli.ts +4 -0
  107. package/src/cli/flag-tables.ts +7 -4
  108. package/src/cli/gallery-cli.ts +14 -2
  109. package/src/cli/gallery-fixtures/edit.ts +60 -0
  110. package/src/cli/gallery-fixtures/fs.ts +17 -17
  111. package/src/cli/gallery-fixtures/search.ts +4 -4
  112. package/src/cli/gc-cli.ts +939 -0
  113. package/src/cli/usage-cli.ts +20 -1
  114. package/src/cli/web-search-cli.ts +1 -1
  115. package/src/cli-commands.ts +1 -0
  116. package/src/collab/display-name.ts +13 -0
  117. package/src/collab/guest.ts +5 -1
  118. package/src/collab/host.ts +2 -13
  119. package/src/commands/gc.ts +46 -0
  120. package/src/commands/worktree.ts +6 -0
  121. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  122. package/src/config/model-discovery.ts +77 -8
  123. package/src/config/model-registry.ts +89 -11
  124. package/src/config/model-resolver.ts +39 -1
  125. package/src/config/models-config-schema.ts +41 -5
  126. package/src/config/models-config.ts +4 -1
  127. package/src/config/settings-schema.ts +130 -27
  128. package/src/config/settings.ts +216 -7
  129. package/src/cursor.ts +1 -1
  130. package/src/dap/config.ts +152 -8
  131. package/src/dap/session.ts +1 -1
  132. package/src/discovery/helpers.ts +3 -1
  133. package/src/edit/hashline/diff.ts +14 -3
  134. package/src/edit/hashline/execute.ts +42 -6
  135. package/src/edit/hashline/filesystem.ts +49 -8
  136. package/src/edit/index.ts +1 -0
  137. package/src/edit/modes/patch.ts +11 -1
  138. package/src/edit/renderer.ts +140 -13
  139. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  140. package/src/eval/__tests__/julia-prelude.test.ts +18 -0
  141. package/src/eval/agent-bridge.ts +26 -3
  142. package/src/eval/jl/runner.jl +7 -1
  143. package/src/eval/js/tool-bridge.ts +2 -2
  144. package/src/export/html/index.ts +1 -1
  145. package/src/export/html/template.js +3 -1
  146. package/src/export/html/tool-views.generated.js +20 -20
  147. package/src/extensibility/extensions/types.ts +22 -22
  148. package/src/extensibility/hooks/types.ts +11 -11
  149. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +10 -3
  150. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +31 -10
  151. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  152. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  153. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  154. package/src/internal-urls/docs-index.generated.txt +2 -2
  155. package/src/internal-urls/docs-index.ts +2 -3
  156. package/src/internal-urls/index.ts +1 -0
  157. package/src/internal-urls/registry-helpers.ts +19 -4
  158. package/src/internal-urls/router.ts +5 -3
  159. package/src/internal-urls/ssh-protocol.ts +367 -0
  160. package/src/internal-urls/types.ts +19 -2
  161. package/src/irc/bus.ts +11 -3
  162. package/src/lsp/index.ts +8 -1
  163. package/src/mcp/oauth-discovery.ts +20 -20
  164. package/src/mcp/tool-bridge.ts +32 -2
  165. package/src/mcp/transports/stdio.test.ts +20 -3
  166. package/src/mcp/transports/stdio.ts +45 -14
  167. package/src/memories/index.ts +1 -1
  168. package/src/modes/acp/acp-event-mapper.ts +2 -2
  169. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  170. package/src/modes/components/agent-hub.ts +3 -0
  171. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  172. package/src/modes/components/custom-editor.ts +90 -5
  173. package/src/modes/components/move-overlay.ts +282 -0
  174. package/src/modes/components/plan-review-overlay.ts +35 -35
  175. package/src/modes/components/plugin-selector.ts +6 -1
  176. package/src/modes/components/queue-mode-selector.ts +6 -1
  177. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  178. package/src/modes/components/session-selector.ts +11 -10
  179. package/src/modes/components/settings-defs.ts +14 -1
  180. package/src/modes/components/settings-selector.ts +196 -29
  181. package/src/modes/components/show-images-selector.ts +6 -1
  182. package/src/modes/components/status-line/component.ts +108 -28
  183. package/src/modes/components/status-line/segments.ts +5 -1
  184. package/src/modes/components/status-line/types.ts +2 -0
  185. package/src/modes/components/theme-selector.ts +6 -1
  186. package/src/modes/components/thinking-selector.ts +6 -1
  187. package/src/modes/components/tool-execution.ts +25 -9
  188. package/src/modes/components/tree-selector.ts +5 -5
  189. package/src/modes/controllers/command-controller.ts +140 -47
  190. package/src/modes/controllers/event-controller.ts +59 -3
  191. package/src/modes/controllers/input-controller.ts +70 -4
  192. package/src/modes/controllers/selector-controller.ts +15 -2
  193. package/src/modes/controllers/streaming-reveal.ts +17 -0
  194. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  195. package/src/modes/interactive-mode.ts +142 -59
  196. package/src/modes/internal-url-autocomplete.ts +17 -2
  197. package/src/modes/prompt-action-autocomplete.ts +3 -1
  198. package/src/modes/running-subagent-badge.ts +13 -0
  199. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  200. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  201. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  202. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  203. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  204. package/src/modes/theme/mermaid-rendering.test.ts +53 -0
  205. package/src/modes/theme/theme.ts +42 -15
  206. package/src/modes/types.ts +9 -1
  207. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  208. package/src/priority.json +15 -0
  209. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  210. package/src/prompts/advisor/system.md +21 -7
  211. package/src/prompts/agents/designer.md +1 -1
  212. package/src/prompts/agents/explore.md +1 -1
  213. package/src/prompts/agents/librarian.md +2 -2
  214. package/src/prompts/agents/plan.md +2 -2
  215. package/src/prompts/agents/reviewer.md +1 -1
  216. package/src/prompts/agents/task.md +2 -2
  217. package/src/prompts/system/active-repo-context.md +4 -0
  218. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  219. package/src/prompts/system/plan-mode-active.md +12 -3
  220. package/src/prompts/system/project-prompt.md +2 -2
  221. package/src/prompts/system/system-prompt.md +8 -6
  222. package/src/prompts/tools/bash.md +2 -2
  223. package/src/prompts/tools/checkpoint.md +1 -1
  224. package/src/prompts/tools/{find.md → glob.md} +1 -1
  225. package/src/prompts/tools/{search.md → grep.md} +3 -3
  226. package/src/prompts/tools/read.md +4 -2
  227. package/src/sdk.ts +98 -29
  228. package/src/session/agent-session.ts +828 -191
  229. package/src/session/session-history-format.ts +2 -2
  230. package/src/session/session-listing.ts +35 -2
  231. package/src/session/session-manager.ts +46 -0
  232. package/src/slash-commands/builtin-registry.ts +158 -22
  233. package/src/slash-commands/helpers/usage-report.ts +23 -2
  234. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  235. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  236. package/src/ssh/connection-manager.ts +12 -15
  237. package/src/ssh/file-transfer.ts +209 -0
  238. package/src/ssh/utils.ts +24 -0
  239. package/src/system-prompt.ts +60 -28
  240. package/src/task/executor.ts +96 -36
  241. package/src/task/index.ts +3 -3
  242. package/src/task/render.ts +14 -13
  243. package/src/task/worktree.ts +38 -1
  244. package/src/tiny/text.ts +49 -4
  245. package/src/tiny/worker.ts +3 -3
  246. package/src/tools/acp-bridge.ts +6 -1
  247. package/src/tools/bash.ts +2 -2
  248. package/src/tools/builtin-names.ts +26 -2
  249. package/src/tools/{find.ts → glob.ts} +48 -42
  250. package/src/tools/{search.ts → grep.ts} +311 -129
  251. package/src/tools/index.ts +13 -14
  252. package/src/tools/irc.ts +6 -2
  253. package/src/tools/path-utils.ts +64 -1
  254. package/src/tools/plan-mode-guard.ts +26 -13
  255. package/src/tools/read.ts +35 -6
  256. package/src/tools/render-utils.ts +14 -0
  257. package/src/tools/renderers.ts +15 -4
  258. package/src/tools/ssh.ts +17 -2
  259. package/src/tools/todo.ts +4 -26
  260. package/src/tools/write.ts +25 -9
  261. package/src/utils/active-repo-context.ts +143 -0
  262. package/src/utils/edit-mode.ts +19 -2
  263. package/src/utils/image-resize.ts +88 -7
  264. package/src/utils/lang-from-path.ts +3 -3
  265. package/src/utils/markit-cache.ts +166 -0
  266. package/src/utils/markit.ts +86 -18
  267. package/src/utils/prompt-path.ts +3 -0
  268. package/src/utils/shell-snapshot.ts +1 -1
  269. package/src/utils/title-generator.ts +1 -1
  270. package/src/web/search/provider.ts +54 -34
  271. package/src/web/search/providers/duckduckgo.ts +140 -0
  272. package/src/web/search/providers/firecrawl.ts +144 -0
  273. package/src/web/search/providers/tinyfish.ts +159 -0
  274. package/src/web/search/providers/xai.ts +292 -0
  275. package/src/web/search/providers/zai.ts +142 -56
  276. package/src/web/search/types.ts +6 -2
@@ -2,7 +2,7 @@
2
2
  * Edit tool renderer and LSP batching helpers.
3
3
  */
4
4
 
5
- import { HL_FILE_PREFIX, HL_FILE_SUFFIX } from "@oh-my-pi/hashline";
5
+ import { HL_FILE_PREFIX, HL_FILE_SUFFIX, HL_MOVE_KEYWORD, HL_REM_KEYWORD } from "@oh-my-pi/hashline";
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
7
  import { sliceWithWidth, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
8
8
  import { sanitizeText } from "@oh-my-pi/pi-utils";
@@ -28,7 +28,15 @@ import {
28
28
  shortenPath,
29
29
  truncateDiffByHunk,
30
30
  } from "../tools/render-utils";
31
- import { fileHyperlink, framedBlock, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
31
+ import {
32
+ fileHyperlink,
33
+ framedBlock,
34
+ Hasher,
35
+ type RenderCache,
36
+ renderStatusLine,
37
+ truncateToWidth,
38
+ WidthAwareText,
39
+ } from "../tui";
32
40
  import type { EditMode } from "../utils/edit-mode";
33
41
  import type { DiffError, DiffResult } from "./diff";
34
42
  import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
@@ -62,6 +70,8 @@ export interface EditToolPerFileResult {
62
70
  oldText?: string;
63
71
  /** Source-of-truth content after the edit; `undefined` for delete operations. */
64
72
  newText?: string;
73
+ /** Pre-move source path; set only when the edit moved/renamed the file. The header renders `sourcePath → path`. */
74
+ sourcePath?: string;
65
75
  }
66
76
 
67
77
  export interface EditToolDetails {
@@ -85,6 +95,8 @@ export interface EditToolDetails {
85
95
  oldText?: string;
86
96
  /** Source-of-truth content after the edit; `undefined` for delete operations. */
87
97
  newText?: string;
98
+ /** Pre-move source path; set only when the edit moved/renamed the file. The header renders `sourcePath → path`. */
99
+ sourcePath?: string;
88
100
  }
89
101
 
90
102
  // ═══════════════════════════════════════════════════════════════════════════
@@ -119,8 +131,16 @@ type EditRenderEntry = {
119
131
  op?: Operation;
120
132
  };
121
133
 
134
+ interface HashlineInputEntry {
135
+ path: string;
136
+ op?: Operation;
137
+ rename?: string;
138
+ /** A SWAP/DEL/INS line-editing op precedes the file op — keeps a move framed. */
139
+ hasLineEdits?: boolean;
140
+ }
141
+
122
142
  interface HashlineInputRenderSummary {
123
- entries: Array<{ path: string }>;
143
+ entries: HashlineInputEntry[];
124
144
  }
125
145
 
126
146
  interface ApplyPatchRenderSummary {
@@ -293,9 +313,10 @@ function renderEditHeader(
293
313
  linkPath?: string;
294
314
  statsSuffix?: string;
295
315
  extraSuffix?: string;
316
+ title?: string;
296
317
  },
297
318
  ): string {
298
- const title = getOperationTitle(options.op);
319
+ const title = options.title ?? getOperationTitle(options.op);
299
320
  const descriptionOptions: EditPathDisplayOptions = {
300
321
  rename: options.rename,
301
322
  firstChangedLine: options.firstChangedLine,
@@ -327,6 +348,51 @@ function renderEditHeader(
327
348
  return buildHeader(fitted.description);
328
349
  }
329
350
 
351
+ /**
352
+ * Inline status row for delete / move-only edits — they carry no diff, so they
353
+ * render as a single line instead of an empty framed container. The completed
354
+ * result uses the eraser/move glyph; a still-streaming call uses the shared
355
+ * pending hourglass like every other tool.
356
+ */
357
+ function renderInlineEditRow(
358
+ uiTheme: Theme,
359
+ opts: { op?: Operation; rename?: string; rawPath: string; linkPath?: string; pending: boolean },
360
+ ): Component {
361
+ const isDelete = opts.op === "delete";
362
+ return new WidthAwareText(
363
+ width =>
364
+ renderEditHeader(width, uiTheme, {
365
+ icon: opts.pending ? "pending" : undefined,
366
+ iconOverride: opts.pending
367
+ ? undefined
368
+ : uiTheme.styledSymbol(isDelete ? "tool.delete" : "tool.move", "accent"),
369
+ op: opts.op,
370
+ title: isDelete ? "Delete" : "Move",
371
+ rawPath: opts.rawPath,
372
+ rename: opts.rename,
373
+ linkPath: opts.linkPath,
374
+ }),
375
+ 0,
376
+ 0,
377
+ );
378
+ }
379
+
380
+ /**
381
+ * Whether a streaming edit call carries any payload worth boxing (a diff
382
+ * preview, replacement text, or a non-empty edits array). Used to keep a
383
+ * move-with-edits framed while a payload-less move/delete folds to an inline
384
+ * row — gated on args, not the async preview, so it can't flash inline before
385
+ * the diff arrives.
386
+ */
387
+ function hasEditCallPayload(args: EditRenderArgs, renderContext: EditRenderContext | undefined): boolean {
388
+ const multi = renderContext?.perFileDiffPreview;
389
+ if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) return true;
390
+ if (args.previewDiff || args.diff || args.newText || args.patch) return true;
391
+ if (Array.isArray(args.edits) && args.edits.length > 0) return true;
392
+ if (renderContext?.editStreamingFallback) return true;
393
+ return false;
394
+ }
395
+
330
396
  function renderPlainTextPreview(text: string, uiTheme: Theme, _filePath?: string): string {
331
397
  const previewLines = sanitizeText(text).split("\n");
332
398
  let preview = "\n\n";
@@ -491,15 +557,39 @@ function parseHashlineInputPreviewHeader(line: string): string | null {
491
557
  return previewPath.length > 0 ? previewPath : null;
492
558
  }
493
559
 
494
- function getHashlineInputPaths(input: string): string[] {
560
+ // Line-editing op headers (SWAP/DEL/INS family), distinct from the file-level
561
+ // REM/MV ops. Body rows are always `+TEXT`, so this only matches real headers.
562
+ const HL_LINE_OP_HEADER = /^(?:SWAP|DEL|INS)\b/;
563
+
564
+ /**
565
+ * Walk a (possibly mid-stream) hashline payload into per-section descriptors:
566
+ * the target path plus any file-level op (`REM` → delete, `MV dest` → rename)
567
+ * and whether a line edit precedes it. Tolerant of partial input so the call
568
+ * preview can label a delete/move before the payload finishes streaming.
569
+ */
570
+ function getHashlineInputSections(input: string): HashlineInputEntry[] {
495
571
  const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
496
- const paths: string[] = [];
572
+ const entries: HashlineInputEntry[] = [];
573
+ let current: HashlineInputEntry | undefined;
497
574
  for (const rawLine of stripped.split("\n")) {
498
575
  const line = rawLine.replace(/\r$/, "");
499
- const path = parseHashlineInputPreviewHeader(line);
500
- if (path) paths.push(path);
576
+ const headerPath = parseHashlineInputPreviewHeader(line);
577
+ if (headerPath) {
578
+ current = { path: headerPath };
579
+ entries.push(current);
580
+ continue;
581
+ }
582
+ if (!current) continue;
583
+ const trimmed = line.trim();
584
+ if (trimmed === HL_REM_KEYWORD) {
585
+ current.op = "delete";
586
+ } else if (trimmed.startsWith(`${HL_MOVE_KEYWORD} `)) {
587
+ current.rename = normalizeHashlineInputPreviewPath(trimmed.slice(HL_MOVE_KEYWORD.length + 1));
588
+ } else if (HL_LINE_OP_HEADER.test(trimmed)) {
589
+ current.hasLineEdits = true;
590
+ }
501
591
  }
502
- return paths;
592
+ return entries;
503
593
  }
504
594
 
505
595
  function getHashlineInputRenderSummary(
@@ -509,7 +599,7 @@ function getHashlineInputRenderSummary(
509
599
  if (editMode !== "hashline" || typeof args.input !== "string") {
510
600
  return undefined;
511
601
  }
512
- return { entries: getHashlineInputPaths(args.input).map(path => ({ path })) };
602
+ return { entries: getHashlineInputSections(args.input) };
513
603
  }
514
604
 
515
605
  function getApplyPatchRenderSummary(
@@ -627,12 +717,24 @@ export const editToolRenderer = {
627
717
  firstHashlineInputEntry?.path ||
628
718
  firstApplyPatchEntry?.path ||
629
719
  "";
630
- const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || firstApplyPatchEntry?.rename;
631
- const op = editArgs.op || firstEdit?.op || firstApplyPatchEntry?.op;
720
+ const rename =
721
+ editArgs.rename ||
722
+ firstEdit?.rename ||
723
+ firstEdit?.move ||
724
+ firstApplyPatchEntry?.rename ||
725
+ firstHashlineInputEntry?.rename;
726
+ const op = editArgs.op || firstEdit?.op || firstApplyPatchEntry?.op || firstHashlineInputEntry?.op;
632
727
  let fileCount = hashlineInputSummary?.entries.length ?? applyPatchSummary?.entries.length ?? 0;
633
728
  if (Array.isArray(editArgs.edits)) {
634
729
  fileCount = countEditFiles(editArgs.edits);
635
730
  }
731
+ // Delete / payload-less move calls render as an inline pending row (no
732
+ // empty framed container), mirroring the completed result but with the
733
+ // shared hourglass instead of the eraser/move glyph.
734
+ const hasPayload = hasEditCallPayload(editArgs, renderContext) || Boolean(firstHashlineInputEntry?.hasLineEdits);
735
+ if (fileCount <= 1 && !applyPatchSummary?.error && (op === "delete" || (rename !== undefined && !hasPayload))) {
736
+ return renderInlineEditRow(uiTheme, { op, rename, rawPath, pending: true });
737
+ }
636
738
  const callPreviewCaches: RenderedStringCache[] = [];
637
739
  return framedBlock(uiTheme, width => {
638
740
  // No status icon on the head row: it's the head of the framed block,
@@ -702,7 +804,9 @@ function renderSingleFileResult(
702
804
  const firstEdit = args?.edits?.[0];
703
805
  const hashlineInputSummary = getHashlineInputRenderSummary(args ?? {}, options.renderContext?.editMode);
704
806
  const firstHashlineInputEntry = hashlineInputSummary?.entries[0];
807
+ const moveSource = details && "sourcePath" in details ? details.sourcePath : undefined;
705
808
  const rawPath =
809
+ moveSource ||
706
810
  args?.file_path ||
707
811
  args?.path ||
708
812
  filePathFromEditEntry(firstEdit?.path) ||
@@ -719,12 +823,26 @@ function renderSingleFileResult(
719
823
  (result.content?.find(c => c.type === "text")?.text ?? "")
720
824
  : "";
721
825
 
826
+ // Delete and move-only results carry no diff to box. Per design these render
827
+ // as an inline status row (eraser / move glyph) rather than an empty framed
828
+ // container. Errors, no-ops, creates, move-with-edits, and anything with
829
+ // diagnostics keep the framed block below.
830
+ if (!isError && !details?.diff && !details?.diagnostics && (op === "delete" || rename)) {
831
+ const linkPath = details && "path" in details ? details.path : undefined;
832
+ return renderInlineEditRow(uiTheme, { op, rename, rawPath, linkPath, pending: false });
833
+ }
834
+
722
835
  let diffSectionRenderDiffFn: ((t: string, o?: { filePath?: string }) => string) | undefined;
723
836
  const diffSectionCache = createRenderedStringCache();
724
837
 
725
838
  return framedBlock(uiTheme, width => {
726
839
  const { expanded, renderContext } = options;
727
- const editDiffPreview = renderContext?.editDiffPreview;
840
+ // A finalized result is authoritative: its `details` describe exactly
841
+ // what happened. The shared streaming `editDiffPreview` is a call-phase
842
+ // artifact (in a batch it reflects only the first file), so consulting it
843
+ // for an empty-diff delete/move/no-op result mislabels the card. Fall
844
+ // back to the preview only when no details exist yet.
845
+ const editDiffPreview = details ? undefined : renderContext?.editDiffPreview;
728
846
  const renderDiffFn = renderContext?.renderDiff ?? plainDiffRender;
729
847
 
730
848
  if (diffSectionRenderDiffFn !== renderDiffFn) {
@@ -756,6 +874,15 @@ function renderSingleFileResult(
756
874
  if (errorText) body = uiTheme.fg("error", replaceTabs(errorText));
757
875
  } else if (details?.diff) {
758
876
  body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
877
+ } else if (details) {
878
+ // Authoritative result with no textual diff: a delete, a move-only
879
+ // rename, or a genuine no-op. The header already names the op
880
+ // (Delete / `src → dst`); only a true no-op needs an explanatory
881
+ // body so an empty card isn't mistaken for a stalled edit.
882
+ if (op !== "delete" && op !== "create" && !rename) {
883
+ const noChangePath = linkPath ? shortenPath(linkPath) : rawPath ? shortenPath(rawPath) : "";
884
+ body = uiTheme.fg("dim", `No changes were made${noChangePath ? ` to ${noChangePath}` : ""}.`);
885
+ }
759
886
  } else if (editDiffPreview) {
760
887
  if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error));
761
888
  else if (editDiffPreview.diff)
@@ -3,7 +3,11 @@ import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { TempDir } from "@oh-my-pi/pi-utils";
5
5
  import { Settings } from "../../config/settings";
6
+ import { AgentProtocolHandler } from "../../internal-urls/agent-protocol";
7
+ import { resetRegisteredArtifactDirsForTests } from "../../internal-urls/registry-helpers";
6
8
  import type { PlanModeState } from "../../plan-mode/state";
9
+ import { AgentRegistry } from "../../registry/agent-registry";
10
+ import type { AgentSession } from "../../session/agent-session";
7
11
  import * as taskDiscovery from "../../task/discovery";
8
12
  import type { ExecutorOptions } from "../../task/executor";
9
13
  import * as taskExecutor from "../../task/executor";
@@ -154,6 +158,8 @@ function spyConcurrencyBarrier(limit: number): { maxInFlight: () => number } {
154
158
  describe("runEvalAgent", () => {
155
159
  afterEach(() => {
156
160
  vi.restoreAllMocks();
161
+ AgentRegistry.resetGlobalForTests();
162
+ resetRegisteredArtifactDirsForTests();
157
163
  });
158
164
 
159
165
  it("resolves the default task agent and agent overrides", async () => {
@@ -246,6 +252,62 @@ describe("runEvalAgent", () => {
246
252
  const options = runSpy.mock.calls[0]?.[0];
247
253
  if (!options) throw new Error("runSubprocess was not called");
248
254
  expect(options.enableLsp).toBe(false);
255
+ expect(options.keepAlive).toBe(false);
256
+ });
257
+
258
+ it("registers temp artifact dirs for in-memory handle results so agent URLs resolve", async () => {
259
+ mockAgents();
260
+ vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
261
+ if (!options.artifactsDir) throw new Error("artifactsDir missing");
262
+ await fs.mkdir(options.artifactsDir, { recursive: true });
263
+ await fs.writeFile(path.join(options.artifactsDir, `${options.id}.md`), "recoverable output");
264
+ return singleResult(options, { output: "recoverable output" });
265
+ });
266
+
267
+ const result = await runEvalAgent({ prompt: "hello", handle: true }, { session: makeSession() });
268
+ const resource = await new AgentProtocolHandler().resolve(new URL(`agent://${result.details.id}`) as never);
269
+
270
+ expect(resource.content).toBe("recoverable output");
271
+ });
272
+
273
+ it("unregisters eval subagents through the bridge cleanup path", async () => {
274
+ AgentRegistry.resetGlobalForTests();
275
+ mockAgents();
276
+ let disposed = false;
277
+ const cleanupSession = {
278
+ dispose: async () => {
279
+ disposed = true;
280
+ },
281
+ } as unknown as AgentSession;
282
+ vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
283
+ AgentRegistry.global().register({
284
+ id: options.id,
285
+ displayName: options.id,
286
+ kind: "sub",
287
+ session: cleanupSession,
288
+ status: "idle",
289
+ });
290
+ await taskExecutor.finalizeSubagentLifecycle({
291
+ id: options.id,
292
+ session: cleanupSession,
293
+ aborted: false,
294
+ keepAlive: options.keepAlive !== false,
295
+ isolated: options.worktree !== undefined,
296
+ agentIdleTtlMs: 0,
297
+ reviveSession: null,
298
+ });
299
+ return singleResult(options);
300
+ });
301
+
302
+ await runEvalAgent({ prompt: "hello", label: "Cleanup" }, { session: makeSession() });
303
+
304
+ expect(disposed).toBe(true);
305
+ expect(AgentRegistry.global().get("Cleanup")).toBeUndefined();
306
+ expect(
307
+ AgentRegistry.global()
308
+ .listVisibleTo("Main")
309
+ .map(ref => ref.id),
310
+ ).not.toContain("Cleanup");
249
311
  });
250
312
 
251
313
  it("maps successful and failed subagent results", async () => {
@@ -1096,6 +1158,45 @@ describe("runEvalAgent isolation", () => {
1096
1158
  await fs.rm(path.dirname(persistedPath!), { recursive: true, force: true });
1097
1159
  });
1098
1160
 
1161
+ it("throws schema calls when nested patch application reports a warning", async () => {
1162
+ mockAgents();
1163
+ mockIsolationContext();
1164
+ const nestedPatch = "diff --git a/file b/file\n";
1165
+ vi.spyOn(isolationRunner, "runIsolatedSubprocess").mockImplementation(async opts =>
1166
+ singleResult(opts.baseOptions, {
1167
+ output: JSON.stringify({ status: "ok" }),
1168
+ patchPath: `/artifacts/${opts.agentId}.patch`,
1169
+ nestedPatches: [{ relativePath: "sub/nested", patch: nestedPatch }],
1170
+ }),
1171
+ );
1172
+ vi.spyOn(isolationRunner, "mergeIsolatedChanges").mockResolvedValue({
1173
+ summary: "\n\nApplied patches: yes",
1174
+ changesApplied: true,
1175
+ hadAnyChanges: true,
1176
+ mergedBranchForNestedPatches: false,
1177
+ });
1178
+ vi.spyOn(isolationRunner, "applyEligibleNestedPatches").mockResolvedValue(
1179
+ "\n\n<system-notification>Some nested repository patches failed to apply.</system-notification>",
1180
+ );
1181
+
1182
+ await expect(
1183
+ runEvalAgent(
1184
+ {
1185
+ prompt: "structured",
1186
+ isolated: true,
1187
+ schema: {
1188
+ type: "object",
1189
+ properties: { status: { type: "string" } },
1190
+ required: ["status"],
1191
+ },
1192
+ },
1193
+ { session: isolatedSession() },
1194
+ ),
1195
+ ).rejects.toThrow(
1196
+ /nested patch apply failed.*Some nested repository patches failed to apply.*nested-0-sub_nested\.patch/s,
1197
+ );
1198
+ });
1199
+
1099
1200
  it("skips the merge phase when apply=false and surfaces the patch artifact instead", async () => {
1100
1201
  mockAgents();
1101
1202
  mockIsolationContext();
@@ -45,4 +45,22 @@ nothing
45
45
  expect(result.output).toContain("META=alpha:true");
46
46
  expect(result.output).toContain("MULTI=2:alpha:json");
47
47
  }, 30_000);
48
+
49
+ it("surfaces the exception type and message in the error output, not just stack frames", async () => {
50
+ using tempDir = TempDir.createSync("@omp-eval-julia-error-");
51
+ const result = await executeJulia(`println("="^8)\nmissing_var_xyz + 1`, {
52
+ cwd: tempDir.path(),
53
+ sessionId: `julia-prelude-error:${crypto.randomUUID()}`,
54
+ kernelOwnerId: OWNER_ID,
55
+ reset: true,
56
+ });
57
+
58
+ // The rendered error must carry the actual exception, not only the
59
+ // runner-internal backtrace frames (regression: traceback-only output
60
+ // hid `ename`/`evalue`).
61
+ expect(result.output).toContain("UndefVarError");
62
+ expect(result.output).toContain("missing_var_xyz");
63
+ // Frames are still present alongside the message.
64
+ expect(result.output).toContain("top-level scope");
65
+ }, 30_000);
48
66
  });
@@ -8,6 +8,7 @@ import { prompt, Snowflake } from "@oh-my-pi/pi-utils";
8
8
  import { type } from "arktype";
9
9
  import { resolveAgentModelPatterns } from "../config/model-resolver";
10
10
  import type { LocalProtocolOptions } from "../internal-urls";
11
+ import { registerArtifactsDir } from "../internal-urls/registry-helpers";
11
12
  import { MCPManager } from "../mcp/manager";
12
13
  import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
13
14
  import { MAIN_AGENT_ID } from "../registry/agent-registry";
@@ -189,6 +190,7 @@ function getOutputManager(session: ToolSession): AgentOutputManager {
189
190
  interface ArtifactPaths {
190
191
  sessionFile: string | null;
191
192
  artifactsDir: string;
193
+ unregisterArtifactsDir?: () => void;
192
194
  /**
193
195
  * True when `artifactsDir` was created off the session path (no session
194
196
  * file). Caller is then free to `rm -rf` it once all isolated patch
@@ -203,7 +205,8 @@ async function getArtifacts(session: ToolSession): Promise<ArtifactPaths> {
203
205
  const tempArtifactsDir = sessionArtifactsDir === null;
204
206
  const artifactsDir = sessionArtifactsDir ?? path.join(os.tmpdir(), `omp-eval-agent-${Snowflake.next()}`);
205
207
  await fs.mkdir(artifactsDir, { recursive: true });
206
- return { sessionFile, artifactsDir, tempArtifactsDir };
208
+ const unregisterArtifactsDir = tempArtifactsDir ? registerArtifactsDir(artifactsDir) : undefined;
209
+ return { sessionFile, artifactsDir, unregisterArtifactsDir, tempArtifactsDir };
207
210
  }
208
211
 
209
212
  /**
@@ -229,6 +232,10 @@ async function persistNestedPatches(
229
232
  return written;
230
233
  }
231
234
 
235
+ function plainIsolationSummary(summary: string): string {
236
+ return summary.replace(/<\/?system-notification>/g, "").trim();
237
+ }
238
+
232
239
  function emitProgressStatus(emitStatus: ((event: JsStatusEvent) => void) | undefined, progress: AgentProgress): void {
233
240
  if (!emitStatus) return;
234
241
  const preview = (progress.assignment ?? progress.task ?? "").split("\n")[0]?.slice(0, 120);
@@ -324,7 +331,7 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
324
331
  };
325
332
  const parentArtifactManager = options.session.getArtifactManager?.() ?? undefined;
326
333
  const mcpManager = options.session.mcpManager ?? MCPManager.instance();
327
- const { sessionFile, artifactsDir, tempArtifactsDir } = await getArtifacts(options.session);
334
+ const { sessionFile, artifactsDir, unregisterArtifactsDir, tempArtifactsDir } = await getArtifacts(options.session);
328
335
  const outputManager = getOutputManager(options.session);
329
336
  const id = await outputManager.allocate(outputIdBase(parsed.label, agentName));
330
337
  const assignment = parsed.prompt.trim();
@@ -385,6 +392,7 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
385
392
  // must not be killed by `task.maxRuntimeMs`. Force the limit off
386
393
  // regardless of the inherited session setting.
387
394
  maxRuntimeMs: 0,
395
+ keepAlive: false,
388
396
  mcpManager,
389
397
  contextFiles,
390
398
  skills: availableSkills,
@@ -494,7 +502,7 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
494
502
  );
495
503
  }
496
504
 
497
- mergeSummary += await applyEligibleNestedPatches({
505
+ const nestedSummary = await applyEligibleNestedPatches({
498
506
  result,
499
507
  repoRoot: isolationContext.repoRoot,
500
508
  mergeMode,
@@ -502,6 +510,20 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
502
510
  mergedBranchForNestedPatches: outcome.mergedBranchForNestedPatches,
503
511
  commitMessage: buildCommitMessage(),
504
512
  });
513
+ mergeSummary += nestedSummary;
514
+ if (structured && nestedSummary.trim()) {
515
+ const recoveryParts: string[] = [];
516
+ if (result.nestedPatches?.length) {
517
+ const nestedPaths = await persistNestedPatches(artifactsDir, result.id, result.nestedPatches);
518
+ recoveryParts.push(
519
+ `Captured nested repository patches (${result.nestedPatches.length}) preserved at: ${nestedPaths.join(", ")}.`,
520
+ );
521
+ }
522
+ const recoveryHint = recoveryParts.length > 0 ? ` ${recoveryParts.join(" ")}` : "";
523
+ throw new ToolError(
524
+ `agent() isolated nested patch apply failed for ${result.id}: ${plainIsolationSummary(nestedSummary)}${recoveryHint}`,
525
+ );
526
+ }
505
527
  } else if (result.branchName) {
506
528
  mergeSummary = `\n\nIsolation: changes captured on branch \`${result.branchName}\` (apply=false). Not merged.`;
507
529
  } else if (result.patchPath) {
@@ -526,6 +548,7 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
526
548
  const shouldCleanupTempArtifacts = tempArtifactsDir && !parsed.handle && (!isIsolated || changesApplied === true);
527
549
  if (shouldCleanupTempArtifacts) {
528
550
  await fs.rm(artifactsDir, { recursive: true, force: true });
551
+ unregisterArtifactsDir?.();
529
552
  }
530
553
 
531
554
  options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
@@ -522,7 +522,13 @@ function emit_error(rid, err, bt)
522
522
  end
523
523
  err_str = String(take!(io))
524
524
 
525
- tb = String[]
525
+ # Seed the traceback with the rendered exception text so the array is a
526
+ # self-contained error display, matching the Python and Ruby runners. The
527
+ # host shows `traceback` verbatim when present and only falls back to
528
+ # `ename: evalue` when it is empty, so a frames-only traceback would hide
529
+ # the real error. Julia's `showerror` output already embeds the exception
530
+ # type for nearly every error and mirrors what the REPL prints after `ERROR: `.
531
+ tb = isempty(err_str) ? String[] : String[err_str]
526
532
  for frame in stacktrace(bt)
527
533
  file = string(frame.file)
528
534
  line = frame.line
@@ -88,9 +88,9 @@ function summarizeToolResult(
88
88
  path: record.path,
89
89
  count: details.matchCount ?? undefined,
90
90
  });
91
- case "find":
91
+ case "glob":
92
92
  return withError({
93
- op: "find",
93
+ op: "glob",
94
94
  pattern: record.pattern,
95
95
  count: details.fileCount ?? undefined,
96
96
  matches: Array.isArray(details.files) ? details.files.slice(0, 20) : undefined,
@@ -9,7 +9,7 @@ import { SessionManager } from "../../session/session-manager";
9
9
  import templateCss from "./template.css" with { type: "text" };
10
10
  import templateHtml from "./template.html" with { type: "text" };
11
11
  import templateJs from "./template.js" with { type: "text" };
12
- // Pre-built React tool renderers: built by `bun --cwd=packages/collab-web run build:tool-views`,
12
+ // Pre-built React tool renderers: built by `gen:tool-views` (`bun run gen:tool-views`),
13
13
  // run automatically by root `prepare` on install and by `prepack` at publish.
14
14
  import toolViewsJs from "./tool-views.generated.js" with { type: "text" };
15
15
  import { webExportThemeVars } from "./web-palette";
@@ -433,10 +433,12 @@
433
433
  const cmd = rawCmd.replace(/[\n\t]/g, ' ').trim().slice(0, 50);
434
434
  return `[bash: ${cmd}${rawCmd.length > 50 ? '...' : ''}]`;
435
435
  }
436
+ case 'search':
436
437
  case 'grep':
437
438
  return `[grep: /${args.pattern || ''}/ in ${shortenPath(String((args.paths || [args.path || '.']).join(', ')))}]`;
438
439
  case 'find':
439
- return `[find: ${shortenPath(String((args.paths || [args.pattern || '.']).join(', ')))}]`;
440
+ case 'glob':
441
+ return `[glob: ${shortenPath(String((args.paths || [args.pattern || '.']).join(', ')))}]`;
440
442
  case 'ls':
441
443
  return `[ls: ${shortenPath(String(args.path || '.'))}]`;
442
444
  default: {