@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -6,18 +6,20 @@
6
6
  */
7
7
  import path from "node:path";
8
8
  import type { Component } from "@oh-my-pi/pi-tui";
9
- import { Container, Text } from "@oh-my-pi/pi-tui";
9
+ import { Container, Markdown, Text } from "@oh-my-pi/pi-tui";
10
10
  import { formatNumber } from "@oh-my-pi/pi-utils";
11
11
  import { settings } from "../config/settings";
12
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
13
  import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
14
- import type { Theme } from "../modes/theme/theme";
14
+ import { shimmerEnabled, shimmerText } from "../modes/theme/shimmer";
15
+ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
15
16
  import {
16
17
  formatBadge,
17
18
  formatDuration,
18
19
  formatMoreItems,
19
20
  formatStatusIcon,
20
21
  replaceTabs,
22
+ type ToolUIStatus,
21
23
  truncateToWidth,
22
24
  } from "../tools/render-utils";
23
25
  import {
@@ -28,7 +30,8 @@ import {
28
30
  type ReportFindingDetails,
29
31
  type SubmitReviewDetails,
30
32
  } from "../tools/review";
31
- import { Ellipsis, Hasher, type RenderCache, renderStatusLine } from "../tui";
33
+ import { framedBlock, renderStatusLine } from "../tui";
34
+ import { repairDoubleEncodedJsonString } from "./repair-args";
32
35
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
33
36
  import type { AgentProgress, SingleResult, TaskItem, TaskParams, TaskToolDetails } from "./types";
34
37
 
@@ -506,28 +509,20 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
506
509
  * preview. The args stream in token by token, so the array grows over time and
507
510
  * trailing entries may be partially parsed — every field access is defensive.
508
511
  */
509
- function renderTaskItemLines(
510
- tasks: TaskItem[] | undefined,
511
- contPrefix: string,
512
- expanded: boolean,
513
- theme: Theme,
514
- ): string[] {
512
+ function renderTaskItemLines(tasks: TaskItem[] | undefined, expanded: boolean, theme: Theme): string[] {
515
513
  const items = tasks ?? [];
516
514
  if (items.length === 0) return [];
517
515
 
518
- const branch = theme.fg("dim", theme.tree.branch);
519
- const last = theme.fg("dim", theme.tree.last);
516
+ const bullet = theme.fg("dim", "•");
520
517
  const cap = expanded ? items.length : Math.min(items.length, 12);
521
518
  const truncated = cap < items.length;
522
519
 
523
520
  const lines: string[] = [];
524
521
  for (let i = 0; i < cap; i++) {
525
522
  const task = items[i] as Partial<TaskItem> | undefined;
526
- const isLastLine = !truncated && i === items.length - 1;
527
- const connector = isLastLine ? last : branch;
528
523
  const rawId = task?.id?.trim();
529
524
  const idLabel = rawId ? formatTaskId(rawId) : `#${i + 1}`;
530
- let line = `${contPrefix}${connector} ${theme.fg("accent", theme.bold(idLabel))}`;
525
+ let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel))}`;
531
526
  const desc = task?.description?.trim();
532
527
  if (desc) {
533
528
  line += `: ${theme.fg("muted", truncateToWidth(replaceTabs(desc), 64))}`;
@@ -535,11 +530,42 @@ function renderTaskItemLines(
535
530
  lines.push(line);
536
531
  }
537
532
  if (truncated) {
538
- lines.push(`${contPrefix}${last} ${theme.fg("dim", formatMoreItems(items.length - cap, "agent"))}`);
533
+ lines.push(`${bullet} ${theme.fg("dim", formatMoreItems(items.length - cap, "agent"))}`);
539
534
  }
540
535
  return lines;
541
536
  }
542
537
 
538
+ /**
539
+ * Build the shared-context section (the `# Goal / # Constraints` background
540
+ * passed to every subagent). Rendered in both the streaming call preview and
541
+ * the merged result frame so the brief stays visible for the whole task
542
+ * lifecycle — not just until the first progress snapshot replaces the call view.
543
+ */
544
+ type TaskRenderSection = { lines: string[] };
545
+ type ContextSectionRenderer = (width: number) => TaskRenderSection;
546
+
547
+ // Default output-block layout is: left border + one-cell content inset + right
548
+ // border. Render markdown at that inner width so the output block does not need
549
+ // to rewrap already-rendered context lines.
550
+ const CONTEXT_FRAME_INSET = 3;
551
+
552
+ function contextMarkdownWidth(frameWidth: number): number {
553
+ return Math.max(1, frameWidth - CONTEXT_FRAME_INSET);
554
+ }
555
+
556
+ function createContextSectionRenderer(args: TaskParams | undefined, theme: Theme): ContextSectionRenderer | undefined {
557
+ // `renderResult` receives the raw tool args (unlike `renderCall`, which is
558
+ // fed through `repairTaskParams`), so undo any per-field double-encoding here
559
+ // too. The repair is idempotent on already-clean text.
560
+ const context = repairDoubleEncodedJsonString(args?.context ?? "").trim();
561
+ if (!context) return undefined;
562
+
563
+ const markdown = new Markdown(context, 0, 0, getMarkdownTheme(), {
564
+ color: text => theme.fg("muted", text),
565
+ });
566
+ return width => ({ lines: markdown.render(contextMarkdownWidth(width)) });
567
+ }
568
+
543
569
  /**
544
570
  * Render the tool call arguments.
545
571
  */
@@ -548,44 +574,34 @@ export function renderCall(
548
574
  options: RenderResultOptions & { renderContext?: { hasResult?: boolean } },
549
575
  theme: Theme,
550
576
  ): Component {
551
- const lines: string[] = [];
552
- lines.push(renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme));
553
-
554
- const context = (args.context ?? "").trim();
555
- const hasContext = context.length > 0;
556
- const branch = theme.fg("dim", theme.tree.branch);
557
- const last = theme.fg("dim", theme.tree.last);
558
- const vertical = theme.fg("dim", theme.tree.vertical);
559
577
  const showIsolated = "isolated" in args && args.isolated === true;
560
- const taskCount = args.tasks?.length ?? 0;
561
-
562
- if (hasContext) {
563
- lines.push(` ${branch} ${theme.fg("dim", "Context")}`);
564
- const contextLines = context.split("\n").map(line => {
565
- const content = line ? theme.fg("muted", replaceTabs(line)) : "";
566
- return ` ${vertical} ${content}`;
567
- });
568
- lines.push(...contextLines);
569
- }
570
-
571
- // `Tasks` is the last child unless the isolation flag follows it.
572
- const tasksIsLast = !showIsolated;
573
- const tasksPrefix = tasksIsLast ? last : branch;
574
- lines.push(` ${tasksPrefix} ${theme.fg("dim", "Tasks")} ${theme.fg("muted", `(${taskCount})`)}`);
575
- const tasksContPrefix = tasksIsLast ? " " : ` ${vertical} `;
576
- // The per-task preview list only exists to surface dispatched agents while
577
- // the call args stream in. Once a result snapshot exists, `renderResult`
578
- // draws the same agents as progress/result lines (id + description), so
579
- // emitting the preview here would render every task twice.
580
- if (!options.renderContext?.hasResult) {
581
- lines.push(...renderTaskItemLines(args.tasks, tasksContPrefix, options.expanded, theme));
582
- }
583
-
584
- if (showIsolated) {
585
- lines.push(` ${last} ${theme.fg("dim", "Isolated")}: ${theme.fg("muted", "true")}`);
586
- }
578
+ const header = renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme);
579
+ const contextSectionRenderer = createContextSectionRenderer(args, theme);
580
+ return framedBlock(theme, width => {
581
+ const sections: Array<{ label?: string; lines: string[]; separator?: boolean }> = [];
582
+
583
+ if (contextSectionRenderer) sections.push(contextSectionRenderer(width));
584
+
585
+ // The per-task preview list only exists to surface dispatched agents while
586
+ // the call args stream in. Once a result snapshot exists, `renderResult`
587
+ // draws the same agents as progress/result lines, so showing the Tasks
588
+ // section here would just repeat the count the result frame already shows.
589
+ if (!options.renderContext?.hasResult) {
590
+ sections.push({
591
+ separator: true,
592
+ lines: renderTaskItemLines(args.tasks, options.expanded, theme),
593
+ });
594
+ }
587
595
 
588
- return new Text(lines.join("\n"), 0, 0);
596
+ return {
597
+ header,
598
+ headerMeta: showIsolated ? "isolated" : undefined,
599
+ sections,
600
+ state: "pending",
601
+ borderColor: "borderMuted",
602
+ width,
603
+ };
604
+ });
589
605
  }
590
606
 
591
607
  /**
@@ -593,14 +609,13 @@ export function renderCall(
593
609
  */
594
610
  function renderAgentProgress(
595
611
  progress: AgentProgress,
596
- isLast: boolean,
612
+ prefix: string,
613
+ continuePrefix: string,
597
614
  expanded: boolean,
598
615
  theme: Theme,
599
616
  spinnerFrame?: number,
600
617
  ): string[] {
601
618
  const lines: string[] = [];
602
- const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
603
- const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
604
619
 
605
620
  const icon = getStatusIcon(progress.status, theme, spinnerFrame);
606
621
  const iconColor =
@@ -614,11 +629,23 @@ function renderAgentProgress(
614
629
  const description = progress.description?.trim();
615
630
  const displayId = formatTaskId(progress.id);
616
631
  const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
617
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
632
+ const indent = prefix ? `${prefix} ` : "";
633
+ let statusLine: string;
634
+ if (progress.status === "running") {
635
+ const bullet = theme.fg("accent", "•");
636
+ const name = theme.fg("accent", description ? theme.bold(displayId) : displayId);
637
+ statusLine = `${indent}${bullet} ${name}`;
638
+ if (description) {
639
+ const desc = shimmerEnabled() ? shimmerText(description, theme) : theme.fg("accent", description);
640
+ statusLine += `${theme.fg("accent", ":")} ${desc}`;
641
+ }
642
+ } else {
643
+ statusLine = `${indent}${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
644
+ }
618
645
 
619
646
  // Show retry-blocked badge so the parent immediately sees that a child
620
647
  // is sleeping on a provider 429, not silently progressing. Wins over the
621
- // generic running spinner because "we're waiting on a quota window" is
648
+ // generic running marker because "we're waiting on a quota window" is
622
649
  // the operationally meaningful state.
623
650
  if (progress.retryState && progress.status === "running") {
624
651
  statusLine += ` ${formatBadge("retrying", "warning", theme)}`;
@@ -867,10 +894,14 @@ function renderFindings(
867
894
  /**
868
895
  * Render final result for a single agent.
869
896
  */
870
- function renderAgentResult(result: SingleResult, isLast: boolean, expanded: boolean, theme: Theme): string[] {
897
+ function renderAgentResult(
898
+ result: SingleResult,
899
+ prefix: string,
900
+ continuePrefix: string,
901
+ expanded: boolean,
902
+ theme: Theme,
903
+ ): string[] {
871
904
  const lines: string[] = [];
872
- const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
873
- const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
874
905
 
875
906
  const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingYieldWarning(result.output);
876
907
  const aborted = result.aborted ?? false;
@@ -899,7 +930,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
899
930
  const description = result.description?.trim();
900
931
  const displayId = formatTaskId(result.id);
901
932
  const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
902
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)} ${formatBadge(
933
+ let statusLine = `${prefix ? `${prefix} ` : ""}${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)} ${formatBadge(
903
934
  statusText,
904
935
  iconColor,
905
936
  theme,
@@ -1043,101 +1074,123 @@ export function renderResult(
1043
1074
  result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails },
1044
1075
  options: RenderResultOptions,
1045
1076
  theme: Theme,
1077
+ args?: TaskParams,
1046
1078
  ): Component {
1047
1079
  const fallbackText = result.content.find(c => c.type === "text")?.text ?? "";
1048
1080
  const details = result.details;
1081
+ const contextSectionRenderer = createContextSectionRenderer(args, theme);
1049
1082
 
1050
1083
  if (!details) {
1051
1084
  const text = result.content.find(c => c.type === "text")?.text || "";
1052
- return new Text(theme.fg("dim", truncateToWidth(text, 100)), 0, 0);
1085
+ const header = renderStatusLine({ icon: "success", title: "Task" }, theme);
1086
+ return framedBlock(theme, width => ({
1087
+ header,
1088
+ sections: [
1089
+ ...(contextSectionRenderer ? [contextSectionRenderer(width)] : []),
1090
+ ...(text ? [{ separator: true, lines: [theme.fg("dim", truncateToWidth(text, width))] }] : []),
1091
+ ],
1092
+ state: "success",
1093
+ borderColor: "borderMuted",
1094
+ width,
1095
+ }));
1053
1096
  }
1054
1097
 
1055
- let cached: RenderCache | undefined;
1098
+ const hasResults = Boolean(details.results && details.results.length > 0);
1099
+ const aborted = hasResults && details.results.some(r => r.aborted);
1100
+ const failed = hasResults && details.results.some(r => !r.aborted && r.exitCode !== 0);
1101
+ const mergeFailed = hasResults && details.results.some(r => !r.aborted && r.exitCode === 0 && Boolean(r.error));
1102
+ const isError = aborted || failed;
1103
+ const agentCount = hasResults ? details.results.length : (details.progress?.length ?? 0);
1104
+ const icon: ToolUIStatus = options.isPartial ? "running" : isError ? "error" : mergeFailed ? "warning" : "success";
1105
+ const header = renderStatusLine(
1106
+ {
1107
+ icon,
1108
+ title: "Task",
1109
+ meta: agentCount > 0 ? [`${agentCount} ${agentCount === 1 ? "agent" : "agents"}`] : undefined,
1110
+ },
1111
+ theme,
1112
+ );
1056
1113
 
1057
- return {
1058
- render(width) {
1059
- const { expanded, isPartial, spinnerFrame } = options;
1060
- const key = new Hasher()
1061
- .bool(expanded)
1062
- .bool(isPartial)
1063
- .u32(spinnerFrame ?? 0)
1064
- .u32(width)
1065
- .digest();
1066
- if (cached?.key === key) return cached.lines;
1067
-
1068
- const lines: string[] = [];
1069
-
1070
- const shouldRenderProgress =
1071
- Boolean(details.progress && details.progress.length > 0) && (isPartial || details.results.length === 0);
1072
- if (shouldRenderProgress && details.progress) {
1073
- details.progress.forEach((progress, i) => {
1074
- const isLast = i === details.progress!.length - 1;
1075
- lines.push(...renderAgentProgress(progress, isLast, expanded, theme, spinnerFrame));
1076
- });
1077
- } else if (details.results && details.results.length > 0) {
1078
- details.results.forEach((res, i) => {
1079
- const isLast = i === details.results.length - 1;
1080
- lines.push(...renderAgentResult(res, isLast, expanded, theme));
1081
- });
1082
-
1083
- const abortedCount = details.results.filter(r => r.aborted).length;
1084
- const mergeFailedCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && r.error).length;
1085
- const successCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && !r.error).length;
1086
- const failCount = details.results.length - successCount - mergeFailedCount - abortedCount;
1087
- let summary = `${theme.fg("dim", "Total:")} `;
1088
- if (abortedCount > 0) {
1089
- summary += theme.fg("error", `${abortedCount} aborted`);
1090
- if (successCount > 0 || mergeFailedCount > 0 || failCount > 0) summary += theme.sep.dot;
1091
- }
1092
- if (successCount > 0) {
1093
- summary += theme.fg("success", `${successCount} succeeded`);
1094
- if (mergeFailedCount > 0 || failCount > 0) summary += theme.sep.dot;
1095
- }
1096
- if (mergeFailedCount > 0) {
1097
- summary += theme.fg("warning", `${mergeFailedCount} merge failed`);
1098
- if (failCount > 0) summary += theme.sep.dot;
1099
- }
1100
- if (failCount > 0) {
1101
- summary += theme.fg("error", `${failCount} failed`);
1102
- }
1103
- summary += `${theme.sep.dot}${theme.fg("dim", formatDuration(details.totalDurationMs))}`;
1104
- lines.push(summary);
1105
- }
1114
+ return framedBlock(theme, width => {
1115
+ const { expanded, isPartial, spinnerFrame } = options;
1116
+ const lines: string[] = [];
1106
1117
 
1107
- if (lines.length === 0) {
1108
- const text = fallbackText.trim() ? fallbackText : "No results";
1109
- const result = [theme.fg("dim", truncateToWidth(text, width))];
1110
- cached = { key, lines: result };
1111
- return result;
1112
- }
1118
+ const shouldRenderProgress =
1119
+ Boolean(details.progress && details.progress.length > 0) && (isPartial || details.results.length === 0);
1120
+ if (shouldRenderProgress && details.progress) {
1121
+ details.progress.forEach(progress => {
1122
+ lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame));
1123
+ });
1124
+ } else if (details.results && details.results.length > 0) {
1125
+ details.results.forEach(res => {
1126
+ lines.push(...renderAgentResult(res, "", " ", expanded, theme));
1127
+ });
1113
1128
 
1114
- if (fallbackText.trim()) {
1115
- const summaryLines = fallbackText.split("\n");
1116
- const markerIndex = summaryLines.findIndex(
1117
- line =>
1118
- line.includes("<system-notification>") ||
1119
- line.startsWith("Applied patches:") ||
1120
- line.startsWith("No changes to apply."),
1121
- );
1122
- if (markerIndex >= 0) {
1123
- const extra = summaryLines.slice(markerIndex);
1124
- for (const line of extra) {
1125
- if (!line.trim()) continue;
1126
- lines.push(theme.fg("dim", line));
1127
- }
1129
+ const abortedCount = details.results.filter(r => r.aborted).length;
1130
+ const mergeFailedCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && r.error).length;
1131
+ const successCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && !r.error).length;
1132
+ const failCount = details.results.length - successCount - mergeFailedCount - abortedCount;
1133
+ const summaryParts: string[] = [];
1134
+ if (abortedCount > 0) summaryParts.push(theme.fg("error", `${abortedCount} aborted`));
1135
+ if (successCount > 0) summaryParts.push(theme.fg("success", `${successCount} succeeded`));
1136
+ if (mergeFailedCount > 0) summaryParts.push(theme.fg("warning", `${mergeFailedCount} merge failed`));
1137
+ if (failCount > 0) summaryParts.push(theme.fg("error", `${failCount} failed`));
1138
+ summaryParts.push(theme.fg("dim", formatDuration(details.totalDurationMs)));
1139
+ // Wrap the run summary in the theme's bracket glyphs (dim chrome, colored
1140
+ // counts) to match the bash tool's `[Wall: … | Exit: …]` footer.
1141
+ lines.push(
1142
+ theme.fg("dim", theme.format.bracketLeft) +
1143
+ summaryParts.join(theme.fg("dim", theme.sep.dot)) +
1144
+ theme.fg("dim", theme.format.bracketRight),
1145
+ );
1146
+ }
1147
+
1148
+ const state = isPartial ? "running" : isError ? "error" : mergeFailed ? "warning" : "success";
1149
+ const borderColor = isError ? "error" : "borderMuted";
1150
+
1151
+ if (lines.length === 0) {
1152
+ const text = fallbackText.trim() ? fallbackText : "No results";
1153
+ return {
1154
+ header,
1155
+ sections: [
1156
+ ...(contextSectionRenderer ? [contextSectionRenderer(width)] : []),
1157
+ { separator: true, lines: [theme.fg("dim", truncateToWidth(text, width))] },
1158
+ ],
1159
+ state,
1160
+ borderColor,
1161
+ width,
1162
+ };
1163
+ }
1164
+
1165
+ if (fallbackText.trim()) {
1166
+ const summaryLines = fallbackText.split("\n");
1167
+ const markerIndex = summaryLines.findIndex(
1168
+ line =>
1169
+ line.includes("<system-notification>") ||
1170
+ line.startsWith("Applied patches:") ||
1171
+ line.startsWith("No changes to apply."),
1172
+ );
1173
+ if (markerIndex >= 0) {
1174
+ const extra = summaryLines.slice(markerIndex);
1175
+ for (const line of extra) {
1176
+ if (!line.trim()) continue;
1177
+ lines.push(theme.fg("dim", line));
1128
1178
  }
1129
1179
  }
1180
+ }
1130
1181
 
1131
- const indented = lines.map(line =>
1132
- line.length > 0 ? truncateToWidth(` ${line}`, width, Ellipsis.Omit) : "",
1133
- );
1134
- cached = { key, lines: indented };
1135
- return indented;
1136
- },
1137
- invalidate() {
1138
- cached = undefined;
1139
- },
1140
- };
1182
+ while (lines.length > 0 && lines[0].trim() === "") lines.shift();
1183
+ return {
1184
+ header,
1185
+ sections: [
1186
+ ...(contextSectionRenderer ? [contextSectionRenderer(width)] : []),
1187
+ ...(lines.length > 0 ? [{ separator: true, lines }] : []),
1188
+ ],
1189
+ state,
1190
+ borderColor,
1191
+ width,
1192
+ };
1193
+ });
1141
1194
  }
1142
1195
 
1143
1196
  function isTaskToolDetails(value: unknown): value is TaskToolDetails {
@@ -1149,13 +1202,23 @@ function isTaskToolDetails(value: unknown): value is TaskToolDetails {
1149
1202
  );
1150
1203
  }
1151
1204
 
1205
+ // Nested subagent snapshots sit one or more levels below the frame border, so
1206
+ // they keep tree guides to convey depth (the parent prepends its own continue
1207
+ // prefix). Only the top-level agent list drops guides (the frame is its box).
1208
+ function nestedMarkers(isLast: boolean, theme: Theme): { prefix: string; continuePrefix: string } {
1209
+ return {
1210
+ prefix: isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch),
1211
+ continuePrefix: isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `,
1212
+ };
1213
+ }
1214
+
1152
1215
  function renderNestedTaskResults(detailsList: TaskToolDetails[], expanded: boolean, theme: Theme): string[] {
1153
1216
  const lines: string[] = [];
1154
1217
  for (const details of detailsList) {
1155
1218
  if (!details.results || details.results.length === 0) continue;
1156
1219
  details.results.forEach((result, index) => {
1157
- const isLast = index === details.results.length - 1;
1158
- lines.push(...renderAgentResult(result, isLast, expanded, theme));
1220
+ const { prefix, continuePrefix } = nestedMarkers(index === details.results.length - 1, theme);
1221
+ lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
1159
1222
  });
1160
1223
  }
1161
1224
  return lines;
@@ -1177,16 +1240,16 @@ function renderNestedTaskTree(
1177
1240
  const hasResults = Boolean(details.results && details.results.length > 0);
1178
1241
  if (hasResults) {
1179
1242
  details.results.forEach((result, index) => {
1180
- const isLast = index === details.results.length - 1;
1181
- lines.push(...renderAgentResult(result, isLast, expanded, theme));
1243
+ const { prefix, continuePrefix } = nestedMarkers(index === details.results.length - 1, theme);
1244
+ lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
1182
1245
  });
1183
1246
  continue;
1184
1247
  }
1185
1248
  const inflight = details.progress;
1186
1249
  if (inflight && inflight.length > 0) {
1187
1250
  inflight.forEach((prog, index) => {
1188
- const isLast = index === inflight.length - 1;
1189
- lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame));
1251
+ const { prefix, continuePrefix } = nestedMarkers(index === inflight.length - 1, theme);
1252
+ lines.push(...renderAgentProgress(prog, prefix, continuePrefix, expanded, theme, spinnerFrame));
1190
1253
  });
1191
1254
  }
1192
1255
  }
@@ -1207,4 +1270,5 @@ subprocessToolRegistry.register<TaskToolDetails>("task", {
1207
1270
  export const taskToolRenderer = {
1208
1271
  renderCall,
1209
1272
  renderResult,
1273
+ mergeCallAndResult: true,
1210
1274
  };
@@ -1,5 +1,9 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
1
4
  import { inflateSync, strFromU8 } from "fflate";
2
5
 
6
+ import { formatBytes } from "./render-utils";
3
7
  import { ToolError } from "./tool-errors";
4
8
 
5
9
  export type ArchiveFormat = "zip" | "tar" | "tar.gz";
@@ -123,11 +127,21 @@ function getArchiveFormatFromPath(filePath: string): ArchiveFormat | undefined {
123
127
  return undefined;
124
128
  }
125
129
 
130
+ export function formatArchiveEntryLines(entries: readonly ArchiveDirectoryEntry[]): string[] {
131
+ return entries.map(entry => {
132
+ if (entry.isDirectory) return `${entry.name}/`;
133
+
134
+ const sizeSuffix = entry.size > 0 ? ` (${formatBytes(entry.size)})` : "";
135
+ return `${entry.name}${sizeSuffix}`;
136
+ });
137
+ }
138
+
126
139
  const ZIP_LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
127
140
  const ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x02014b50;
128
141
  const ZIP64_EOCD_SIGNATURE = 0x06064b50;
129
142
  const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
130
143
  const ZIP_EOCD_SIGNATURE = 0x06054b50;
144
+ const ZIP_DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
131
145
  const ZIP_EOCD_MIN_LENGTH = 22;
132
146
  const ZIP_EOCD_MAX_COMMENT_LENGTH = 0xffff;
133
147
  const ZIP64_EOCD_LOCATOR_LENGTH = 20;
@@ -167,6 +181,37 @@ function readUInt32LE(bytes: Uint8Array, offset: number): number {
167
181
  return (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;
168
182
  }
169
183
 
184
+ function bytesMatchAscii(bytes: Uint8Array, offset: number, value: string): boolean {
185
+ if (bytes.byteLength < offset + value.length) return false;
186
+ for (let index = 0; index < value.length; index++) {
187
+ if (bytes[offset + index] !== value.charCodeAt(index)) return false;
188
+ }
189
+ return true;
190
+ }
191
+
192
+ export function sniffArchiveFormat(bytes: Uint8Array): ArchiveFormat | undefined {
193
+ if (bytes.byteLength >= 4) {
194
+ const signature = readUInt32LE(bytes, 0);
195
+ if (
196
+ signature === ZIP_LOCAL_FILE_HEADER_SIGNATURE ||
197
+ signature === ZIP_EOCD_SIGNATURE ||
198
+ signature === ZIP_DATA_DESCRIPTOR_SIGNATURE
199
+ ) {
200
+ return "zip";
201
+ }
202
+ }
203
+
204
+ if (bytes.byteLength >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) {
205
+ return "tar.gz";
206
+ }
207
+
208
+ if (bytesMatchAscii(bytes, 257, "ustar")) {
209
+ return "tar";
210
+ }
211
+
212
+ return undefined;
213
+ }
214
+
170
215
  function readUInt64LEAsNumber(bytes: Uint8Array, offset: number): number {
171
216
  const value = readUInt32LE(bytes, offset) + readUInt32LE(bytes, offset + 4) * ZIP_UINT32_RANGE;
172
217
  if (!Number.isSafeInteger(value)) {
@@ -627,3 +672,22 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
627
672
  format === "zip" ? await readZipEntries(filePath) : await readTarEntries(await Bun.file(filePath).bytes());
628
673
  return new ArchiveReader(format, entries);
629
674
  }
675
+
676
+ export async function listArchiveRoot(
677
+ bytes: Uint8Array,
678
+ format: ArchiveFormat,
679
+ opts: { limit?: number } = {},
680
+ ): Promise<string> {
681
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-archive-"));
682
+ const tempPath = path.join(tempDir, `payload.${format}`);
683
+ try {
684
+ await Bun.write(tempPath, bytes);
685
+ const archive = await openArchive(tempPath);
686
+ const entries = archive.listDirectory("");
687
+ const limitedEntries = opts.limit !== undefined && opts.limit > 0 ? entries.slice(0, opts.limit) : entries;
688
+ const lines = formatArchiveEntryLines(limitedEntries);
689
+ return lines.length > 0 ? lines.join("\n") : "(empty archive directory)";
690
+ } finally {
691
+ await fs.rm(tempDir, { recursive: true, force: true });
692
+ }
693
+ }