mixdog 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,10 +1,29 @@
1
+ import { displayModelName as sharedDisplayModelName } from '../../ui/model-display.mjs';
2
+
1
3
  const DEFAULT_SUMMARY_MAX = 160;
4
+ // Semantic cap for collapsed agent/task card one-liners (spawn, send, response).
5
+ export const AGENT_SURFACE_BRIEF_MAX = 120;
2
6
  const STATUS_SEPARATOR = ' · ';
3
7
 
4
8
  export function stripToolPrefix(name) {
5
- return String(name || 'tool')
6
- .replace(/^mcp__.*__/, '')
7
- .replace(/^functions\./, '');
9
+ const text = rawToolName(name);
10
+ const mcp = parseMcpToolName(text);
11
+ return mcp ? mcp.tool : text;
12
+ }
13
+
14
+ function rawToolName(name) {
15
+ return String(name || 'tool').replace(/^functions\./, '');
16
+ }
17
+
18
+ export function parseMcpToolName(name) {
19
+ const text = rawToolName(name);
20
+ const match = /^mcp__(.+?)__(.+)$/.exec(text);
21
+ if (!match) return null;
22
+ return { server: match[1], tool: match[2] };
23
+ }
24
+
25
+ export function isMcpToolName(name) {
26
+ return Boolean(parseMcpToolName(name));
8
27
  }
9
28
 
10
29
  export function normalizeToolName(name) {
@@ -61,6 +80,12 @@ function compactSlash(left, right) {
61
80
  return a && b ? `${a}/${b}` : a || b;
62
81
  }
63
82
 
83
+ function mcpToolTarget(name, max = DEFAULT_SUMMARY_MAX) {
84
+ const mcp = parseMcpToolName(name);
85
+ if (!mcp) return '';
86
+ return truncateToolText(compactSlash(mcp.server, mcp.tool), max);
87
+ }
88
+
64
89
  function quoted(value, max) {
65
90
  const text = truncateToolText(value || '', max);
66
91
  return text ? `"${text}"` : '';
@@ -73,6 +98,52 @@ function firstText(...values) {
73
98
  return '';
74
99
  }
75
100
 
101
+ function splitToolSearchSelection(value) {
102
+ if (Array.isArray(value)) return value.map((item) => String(item || '').trim()).filter(Boolean);
103
+ return String(value || '')
104
+ .split(/[,\s]+/)
105
+ .map((item) => item.trim())
106
+ .filter(Boolean);
107
+ }
108
+
109
+ function toolSearchTargetKind(value) {
110
+ const lower = String(value || '').trim().toLowerCase();
111
+ if (!lower) return '';
112
+ if (lower.startsWith('mcp__') || lower.includes('_mcp_') || lower.includes('mcp')) return 'MCP';
113
+ if (lower === 'skill' || lower.startsWith('skill:') || lower.startsWith('skill_') || lower.startsWith('skills_') || lower.includes('skill')) return 'Skills';
114
+ return 'Tools';
115
+ }
116
+
117
+ function orderedToolSearchKinds(kinds) {
118
+ const set = new Set((kinds || []).filter(Boolean));
119
+ const order = ['Tools', 'MCP', 'Skills'];
120
+ const out = order.filter((kind) => set.has(kind));
121
+ return out.length ? out : ['Tools'];
122
+ }
123
+
124
+ function inferToolSearchKinds(args = {}) {
125
+ const selected = splitToolSearchSelection(args.select);
126
+ if (selected.length) return orderedToolSearchKinds(selected.map(toolSearchTargetKind));
127
+
128
+ const query = firstText(args.query, args.q, args.text).toLowerCase();
129
+ const kinds = [];
130
+ if (/\bmcp\b|mcp__|mcp[-_\s]?server/.test(query)) kinds.push('MCP');
131
+ if (/\bskills?\b|skill_|use[-_\s]?skill/.test(query)) kinds.push('Skills');
132
+ return orderedToolSearchKinds(kinds);
133
+ }
134
+
135
+ function toolSearchDisplayLabel(args = {}) {
136
+ return `Load ${inferToolSearchKinds(args).join('/')}`;
137
+ }
138
+
139
+ function displayToolSearchTarget(value) {
140
+ const text = String(value || '').trim();
141
+ if (/^skill:/i.test(text)) return text.slice('skill:'.length);
142
+ const mcp = /^mcp__(.*?)__(.+)$/.exec(text);
143
+ if (mcp) return `${mcp[1]}.${mcp[2]}`;
144
+ return stripToolPrefix(text);
145
+ }
146
+
76
147
  function titleizeToolName(name) {
77
148
  return stripToolPrefix(name)
78
149
  .split(/[_\s-]+/)
@@ -89,7 +160,6 @@ function titleizeToolName(name) {
89
160
 
90
161
  const AGENT_DISPLAY_NAMES = new Map([
91
162
  ['explore', 'Explore'],
92
- ['web-researcher', 'Web Researcher'],
93
163
  ['maintainer', 'Maintainer'],
94
164
  ['worker', 'Worker'],
95
165
  ['heavy-worker', 'Heavy Worker'],
@@ -121,41 +191,22 @@ function displayAgentName(value) {
121
191
  return AGENT_DISPLAY_NAMES.get(key) || titleizeDisplayName(text);
122
192
  }
123
193
 
124
- function displayModelName(model) {
125
- const text = String(model || '').trim();
126
- if (!text) return '';
127
- const raw = text.includes('/') ? (text.split('/').filter(Boolean).at(-1) || text) : text;
128
- const lower = raw.toLowerCase();
129
- const newClaude = /^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?/i.exec(lower);
130
- if (newClaude) {
131
- const family = titleizeDisplayName(newClaude[1]);
132
- const minor = newClaude[3] && newClaude[3].length <= 2 ? `.${newClaude[3]}` : '';
133
- return `${family} ${newClaude[2]}${minor}`;
134
- }
135
- const oldClaude = /^claude-(\d+)(?:-(\d+))?-(opus|sonnet|haiku)(?:-|$)/i.exec(lower);
136
- if (oldClaude) {
137
- const family = titleizeDisplayName(oldClaude[3]);
138
- return `${family} ${oldClaude[1]}${oldClaude[2] ? `.${oldClaude[2]}` : ''}`;
139
- }
140
- if (lower.startsWith('gpt-')) {
141
- return raw
142
- .split('-')
143
- .map((part, index) => (index === 0 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1)))
144
- .join('-');
145
- }
146
- if (lower.startsWith('grok-')) {
147
- return raw
148
- .split('-')
149
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
150
- .join(' ');
151
- }
152
- return raw;
194
+ export function displayModelName(model, provider, displayHint) {
195
+ const text = String(model ?? '').trim();
196
+ const modelId = text
197
+ ? (text.includes('/') ? (text.split('/').filter(Boolean).at(-1) || text) : text)
198
+ : '';
199
+ const shown = sharedDisplayModelName(modelId, provider, displayHint);
200
+ return shown || modelId;
153
201
  }
154
202
 
155
203
  function bridgeAgentModelSummary(args) {
204
+ const provider = firstText(args.provider, args.providerId, args.provider_id);
205
+ const modelId = firstText(args.model);
206
+ const displayHint = firstText(args.modelDisplay, args.model_display, args.displayModel);
156
207
  return compactParts([
157
208
  displayAgentName(firstText(args.agent, args.role, args.name, args.subagent_type)),
158
- displayModelName(firstText(args.modelDisplay, args.model_display, args.displayModel, args.model)),
209
+ displayModelName(modelId, provider, displayHint),
159
210
  ]);
160
211
  }
161
212
 
@@ -186,6 +237,36 @@ function summarizePatch(patch, basePath) {
186
237
  return text ? 'patch' : '';
187
238
  }
188
239
 
240
+ function collectionCount(...values) {
241
+ for (const value of values) {
242
+ if (Array.isArray(value)) {
243
+ const count = value.filter((item) => item != null && String(item).trim()).length;
244
+ return count || value.length || 0;
245
+ }
246
+ if (value && typeof value === 'object') return 1;
247
+ if (value != null && String(value).trim()) return 1;
248
+ }
249
+ return 0;
250
+ }
251
+
252
+ function formatCountedUnit(count, singular, pluralText = `${singular}s`) {
253
+ const n = Math.max(0, Number(count || 0));
254
+ return n > 0 ? `${n} ${pluralize(n, singular, pluralText)}` : '';
255
+ }
256
+
257
+ function patchFileCount(args = {}) {
258
+ const patchText = String(args.patch ?? '');
259
+ if (patchText) {
260
+ const files = new Set();
261
+ for (const line of patchText.split('\n')) {
262
+ const match = /^\*\*\*\s+(?:Update|Add|Delete) File:\s+(.+)\s*$/.exec(line);
263
+ if (match) files.add(match[1].trim());
264
+ }
265
+ if (files.size > 0) return files.size;
266
+ }
267
+ return collectionCount(args.path, args.file, args.file_path, args.base_path);
268
+ }
269
+
189
270
  function codeGraphLabel(args) {
190
271
  const mode = String(args.mode || args.action || '').toLowerCase();
191
272
  if (mode === 'prewarm' || mode === 'index' || mode === 'build' || mode === 'refresh') return 'Setup';
@@ -201,6 +282,7 @@ function codeGraphSummary(args, max) {
201
282
  }
202
283
 
203
284
  export function displayToolName(name, args = {}) {
285
+ if (isMcpToolName(name)) return 'MCP';
204
286
  const normalized = normalizeToolName(name);
205
287
  switch (normalized) {
206
288
  case 'read':
@@ -209,24 +291,32 @@ export function displayToolName(name, args = {}) {
209
291
  return 'Read';
210
292
  case 'apply_patch': {
211
293
  const parsed = parseToolArgs(args);
294
+ if (parsed && parsed.dry_run === true) return 'Check';
212
295
  return parsed && parsed.old_string === '' ? 'Create' : 'Update';
213
296
  }
214
297
  case 'shell':
298
+ case 'bash':
215
299
  case 'bash_session':
216
300
  case 'shell_command':
217
- case 'task':
218
- case 'trigger_schedule':
301
+ case 'job_wait':
219
302
  return 'Run';
303
+ case 'task':
304
+ return 'Task';
220
305
  case 'grep':
306
+ case 'find':
221
307
  case 'glob':
222
- case 'tool_search':
308
+ case 'list':
309
+ case 'ls':
223
310
  return 'Search';
311
+ case 'trigger_schedule':
312
+ return 'Schedule';
313
+ case 'tool_search':
314
+ return toolSearchDisplayLabel(parseToolArgs(args));
224
315
  case 'search':
225
316
  case 'search_query':
226
317
  case 'image_query':
227
318
  case 'web_search':
228
319
  case 'web_search_call':
229
- case 'firecrawl_search':
230
320
  return 'Web Search';
231
321
  case 'explore':
232
322
  return 'Explore';
@@ -282,9 +372,20 @@ export function displayToolName(name, args = {}) {
282
372
  export function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}) {
283
373
  const a = parseToolArgs(args);
284
374
  if (!a || typeof a !== 'object') return '';
285
- switch (normalizeToolName(name)) {
375
+ const normalized = normalizeToolName(name);
376
+ const mcpTarget = mcpToolTarget(name, max);
377
+ if (mcpTarget) {
378
+ return compactParts([
379
+ mcpTarget,
380
+ truncateToolText(firstText(a.query, a.q, a.text, a.prompt, a.path, a.uri, a.name, a.id, a.action), Math.min(max, 80)),
381
+ ]);
382
+ }
383
+ switch (normalized) {
286
384
  case 'read':
287
385
  if (!a.path && !a.file_path) return '';
386
+ if (Array.isArray(a.path) || Array.isArray(a.file_path)) {
387
+ return formatCountedUnit(collectionCount(a.path, a.file_path), 'file');
388
+ }
288
389
  return compactParts([
289
390
  displayToolPath(a.path ?? a.file_path),
290
391
  a.pages ? `pages ${a.pages}` : summarizeLineWindow(a),
@@ -294,19 +395,27 @@ export function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}
294
395
  case 'apply_patch':
295
396
  return summarizePatch(a.patch, a.base_path);
296
397
  case 'shell':
398
+ case 'bash':
297
399
  case 'bash_session':
298
400
  case 'shell_command':
401
+ case 'job_wait':
299
402
  return truncateCommand(a.description || a.command || a.cmd || '', max);
300
403
  case 'task':
301
404
  return compactParts([a.action || a.type || 'task', a.task_id || '']);
302
405
  case 'list':
303
406
  case 'ls':
407
+ if (Array.isArray(a.path) || Array.isArray(a.dir) || Array.isArray(a.cwd)) {
408
+ return formatCountedUnit(collectionCount(a.path, a.dir, a.cwd), 'directory', 'directories');
409
+ }
304
410
  return compactParts([
305
411
  displayToolPath(a.path ?? a.dir ?? a.cwd ?? ''),
306
412
  a.head_limit || a.limit ? `${a.head_limit ?? a.limit} entries` : '',
307
413
  ]);
308
414
  case 'grep':
309
415
  if (!a.pattern && !a.query) return '';
416
+ if (Array.isArray(a.pattern) || Array.isArray(a.query)) {
417
+ return formatCountedUnit(collectionCount(a.pattern, a.query), 'pattern');
418
+ }
310
419
  return compactParts([
311
420
  `pattern: ${quoted(a.pattern ?? a.query, max)}`,
312
421
  a.path ? `path: ${displayToolPath(a.path)}` : '',
@@ -314,23 +423,47 @@ export function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}
314
423
  ]);
315
424
  case 'glob':
316
425
  if (!a.pattern && !a.glob) return '';
426
+ if (Array.isArray(a.pattern) || Array.isArray(a.glob)) {
427
+ return formatCountedUnit(collectionCount(a.pattern, a.glob), 'glob');
428
+ }
317
429
  return compactParts([
318
430
  `pattern: ${quoted(a.pattern ?? a.glob, max)}`,
319
431
  a.path ? `path: ${displayToolPath(a.path)}` : '',
320
432
  ]);
433
+ case 'find':
434
+ if (!a.query && !a.fuzzy) return '';
435
+ if (Array.isArray(a.query) || Array.isArray(a.fuzzy)) {
436
+ return formatCountedUnit(collectionCount(a.query, a.fuzzy), 'query', 'queries');
437
+ }
438
+ return compactParts([
439
+ quoted(a.query ?? a.fuzzy, max),
440
+ a.path ? `path: ${displayToolPath(a.path)}` : '',
441
+ ]);
321
442
  case 'search':
322
443
  case 'search_query':
323
444
  case 'image_query':
324
445
  case 'web_search':
325
446
  case 'web_search_call':
326
- case 'firecrawl_search':
447
+ if (Array.isArray(a.query) || Array.isArray(a.keywords)) {
448
+ return formatCountedUnit(collectionCount(a.query, a.keywords), 'query', 'queries');
449
+ }
327
450
  return quoted(a.query || a.keywords || '', max);
328
451
  case 'explore':
452
+ if (Array.isArray(a.query) || Array.isArray(a.prompt) || Array.isArray(a.task) || Array.isArray(a.goal)) {
453
+ return formatCountedUnit(collectionCount(a.query, a.prompt, a.task, a.goal), 'query', 'queries');
454
+ }
329
455
  return truncateSingleLine(firstText(a.query, a.prompt, a.task, a.goal, a.path), Math.min(max, 80));
330
456
  case 'tool_search':
331
- return quoted(firstText(a.query, a.q, a.text), max);
457
+ {
458
+ const selected = splitToolSearchSelection(a.select);
459
+ if (selected.length) return truncateToolText(selected.map(displayToolSearchTarget).join(', '), max);
460
+ return quoted(firstText(a.query, a.q, a.text), max);
461
+ }
332
462
  case 'web_fetch':
333
463
  case 'fetch':
464
+ if (Array.isArray(a.url) || Array.isArray(a.uri)) {
465
+ return formatCountedUnit(collectionCount(a.url, a.uri), 'URL', 'URLs');
466
+ }
334
467
  return truncateToolText(a.url || a.uri || '', max);
335
468
  case 'download_attachment':
336
469
  return displayToolPath(a.filename || a.name || a.url || '');
@@ -396,7 +529,7 @@ export function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}
396
529
  case 'skill_view':
397
530
  case 'skills_list':
398
531
  case 'use_skill':
399
- return truncateToolText(firstText(a.name, a.skill, a.skill_name), max);
532
+ return truncateToolText(firstText(a.name, a.skill, a.skill_name, a.query, a.q, normalized === 'skills_list' ? 'all skills' : ''), max);
400
533
  default: {
401
534
  const primary = firstText(a.name, a.skill, a.query, a.title, a.path, a.file, a.target, a.id, a.action);
402
535
  if (primary) return truncateToolText(primary, Math.min(max, 80));
@@ -477,10 +610,13 @@ function parseLineDelta(delta) {
477
610
 
478
611
  function formatLineDelta(totals) {
479
612
  if (!totals?.seen) return '';
613
+ const added = Number(totals.added) || 0;
614
+ const removed = Number(totals.removed) || 0;
615
+ if (added === 0 && removed === 0) return '';
480
616
  const parts = [];
481
- if (totals.added) parts.push(`+${totals.added} Lines`);
482
- if (totals.removed) parts.push(`-${totals.removed} Lines`);
483
- return parts.join(STATUS_SEPARATOR) || '±0 Lines';
617
+ if (added > 0) parts.push(`+${added} ${pluralize(added, 'line')}`);
618
+ if (removed > 0) parts.push(`-${removed} ${pluralize(removed, 'line')}`);
619
+ return parts.join(STATUS_SEPARATOR);
484
620
  }
485
621
 
486
622
  function parseUpdateSummary(text) {
@@ -510,6 +646,10 @@ function looksLineOriented(text) {
510
646
  }
511
647
 
512
648
  function summarizeUpdateResult(text, args) {
649
+ // A dry_run patch validates without writing, so the collapsed detail must not
650
+ // claim a real mutation ("Updated/Created/Deleted foo.js"). Map every action
651
+ // to "Checked" wording, matching the dry-run header (Checking/Checked).
652
+ const isDryRun = parseToolArgs(args)?.dry_run === true;
513
653
  const changed = [];
514
654
  for (const line of String(text ?? '').split('\n')) {
515
655
  const ok = /^\s*OK\s+(modify|add|delete|create)\s+(.+?)\s*$/i.exec(line);
@@ -526,8 +666,10 @@ function summarizeUpdateResult(text, args) {
526
666
  }
527
667
  if (changed.length === 1) {
528
668
  const item = changed[0];
529
- const action = item.action === 'delete' ? 'Deleted' : item.action === 'add' || item.action === 'create' || item.action === 'created' ? 'Created' : 'Updated';
530
- return compactParts([`${action} ${displayToolPath(item.path)}`, item.delta]);
669
+ const action = isDryRun
670
+ ? 'Checked'
671
+ : item.action === 'delete' ? 'Deleted' : item.action === 'add' || item.action === 'create' || item.action === 'created' ? 'Created' : 'Updated';
672
+ return compactParts([`${action} ${displayToolPath(item.path)}`, formatLineDelta(parseLineDelta(item.delta))]);
531
673
  }
532
674
  if (changed.length > 1) {
533
675
  const totals = changed.reduce((acc, item) => {
@@ -537,12 +679,12 @@ function summarizeUpdateResult(text, args) {
537
679
  acc.seen = acc.seen || delta.seen;
538
680
  return acc;
539
681
  }, { added: 0, removed: 0, seen: false });
540
- return compactParts([`Updated ${changed.length} Files`, formatLineDelta(totals)]);
682
+ return compactParts([`${isDryRun ? 'Checked' : 'Updated'} ${changed.length} Files`, formatLineDelta(totals)]);
541
683
  }
542
684
 
543
685
  const parsedArgs = parseToolArgs(args);
544
686
  const target = parsedArgs.path ?? parsedArgs.file ?? parsedArgs.file_path ?? '';
545
- if (target) return `Updated ${displayToolPath(target)}`;
687
+ if (target) return `${isDryRun ? 'Checked' : 'Updated'} ${displayToolPath(target)}`;
546
688
  return null;
547
689
  }
548
690
 
@@ -552,21 +694,66 @@ function textBetweenTag(text, tag) {
552
694
  return match ? match[1].trim() : '';
553
695
  }
554
696
 
697
+ // Strip the most common inline-markdown markers so a one-line card summary
698
+ // reads as plain prose ("**not clean**" → "not clean", "`x`" → "x"). Block
699
+ // markers (#, >, -, 1.) at line start are dropped too. Whitespace is collapsed
700
+ // by the caller's truncateSingleLine.
701
+ function stripInlineMarkdown(value) {
702
+ return String(value ?? '')
703
+ .replace(/^\s{0,3}(?:#{1,6}\s+|>\s+|[-*+]\s+|\d+\.\s+)/, '')
704
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
705
+ .replace(/__([^_]+)__/g, '$1')
706
+ .replace(/(?<!\*)\*(?!\*)([^*]+)\*(?!\*)/g, '$1')
707
+ .replace(/`([^`]+)`/g, '$1')
708
+ .trim();
709
+ }
710
+
555
711
  function firstAgentResultLine(text) {
556
712
  const finalAnswer = textBetweenTag(text, 'final-answer') || textBetweenTag(text, 'result');
557
713
  const raw = finalAnswer || text;
558
714
  for (const line of String(raw ?? '').split('\n')) {
559
715
  const trimmed = line.trim();
560
716
  if (!trimmed) continue;
561
- if (/^bridge result\b/i.test(trimmed)) continue;
717
+ if (/^agent result\b/i.test(trimmed)) continue;
562
718
  if (/^<\/?(?:final-answer|task-notification|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>$/i.test(trimmed)) continue;
563
- if (/^(?:bridge job|bridge task|status|type|target|role|agent|preset|model|effort|fast|limits|session|job|task-id|task_id|notification|queueDepth):\s*/i.test(trimmed)) continue;
719
+ if (/^(?:agent task|status|type|target|role|agent|preset|model|effort|fast|limits|session|task-id|task_id|notification|queueDepth):\s*/i.test(trimmed)) continue;
564
720
  if (/^\[[a-z-]+:\s*[^\]]*\]$/i.test(trimmed)) continue;
565
- return truncateSingleLine(trimmed, 120);
721
+ return truncateSingleLine(trimmed, AGENT_SURFACE_BRIEF_MAX);
566
722
  }
567
723
  return '';
568
724
  }
569
725
 
726
+ function summarizeGenericResult(text) {
727
+ const trimmed = String(text ?? '').trim();
728
+ if (!trimmed) return null;
729
+ if (/^(?:undefined|null)$/i.test(trimmed)) return null;
730
+
731
+ if (/^[\[{]/.test(trimmed)) {
732
+ try {
733
+ const parsed = JSON.parse(trimmed);
734
+ if (Array.isArray(parsed)) return `${parsed.length} ${pluralize(parsed.length, 'item')}`;
735
+ if (parsed && typeof parsed === 'object') {
736
+ const status = firstText(parsed.status, parsed.state, parsed.result, parsed.message);
737
+ if (status) return truncateSingleLine(titleStatus(status), 120);
738
+ if (parsed.cwd) return truncateSingleLine(parsed.cwd, 120);
739
+ if (typeof parsed.ok === 'boolean') return parsed.ok ? 'Ok' : 'Failed';
740
+ for (const key of ['items', 'results', 'resources', 'templates', 'providers', 'schedules', 'channels', 'tools']) {
741
+ if (Array.isArray(parsed[key])) return `${parsed[key].length} ${pluralize(parsed[key].length, (key.slice(0, -1) || 'item').toLowerCase())}`;
742
+ }
743
+ }
744
+ } catch {
745
+ return null;
746
+ }
747
+ }
748
+
749
+ const line = firstAgentResultLine(text) || trimmed.split('\n').map((item) => item.trim()).find(Boolean) || '';
750
+ if (!line || line === '{' || line === '[') return null;
751
+ if (/^(ok|done|success|saved|sent|updated|reloaded|connected|enabled|disabled|active|inactive)$/i.test(line)) {
752
+ return titleStatus(line);
753
+ }
754
+ return truncateSingleLine(line, 120);
755
+ }
756
+
570
757
  /**
571
758
  * Derive a short semantic one-liner for a completed tool call using only the
572
759
  * tool name, parsed args, and the raw result text. Returns null when nothing
@@ -576,6 +763,8 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
576
763
  if (isError) return null;
577
764
  const text = String(resultText ?? '');
578
765
  const trimmed = text.trim();
766
+ if (/^(?:undefined|null)$/i.test(trimmed)) return null;
767
+ if (isMcpToolName(name)) return trimmed ? firstAgentResultLine(text) || null : null;
579
768
  const normalized = normalizeToolName(name);
580
769
 
581
770
  switch (normalized) {
@@ -585,7 +774,7 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
585
774
  if (/^\[image:/i.test(trimmed)) return 'Image';
586
775
  if (!trimmed) return null;
587
776
  const n = text.split('\n').length;
588
- return `${n} ${pluralize(n, 'Line')}`;
777
+ return `${n} ${pluralize(n, 'line')}`;
589
778
  }
590
779
  case 'apply_patch': {
591
780
  const updateSummary = summarizeUpdateResult(text, args);
@@ -613,17 +802,32 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
613
802
  if (!trimmed || !looksLineOriented(text)) return null;
614
803
  const n = countNonEmptyLines(text);
615
804
  if (n === 0) return null;
616
- return `${n} ${pluralize(n, 'Match', 'Matches')}`;
805
+ return `${n} ${pluralize(n, 'match', 'matches')}`;
617
806
  }
618
807
  case 'glob': {
619
808
  if (!trimmed || !looksLineOriented(text)) return null;
620
809
  const n = countNonEmptyLines(text);
621
810
  if (n === 0) return null;
622
- return `${n} ${pluralize(n, 'File')}`;
811
+ return `${n} ${pluralize(n, 'file')}`;
812
+ }
813
+ case 'find': {
814
+ if (!trimmed || !looksLineOriented(text)) return null;
815
+ const n = countNonEmptyLines(text);
816
+ if (n === 0) return null;
817
+ return `${n} ${pluralize(n, 'candidate')}`;
818
+ }
819
+ case 'list':
820
+ case 'ls': {
821
+ if (!trimmed || !looksLineOriented(text)) return null;
822
+ const n = countNonEmptyLines(text);
823
+ if (n === 0) return null;
824
+ return `${n} ${pluralize(n, 'entry', 'entries')}`;
623
825
  }
624
826
  case 'shell':
827
+ case 'bash':
625
828
  case 'bash_session':
626
- case 'shell_command': {
829
+ case 'shell_command':
830
+ case 'job_wait': {
627
831
  if (!trimmed) return '(No Output)';
628
832
  const job = /^\[(?:task_id|job):\s*([^\]]+)\]/mi.exec(text);
629
833
  const status = /^\[status:\s*([^\]]+)\]/mi.exec(text);
@@ -635,11 +839,12 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
635
839
  exit ? `Exit ${exit[1]}` : '',
636
840
  ]);
637
841
  }
638
- return null;
842
+ const firstLine = trimmed.split('\n').map((line) => line.trim()).find(Boolean) || trimmed;
843
+ return truncateSingleLine(firstLine, 120);
639
844
  }
640
845
  case 'code_graph': {
641
846
  const match = /(\d+)\s+(references|definitions|symbols|callers|callees|results|matches)/i.exec(text);
642
- if (match) return `${match[1]} ${titleWord(match[2])}`;
847
+ if (match) return `${match[1]} ${String(match[2]).toLowerCase()}`;
643
848
  return null;
644
849
  }
645
850
  case 'web_fetch':
@@ -654,35 +859,94 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
654
859
  if (status) return `HTTP ${status[1]}`;
655
860
  return null;
656
861
  }
862
+ case 'download_attachment':
863
+ return summarizeGenericResult(text);
657
864
  case 'search': {
658
865
  const match = /(\d+)\s+results?/i.exec(text);
659
866
  if (match) {
660
867
  const n = Number(match[1]);
661
- return `${n} ${pluralize(n, 'Result')}`;
868
+ return `${n} ${pluralize(n, 'result')}`;
662
869
  }
663
870
  return null;
664
871
  }
665
872
  case 'search_query':
666
873
  case 'image_query':
667
874
  case 'web_search':
668
- case 'web_search_call':
669
- case 'firecrawl_search': {
875
+ case 'web_search_call': {
670
876
  const match = /(\d+)\s+results?/i.exec(text);
671
877
  if (match) {
672
878
  const n = Number(match[1]);
673
- return `${n} ${pluralize(n, 'Result')}`;
879
+ return `${n} ${pluralize(n, 'result')}`;
674
880
  }
675
881
  return trimmed ? firstAgentResultLine(text) || null : null;
676
882
  }
677
883
  case 'explore': {
678
884
  return trimmed ? firstAgentResultLine(text) || null : null;
679
885
  }
680
- case 'bridge':
886
+ case 'recall':
887
+ case 'search_memories':
888
+ case 'memory':
889
+ case 'remember':
890
+ case 'save_memory':
891
+ case 'update_memory':
892
+ case 'reply':
893
+ case 'react':
894
+ case 'edit_message':
895
+ case 'activate_channel_bridge':
896
+ case 'inject_command':
897
+ case 'request_user_input':
898
+ case 'update_plan':
899
+ case 'diagnostics':
900
+ case 'open_config':
901
+ case 'provider_status':
902
+ case 'channel_status':
903
+ case 'schedule_status':
904
+ case 'schedule_control':
905
+ case 'trigger_schedule':
906
+ case 'reload_config':
907
+ case 'cwd':
908
+ case 'list_mcp_resources':
909
+ case 'list_mcp_resource_templates':
910
+ return summarizeGenericResult(text);
911
+ case 'skill':
912
+ case 'skill_execute':
913
+ case 'skill_view':
914
+ case 'skills_list':
915
+ case 'use_skill': {
916
+ const parsedArgs = parseToolArgs(args);
917
+ const target = firstText(parsedArgs.name, parsedArgs.skill, parsedArgs.skill_name);
918
+ if (normalized === 'skills_list') {
919
+ const count = /(\d+)\s+skills?/i.exec(text);
920
+ if (count) return `${Number(count[1]) || count[1]} ${pluralize(Number(count[1]) || 0, 'skill')}`;
921
+ const lines = countNonEmptyLines(text);
922
+ return lines > 0 ? `${lines} ${pluralize(lines, 'skill')}` : null;
923
+ }
924
+ if (target) {
925
+ const verb = normalized === 'skill' || normalized === 'skill_view' ? 'Loaded' : 'Used';
926
+ return `${verb} ${truncateToolText(target, 80)}`;
927
+ }
928
+ return trimmed ? firstAgentResultLine(text) || null : null;
929
+ }
681
930
  case 'agent':
682
931
  case 'task': {
932
+ // Status-check (list/status) envelopes start with "agents: N" / "tasks: M"
933
+ // (or "(no agents or tasks)"). Collapse them to a tight count summary
934
+ // instead of leaking the raw "agents: 3 …" worker dump into the card.
935
+ if (/^\(no agents or tasks\)$/im.test(text)) return 'No agents or tasks';
936
+ const agentsCount = /^agents:\s*(\d+)/im.exec(text);
937
+ const tasksCount = /^tasks:\s*(\d+)/im.exec(text);
938
+ if (agentsCount || tasksCount) {
939
+ const a = agentsCount ? Number(agentsCount[1]) : 0;
940
+ const t = tasksCount ? Number(tasksCount[1]) : 0;
941
+ const parts = [];
942
+ if (agentsCount) parts.push(`${a} ${pluralize(a, 'agent')}`);
943
+ if (tasksCount && t > 0) parts.push(`${t} ${pluralize(t, 'task')}`);
944
+ return compactParts(parts) || 'No agents or tasks';
945
+ }
683
946
  const answerLine = firstAgentResultLine(text);
684
- if (answerLine) return answerLine;
685
- const job = /^bridge job:\s*(job_[^\s]+)/mi.exec(text);
947
+ // Agent/task result cards show only a one-liner; full report via ctrl+o.
948
+ if (answerLine) return truncateSingleLine(stripInlineMarkdown(answerLine), AGENT_SURFACE_BRIEF_MAX);
949
+ const task = /^agent task:\s*(\S+)/mi.exec(text);
686
950
  const status = /^status:\s*([^\s(]+)/mi.exec(text);
687
951
  const role = /^role:\s*(.+)$/mi.exec(text);
688
952
  const preset = /^preset:\s*(.+)$/mi.exec(text);
@@ -694,6 +958,7 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
694
958
  ]);
695
959
  if (agentModel) return agentModel;
696
960
  const parts = [
961
+ task ? task[1] : '',
697
962
  role ? role[1] : '',
698
963
  preset ? preset[1] : '',
699
964
  model ? model[1] : '',
@@ -701,7 +966,7 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
701
966
  limits ? limits[1] : '',
702
967
  ].filter(Boolean);
703
968
  if (parts.length) return compactParts(parts);
704
- if (job) return status ? `${job[1]} ${titleStatus(status[1])}` : job[1];
969
+ if (task) return status ? `${task[1]} ${titleStatus(status[1])}` : task[1];
705
970
  return null;
706
971
  }
707
972
  default:
@@ -713,6 +978,36 @@ export function isExplorerSurface(label) {
713
978
  return label === 'Read' || label === 'Search';
714
979
  }
715
980
 
981
+ function truncateAgentSurfaceBrief(value, max = AGENT_SURFACE_BRIEF_MAX) {
982
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
983
+ if (!text) return '';
984
+ if (text.length <= max) return text;
985
+ return `${text.slice(0, Math.max(1, max - 1))}\u2026`;
986
+ }
987
+
988
+ // Tight one-liner for an agent/task card: prefer the inbound prompt/message for
989
+ // spawn/send, or a stripped first line of the result when a response landed.
990
+ export function summarizeAgentSurfaceBrief(name, args, resultText, { isError = false, isResponse = false } = {}) {
991
+ const a = parseToolArgs(args);
992
+ const action = String(a?.type || a?.action || '').toLowerCase();
993
+ const text = String(resultText ?? '').trim();
994
+ if (isResponse && text) {
995
+ const fromResult = summarizeToolResult(name, args, text, isError);
996
+ if (fromResult) return truncateAgentSurfaceBrief(stripInlineMarkdown(fromResult));
997
+ const line = firstAgentResultLine(text);
998
+ if (line) return truncateAgentSurfaceBrief(stripInlineMarkdown(line));
999
+ }
1000
+ const outbound = firstText(a?.prompt, a?.message);
1001
+ if (outbound && (action === 'spawn' || action === 'send' || !action)) {
1002
+ return truncateAgentSurfaceBrief(outbound);
1003
+ }
1004
+ if ((action === 'spawn' || action === 'send') && text && !isResponse) {
1005
+ const fromResult = summarizeToolResult(name, args, text, isError);
1006
+ if (fromResult) return truncateAgentSurfaceBrief(stripInlineMarkdown(fromResult));
1007
+ }
1008
+ return '';
1009
+ }
1010
+
716
1011
  export function isMemorySurface(label) {
717
1012
  return label === 'Memory';
718
1013
  }
@@ -720,8 +1015,8 @@ export function isMemorySurface(label) {
720
1015
  // ── Aggregate tool-card classification & formatting ──────────────
721
1016
 
722
1017
  export const CATEGORY_ORDER = [
723
- 'Read', 'Search', 'Web Research', 'Memory', 'Explore',
724
- 'Patch', 'Shell', 'Agent', 'Channel', 'Setup', 'Other',
1018
+ 'Read', 'Search', 'Load', 'MCP', 'Skill', 'Web Research', 'Memory', 'Explore',
1019
+ 'Patch', 'Shell', 'Agent', 'Task', 'Schedule', 'Channel', 'Setup', 'Other',
725
1020
  ];
726
1021
 
727
1022
  const TOOL_CATEGORY = new Map([
@@ -729,16 +1024,16 @@ const TOOL_CATEGORY = new Map([
729
1024
  ['view_image', 'Read'],
730
1025
  ['read_mcp_resource', 'Read'],
731
1026
  ['grep', 'Search'],
1027
+ ['find', 'Search'],
732
1028
  ['glob', 'Search'],
733
1029
  ['list', 'Search'],
734
1030
  ['ls', 'Search'],
735
- ['tool_search', 'Search'],
1031
+ ['tool_search', 'Load'],
736
1032
  ['search', 'Web Research'],
737
1033
  ['web_search', 'Web Research'],
738
1034
  ['search_query', 'Web Research'],
739
1035
  ['image_query', 'Web Research'],
740
1036
  ['web_search_call', 'Web Research'],
741
- ['firecrawl_search', 'Web Research'],
742
1037
  ['web_fetch', 'Web Research'],
743
1038
  ['fetch', 'Web Research'],
744
1039
  ['download_attachment', 'Web Research'],
@@ -756,8 +1051,7 @@ const TOOL_CATEGORY = new Map([
756
1051
  ['shell_command', 'Shell'],
757
1052
  ['bash_session', 'Shell'],
758
1053
  ['job_wait', 'Shell'],
759
- ['task', 'Agent'],
760
- ['bridge', 'Agent'],
1054
+ ['task', 'Task'],
761
1055
  ['agent', 'Agent'],
762
1056
  ['reply', 'Channel'],
763
1057
  ['react', 'Channel'],
@@ -776,16 +1070,17 @@ const TOOL_CATEGORY = new Map([
776
1070
  ['cwd', 'Setup'],
777
1071
  ['request_user_input', 'Setup'],
778
1072
  ['update_plan', 'Setup'],
779
- ['trigger_schedule', 'Setup'],
780
- ['skill', 'Setup'],
781
- ['skill_execute', 'Setup'],
782
- ['skill_view', 'Setup'],
783
- ['skills_list', 'Setup'],
784
- ['use_skill', 'Setup'],
1073
+ ['trigger_schedule', 'Schedule'],
1074
+ ['skill', 'Skill'],
1075
+ ['skill_execute', 'Skill'],
1076
+ ['skill_view', 'Skill'],
1077
+ ['skills_list', 'Skill'],
1078
+ ['use_skill', 'Skill'],
785
1079
  ]);
786
1080
 
787
1081
  /** Return the aggregate category for a tool name + args. */
788
1082
  export function classifyToolCategory(name, args = {}) {
1083
+ if (isMcpToolName(name)) return 'MCP';
789
1084
  const normalized = normalizeToolName(name);
790
1085
  if (normalized === 'code_graph') {
791
1086
  const mode = String(args.mode || args.action || '').toLowerCase();
@@ -796,16 +1091,21 @@ export function classifyToolCategory(name, args = {}) {
796
1091
  }
797
1092
 
798
1093
  const CATEGORY_COPY = new Map([
799
- ['Read', { active: 'Reading', done: 'Read', noun: 'item' }],
800
- ['Search', { active: 'Searching', done: 'Searched', noun: 'item' }],
801
- ['Web Research', { active: 'Researching', done: 'Researched', noun: 'web item' }],
1094
+ ['Read', { active: 'Reading', done: 'Read', noun: 'file' }],
1095
+ ['Search', { active: 'Searching', done: 'Searched', noun: 'file' }],
1096
+ ['Load', { active: 'Loading', done: 'Loaded', noun: 'tool' }],
1097
+ ['MCP', { active: 'Using', done: 'Used', noun: 'MCP tool' }],
1098
+ ['Skill', { active: 'Loading', done: 'Loaded', noun: 'skill' }],
1099
+ ['Web Research', { active: 'Researching', done: 'Researched', noun: 'query', pluralNoun: 'queries' }],
802
1100
  ['Memory', { active: 'Checking', done: 'Checked', noun: 'memory item' }],
803
- ['Explore', { active: 'Exploring', done: 'Explored', noun: 'item' }],
804
- ['Patch', { active: 'Editing', done: 'Edited', noun: 'item' }],
1101
+ ['Explore', { active: 'Exploring', done: 'Explored', noun: 'query', pluralNoun: 'queries' }],
1102
+ ['Patch', { active: 'Editing', done: 'Edited', noun: 'file' }],
805
1103
  ['Shell', { active: 'Running', done: 'Ran', noun: 'command' }],
806
1104
  ['Agent', { active: 'Calling', done: 'Called', noun: 'agent' }],
1105
+ ['Task', { active: 'Checking', done: 'Checked', noun: 'task' }],
1106
+ ['Schedule', { active: 'Running', done: 'Ran', noun: 'schedule' }],
807
1107
  ['Channel', { active: 'Sending', done: 'Sent', noun: 'message' }],
808
- ['Setup', { active: 'Setting Up', done: 'Set Up', noun: 'item' }],
1108
+ ['Setup', { active: 'Setting up', done: 'Set up', noun: 'item' }],
809
1109
  ['Other', { active: 'Calling', done: 'Called', noun: 'tool' }],
810
1110
  ]);
811
1111
 
@@ -823,17 +1123,241 @@ function categoryNoun(category, count) {
823
1123
  return pluralize(count, copy.noun, copy.pluralNoun || `${copy.noun}s`);
824
1124
  }
825
1125
 
1126
+ function categoryCopy(category) {
1127
+ return CATEGORY_COPY.get(category) || CATEGORY_COPY.get('Other') || { active: 'Calling', done: 'Called', noun: 'tool' };
1128
+ }
1129
+
1130
+ function unitDescriptor(category, overrides = {}) {
1131
+ const copy = categoryCopy(category);
1132
+ return {
1133
+ category,
1134
+ active: overrides.active || copy.active,
1135
+ done: overrides.done || copy.done,
1136
+ noun: overrides.noun || copy.noun || 'item',
1137
+ pluralNoun: overrides.pluralNoun || copy.pluralNoun || `${overrides.noun || copy.noun || 'item'}s`,
1138
+ count: Math.max(1, Number(overrides.count || 1)),
1139
+ };
1140
+ }
1141
+
1142
+ function queryCount(args, ...keys) {
1143
+ return collectionCount(...keys.map((key) => args?.[key]));
1144
+ }
1145
+
1146
+ export function toolWorkUnit(name, args = {}, category = '') {
1147
+ const a = parseToolArgs(args);
1148
+ const normalized = normalizeToolName(name);
1149
+ const cat = category || classifyToolCategory(name, a);
1150
+ if (isMcpToolName(name)) {
1151
+ return unitDescriptor('MCP', { count: queryCount(a, 'query', 'q', 'text', 'prompt', 'path', 'uri', 'name', 'id', 'action') || 1 });
1152
+ }
1153
+ switch (normalized) {
1154
+ case 'read':
1155
+ return unitDescriptor('Read', { count: queryCount(a, 'path', 'paths', 'file_path', 'file', 'files') || 1, noun: 'file' });
1156
+ case 'view_image':
1157
+ return unitDescriptor('Read', { count: queryCount(a, 'path', 'file_path', 'file') || 1, noun: 'image' });
1158
+ case 'read_mcp_resource':
1159
+ return unitDescriptor('Read', { count: queryCount(a, 'uri', 'uris') || 1, noun: 'resource' });
1160
+ case 'apply_patch': {
1161
+ const creating = a.old_string === '';
1162
+ // A dry_run patch validates the diff WITHOUT writing any file, so the
1163
+ // header must not claim "Editing/Edited" (which made a pure validation
1164
+ // look like a real edit). Surface it as "Checking/Checked" instead.
1165
+ if (a.dry_run === true) {
1166
+ return unitDescriptor('Patch', {
1167
+ count: patchFileCount(a) || 1,
1168
+ active: 'Checking',
1169
+ done: 'Checked',
1170
+ noun: 'file',
1171
+ });
1172
+ }
1173
+ return unitDescriptor('Patch', {
1174
+ count: patchFileCount(a) || 1,
1175
+ active: creating ? 'Creating' : 'Editing',
1176
+ done: creating ? 'Created' : 'Edited',
1177
+ noun: 'file',
1178
+ });
1179
+ }
1180
+ case 'grep':
1181
+ return unitDescriptor('Search', { count: queryCount(a, 'pattern', 'patterns', 'query') || 1, active: 'Searching', done: 'Searched', noun: 'pattern' });
1182
+ case 'glob':
1183
+ return unitDescriptor('Search', { count: queryCount(a, 'pattern', 'patterns', 'glob', 'globs') || 1, active: 'Finding', done: 'Found', noun: 'glob' });
1184
+ case 'find':
1185
+ return unitDescriptor('Search', { count: queryCount(a, 'query', 'queries', 'fuzzy') || 1, active: 'Finding', done: 'Found', noun: 'query', pluralNoun: 'queries' });
1186
+ case 'list':
1187
+ case 'ls':
1188
+ return unitDescriptor('Search', { count: queryCount(a, 'path', 'paths', 'dir', 'dirs', 'cwd') || 1, active: 'Listing', done: 'Listed', noun: 'directory', pluralNoun: 'directories' });
1189
+ case 'tool_search': {
1190
+ const selected = splitToolSearchSelection(a.select);
1191
+ if (selected.length) return unitDescriptor('Load', { count: selected.length, noun: 'tool' });
1192
+ return unitDescriptor('Load', { count: queryCount(a, 'query', 'q', 'text') || 1, noun: 'query', pluralNoun: 'queries' });
1193
+ }
1194
+ case 'search':
1195
+ case 'search_query':
1196
+ case 'image_query':
1197
+ case 'web_search':
1198
+ case 'web_search_call':
1199
+ return unitDescriptor('Web Research', { count: queryCount(a, 'query', 'queries', 'keywords') || 1, noun: 'query', pluralNoun: 'queries' });
1200
+ case 'web_fetch':
1201
+ case 'fetch':
1202
+ return unitDescriptor('Web Research', { count: queryCount(a, 'url', 'urls', 'uri', 'uris') || 1, active: 'Fetching', done: 'Fetched', noun: 'URL', pluralNoun: 'URLs' });
1203
+ case 'download_attachment':
1204
+ return unitDescriptor('Web Research', { count: queryCount(a, 'url', 'urls', 'filename', 'name') || 1, active: 'Downloading', done: 'Downloaded', noun: 'attachment' });
1205
+ case 'recall':
1206
+ case 'recall_memory':
1207
+ case 'search_memories':
1208
+ return unitDescriptor('Memory', { count: queryCount(a, 'query', 'queries', 'text', 'input') || 1, noun: 'memory item', pluralNoun: 'memory items' });
1209
+ case 'remember':
1210
+ case 'save_memory':
1211
+ case 'update_memory':
1212
+ case 'memory':
1213
+ return unitDescriptor('Memory', { count: queryCount(a, 'entries', 'items', 'memories', 'query', 'text', 'value') || 1, active: 'Writing', done: 'Wrote', noun: 'memory item' });
1214
+ case 'explore':
1215
+ return unitDescriptor('Explore', { count: queryCount(a, 'query', 'queries', 'prompt', 'task', 'goal') || 1, noun: 'query', pluralNoun: 'queries' });
1216
+ case 'shell':
1217
+ case 'bash':
1218
+ case 'bash_session':
1219
+ case 'shell_command':
1220
+ case 'job_wait':
1221
+ return unitDescriptor('Shell', { count: queryCount(a, 'command', 'commands', 'cmd') || 1, noun: 'command' });
1222
+ case 'agent':
1223
+ case 'bridge':
1224
+ return unitDescriptor('Agent', { count: queryCount(a, 'agents', 'roles', 'role', 'tag', 'task_id', 'sessionId') || 1, noun: 'agent' });
1225
+ case 'task':
1226
+ return unitDescriptor('Task', { count: queryCount(a, 'task_id', 'task_ids', 'id', 'ids') || 1, noun: 'task' });
1227
+ case 'skill':
1228
+ case 'skill_execute':
1229
+ case 'skill_view':
1230
+ case 'skills_list':
1231
+ case 'use_skill':
1232
+ return unitDescriptor('Skill', { count: queryCount(a, 'name', 'skill', 'skill_name', 'query', 'q') || 1, noun: 'skill' });
1233
+ case 'reply':
1234
+ return unitDescriptor('Channel', { count: queryCount(a, 'messages', 'messageId', 'text') || 1, noun: 'message' });
1235
+ case 'react':
1236
+ return unitDescriptor('Channel', { count: queryCount(a, 'emoji', 'messageId') || 1, active: 'Reacting', done: 'Reacted', noun: 'reaction' });
1237
+ case 'edit_message':
1238
+ return unitDescriptor('Channel', { count: queryCount(a, 'messageId', 'text') || 1, active: 'Editing', done: 'Edited', noun: 'message' });
1239
+ case 'activate_channel_bridge':
1240
+ return unitDescriptor('Channel', { active: 'Toggling', done: 'Toggled', noun: 'channel bridge' });
1241
+ case 'inject_command':
1242
+ return unitDescriptor('Channel', { count: queryCount(a, 'command', 'text', 'name') || 1, active: 'Injecting', done: 'Injected', noun: 'command' });
1243
+ case 'trigger_schedule':
1244
+ return unitDescriptor('Schedule', { count: queryCount(a, 'name', 'id') || 1, noun: 'schedule' });
1245
+ case 'code_graph': {
1246
+ const mode = String(a.mode || a.action || '').toLowerCase();
1247
+ const searching = mode === 'search' || mode === 'find_symbol' || mode === 'references' || mode === 'callers' || mode === 'callees';
1248
+ return unitDescriptor(searching ? 'Search' : 'Read', {
1249
+ count: queryCount(a, 'symbols', 'symbol', 'query', 'file', 'path') || 1,
1250
+ active: searching ? 'Mapping' : 'Reading',
1251
+ done: searching ? 'Mapped' : 'Read',
1252
+ noun: searching ? 'symbol' : 'file',
1253
+ });
1254
+ }
1255
+ case 'request_user_input':
1256
+ return unitDescriptor('Setup', { active: 'Asking', done: 'Asked', noun: 'user' });
1257
+ case 'update_plan':
1258
+ return unitDescriptor('Setup', { active: 'Updating', done: 'Updated', noun: 'plan' });
1259
+ case 'diagnostics':
1260
+ return unitDescriptor('Setup', { active: 'Checking', done: 'Checked', noun: 'diagnostic' });
1261
+ case 'open_config':
1262
+ return unitDescriptor('Setup', { active: 'Opening', done: 'Opened', noun: 'config' });
1263
+ case 'provider_status':
1264
+ return unitDescriptor('Setup', { active: 'Checking', done: 'Checked', noun: 'provider' });
1265
+ case 'channel_status':
1266
+ return unitDescriptor('Setup', { active: 'Checking', done: 'Checked', noun: 'channel' });
1267
+ case 'schedule_status':
1268
+ return unitDescriptor('Setup', { active: 'Checking', done: 'Checked', noun: 'schedule' });
1269
+ case 'schedule_control':
1270
+ return unitDescriptor('Setup', { active: 'Updating', done: 'Updated', noun: 'schedule' });
1271
+ case 'reload_config':
1272
+ return unitDescriptor('Setup', { active: 'Reloading', done: 'Reloaded', noun: 'config' });
1273
+ case 'list_mcp_resources':
1274
+ return unitDescriptor('Setup', { active: 'Listing', done: 'Listed', noun: 'MCP resource' });
1275
+ case 'list_mcp_resource_templates':
1276
+ return unitDescriptor('Setup', { active: 'Listing', done: 'Listed', noun: 'MCP resource template' });
1277
+ case 'cwd': {
1278
+ const action = String(a.action || a.type || '').toLowerCase();
1279
+ return action === 'set'
1280
+ ? unitDescriptor('Setup', { active: 'Setting', done: 'Set', noun: 'working directory', pluralNoun: 'working directories' })
1281
+ : unitDescriptor('Setup', { active: 'Checking', done: 'Checked', noun: 'working directory', pluralNoun: 'working directories' });
1282
+ }
1283
+ default:
1284
+ return unitDescriptor(cat, { count: queryCount(a, 'items', 'targets', 'query', 'path', 'name', 'id', 'action') || 1 });
1285
+ }
1286
+ }
1287
+
1288
+ function lifecycleVerb(unit, pending, { stableVerbWidth = false } = {}) {
1289
+ const active = String(unit.active || '');
1290
+ const done = String(unit.done || '');
1291
+ const verb = pending ? active : done;
1292
+ if (!stableVerbWidth) return verb;
1293
+ return verb.padEnd(Math.max(active.length, done.length), ' ');
1294
+ }
1295
+
1296
+ export function formatToolActionHeader(name, args = {}, { pending = false, count = 1, category = '', stableVerbWidth = false } = {}) {
1297
+ const unit = toolWorkUnit(name, args, category);
1298
+ const n = Math.max(1, Number(unit.count || count || 1));
1299
+ const verb = lifecycleVerb(unit, pending, { stableVerbWidth });
1300
+ return `${verb} ${n} ${pluralize(n, unit.noun, unit.pluralNoun)}`;
1301
+ }
1302
+
1303
+ export function aggregateToolCategoryEntry(name, args = {}, category = '') {
1304
+ const cat = category || classifyToolCategory(name, args);
1305
+ const unit = toolWorkUnit(name, args, cat);
1306
+ const key = [cat, unit.active, unit.done, unit.noun, unit.pluralNoun].join('|');
1307
+ return {
1308
+ key,
1309
+ category: cat,
1310
+ active: unit.active,
1311
+ done: unit.done,
1312
+ noun: unit.noun,
1313
+ pluralNoun: unit.pluralNoun,
1314
+ count: Math.max(1, Number(unit.count || 1)),
1315
+ };
1316
+ }
1317
+
1318
+ function aggregateCount(value) {
1319
+ if (value && typeof value === 'object') return Math.max(0, Number(value.count || 0));
1320
+ return Math.max(0, Number(value || 0));
1321
+ }
1322
+
1323
+ function aggregateDescriptor(key, value) {
1324
+ if (value && typeof value === 'object') {
1325
+ const category = value.category || String(key || '').split('|')[0] || 'Other';
1326
+ const copy = categoryCopy(category);
1327
+ const noun = value.noun || copy.noun || 'item';
1328
+ return {
1329
+ category,
1330
+ active: value.active || copy.active,
1331
+ done: value.done || copy.done,
1332
+ noun,
1333
+ pluralNoun: value.pluralNoun || copy.pluralNoun || `${noun}s`,
1334
+ count: aggregateCount(value),
1335
+ };
1336
+ }
1337
+ const category = String(key || '');
1338
+ const copy = categoryCopy(category);
1339
+ const noun = copy.noun || 'item';
1340
+ return {
1341
+ category,
1342
+ active: copy.active,
1343
+ done: copy.done,
1344
+ noun,
1345
+ pluralNoun: copy.pluralNoun || `${noun}s`,
1346
+ count: aggregateCount(value),
1347
+ };
1348
+ }
1349
+
826
1350
  /**
827
1351
  * Build a comma-separated header from per-category counts.
828
1352
  * e.g. "Read 6 items, Searched 5 items, Called 1 agent"
829
1353
  */
830
- export function formatAggregateHeader(categories, { pending = false, order = null } = {}) {
1354
+ export function formatAggregateHeader(categories, { pending = false, order = null, stableVerbWidth = false } = {}) {
831
1355
  const categoryKeys = Object.keys(categories || {});
832
1356
  const preferred = Array.isArray(order) && order.length ? order : categoryKeys;
833
1357
  const seen = new Set();
834
1358
  const ordered = [];
835
1359
  const add = (cat) => {
836
- if (!cat || seen.has(cat) || (categories[cat] || 0) <= 0) return;
1360
+ if (!cat || seen.has(cat) || aggregateCount(categories[cat]) <= 0) return;
837
1361
  seen.add(cat);
838
1362
  ordered.push(cat);
839
1363
  };
@@ -843,10 +1367,9 @@ export function formatAggregateHeader(categories, { pending = false, order = nul
843
1367
 
844
1368
  return ordered
845
1369
  .map((cat) => {
846
- const count = Number(categories[cat] || 0);
847
- const label = pending ? activeCategoryLabel(cat) : doneCategoryLabel(cat);
848
- if (pending) return label;
849
- return `${label} ${count} ${categoryNoun(cat, count)}`;
1370
+ const item = aggregateDescriptor(cat, categories[cat]);
1371
+ const label = lifecycleVerb(item, pending, { stableVerbWidth });
1372
+ return `${label} ${item.count} ${pluralize(item.count, item.noun, item.pluralNoun)}`;
850
1373
  })
851
1374
  .join(', ');
852
1375
  }
@@ -882,13 +1405,13 @@ export function formatAggregateDetail(summaries) {
882
1405
 
883
1406
  let match = /^(?:Read\s+)?(\d+)\s+lines?$/i.exec(text);
884
1407
  if (match) {
885
- const metric = addMetric('read_lines', { count: 0, render: (m) => `${m.count} ${pluralize(m.count, 'Line')}` });
1408
+ const metric = addMetric('read_lines', { count: 0, render: (m) => `${m.count} ${pluralize(m.count, 'line')}` });
886
1409
  metric.count += Number(match[1]);
887
1410
  continue;
888
1411
  }
889
1412
 
890
1413
  if (/^(?:Read\s+)?image$/i.test(text)) {
891
- const metric = addMetric('read_images', { count: 0, render: (m) => `${m.count} ${pluralize(m.count, 'Image')}` });
1414
+ const metric = addMetric('read_images', { count: 0, render: (m) => `${m.count} ${pluralize(m.count, 'image')}` });
892
1415
  metric.count += 1;
893
1416
  continue;
894
1417
  }
@@ -899,7 +1422,7 @@ export function formatAggregateDetail(summaries) {
899
1422
  const singular = nounRaw.endsWith('ies') ? `${nounRaw.slice(0, -3)}y` : nounRaw.endsWith('s') ? nounRaw.slice(0, -1) : nounRaw;
900
1423
  const plural = nounRaw.endsWith('s') ? nounRaw : `${nounRaw}s`;
901
1424
  const key = `found_${plural}`;
902
- const metric = addMetric(key, { count: 0, singular, plural, render: (m) => `${m.count} ${pluralize(m.count, titleWord(m.singular), titleWord(m.plural))}` });
1425
+ const metric = addMetric(key, { count: 0, singular, plural, render: (m) => `${m.count} ${pluralize(m.count, m.singular, m.plural)}` });
903
1426
  metric.count += Number(match[1]);
904
1427
  continue;
905
1428
  }
@@ -922,10 +1445,16 @@ export function formatAggregateDetail(summaries) {
922
1445
  removed: 0,
923
1446
  seen: false,
924
1447
  render: (m) => {
1448
+ // The aggregate header already carries the action + file count
1449
+ // (e.g. "Edited 2 files"), so the detail row shows only the merged
1450
+ // line delta. Fall back to the action + file/count summary only when
1451
+ // there is no +/- delta to show (e.g. pure create/delete).
1452
+ const delta = formatLineDelta(m);
1453
+ if (delta) return delta;
925
1454
  const count = m.fileCount + m.files.size;
926
1455
  const action = m.actions.size === 1 ? [...m.actions][0] : 'Updated';
927
- const target = count === 1 && m.fileCount === 0 ? [...m.files][0] : `${count} ${pluralize(count, 'File')}`;
928
- return compactParts([`${action} ${target}`, formatLineDelta(m)]);
1456
+ const target = count === 1 && m.fileCount === 0 ? [...m.files][0] : `${count} ${pluralize(count, 'file')}`;
1457
+ return `${action} ${target}`;
929
1458
  },
930
1459
  });
931
1460
  if (update.file) metric.files.add(update.file);