mixdog 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  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 +344 -0
  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 +1223 -95
  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 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. 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', 'Edit', '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'],
@@ -751,15 +1046,12 @@ const TOOL_CATEGORY = new Map([
751
1046
  ['memory', 'Memory'],
752
1047
  ['explore', 'Explore'],
753
1048
  ['apply_patch', 'Patch'],
754
- ['write', 'Edit'],
755
- ['edit', 'Edit'],
756
1049
  ['bash', 'Shell'],
757
1050
  ['shell', 'Shell'],
758
1051
  ['shell_command', 'Shell'],
759
1052
  ['bash_session', 'Shell'],
760
1053
  ['job_wait', 'Shell'],
761
- ['task', 'Agent'],
762
- ['bridge', 'Agent'],
1054
+ ['task', 'Task'],
763
1055
  ['agent', 'Agent'],
764
1056
  ['reply', 'Channel'],
765
1057
  ['react', 'Channel'],
@@ -778,16 +1070,17 @@ const TOOL_CATEGORY = new Map([
778
1070
  ['cwd', 'Setup'],
779
1071
  ['request_user_input', 'Setup'],
780
1072
  ['update_plan', 'Setup'],
781
- ['trigger_schedule', 'Setup'],
782
- ['skill', 'Setup'],
783
- ['skill_execute', 'Setup'],
784
- ['skill_view', 'Setup'],
785
- ['skills_list', 'Setup'],
786
- ['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'],
787
1079
  ]);
788
1080
 
789
1081
  /** Return the aggregate category for a tool name + args. */
790
1082
  export function classifyToolCategory(name, args = {}) {
1083
+ if (isMcpToolName(name)) return 'MCP';
791
1084
  const normalized = normalizeToolName(name);
792
1085
  if (normalized === 'code_graph') {
793
1086
  const mode = String(args.mode || args.action || '').toLowerCase();
@@ -798,17 +1091,21 @@ export function classifyToolCategory(name, args = {}) {
798
1091
  }
799
1092
 
800
1093
  const CATEGORY_COPY = new Map([
801
- ['Read', { active: 'Reading', done: 'Read', noun: 'item' }],
802
- ['Search', { active: 'Searching', done: 'Searched', noun: 'item' }],
803
- ['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' }],
804
1100
  ['Memory', { active: 'Checking', done: 'Checked', noun: 'memory item' }],
805
- ['Explore', { active: 'Exploring', done: 'Explored', noun: 'item' }],
806
- ['Patch', { active: 'Editing', done: 'Edited', noun: 'item' }],
807
- ['Edit', { active: 'Editing', done: 'Edited', noun: 'item' }],
1101
+ ['Explore', { active: 'Exploring', done: 'Explored', noun: 'query', pluralNoun: 'queries' }],
1102
+ ['Patch', { active: 'Editing', done: 'Edited', noun: 'file' }],
808
1103
  ['Shell', { active: 'Running', done: 'Ran', noun: 'command' }],
809
1104
  ['Agent', { active: 'Calling', done: 'Called', noun: 'agent' }],
1105
+ ['Task', { active: 'Checking', done: 'Checked', noun: 'task' }],
1106
+ ['Schedule', { active: 'Running', done: 'Ran', noun: 'schedule' }],
810
1107
  ['Channel', { active: 'Sending', done: 'Sent', noun: 'message' }],
811
- ['Setup', { active: 'Setting Up', done: 'Set Up', noun: 'item' }],
1108
+ ['Setup', { active: 'Setting up', done: 'Set up', noun: 'item' }],
812
1109
  ['Other', { active: 'Calling', done: 'Called', noun: 'tool' }],
813
1110
  ]);
814
1111
 
@@ -826,17 +1123,241 @@ function categoryNoun(category, count) {
826
1123
  return pluralize(count, copy.noun, copy.pluralNoun || `${copy.noun}s`);
827
1124
  }
828
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
+
829
1350
  /**
830
1351
  * Build a comma-separated header from per-category counts.
831
1352
  * e.g. "Read 6 items, Searched 5 items, Called 1 agent"
832
1353
  */
833
- export function formatAggregateHeader(categories, { pending = false, order = null } = {}) {
1354
+ export function formatAggregateHeader(categories, { pending = false, order = null, stableVerbWidth = false } = {}) {
834
1355
  const categoryKeys = Object.keys(categories || {});
835
1356
  const preferred = Array.isArray(order) && order.length ? order : categoryKeys;
836
1357
  const seen = new Set();
837
1358
  const ordered = [];
838
1359
  const add = (cat) => {
839
- if (!cat || seen.has(cat) || (categories[cat] || 0) <= 0) return;
1360
+ if (!cat || seen.has(cat) || aggregateCount(categories[cat]) <= 0) return;
840
1361
  seen.add(cat);
841
1362
  ordered.push(cat);
842
1363
  };
@@ -846,10 +1367,9 @@ export function formatAggregateHeader(categories, { pending = false, order = nul
846
1367
 
847
1368
  return ordered
848
1369
  .map((cat) => {
849
- const count = Number(categories[cat] || 0);
850
- const label = pending ? activeCategoryLabel(cat) : doneCategoryLabel(cat);
851
- if (pending) return label;
852
- 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)}`;
853
1373
  })
854
1374
  .join(', ');
855
1375
  }
@@ -885,13 +1405,13 @@ export function formatAggregateDetail(summaries) {
885
1405
 
886
1406
  let match = /^(?:Read\s+)?(\d+)\s+lines?$/i.exec(text);
887
1407
  if (match) {
888
- 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')}` });
889
1409
  metric.count += Number(match[1]);
890
1410
  continue;
891
1411
  }
892
1412
 
893
1413
  if (/^(?:Read\s+)?image$/i.test(text)) {
894
- 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')}` });
895
1415
  metric.count += 1;
896
1416
  continue;
897
1417
  }
@@ -902,7 +1422,7 @@ export function formatAggregateDetail(summaries) {
902
1422
  const singular = nounRaw.endsWith('ies') ? `${nounRaw.slice(0, -3)}y` : nounRaw.endsWith('s') ? nounRaw.slice(0, -1) : nounRaw;
903
1423
  const plural = nounRaw.endsWith('s') ? nounRaw : `${nounRaw}s`;
904
1424
  const key = `found_${plural}`;
905
- 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)}` });
906
1426
  metric.count += Number(match[1]);
907
1427
  continue;
908
1428
  }
@@ -925,10 +1445,16 @@ export function formatAggregateDetail(summaries) {
925
1445
  removed: 0,
926
1446
  seen: false,
927
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;
928
1454
  const count = m.fileCount + m.files.size;
929
1455
  const action = m.actions.size === 1 ? [...m.actions][0] : 'Updated';
930
- const target = count === 1 && m.fileCount === 0 ? [...m.files][0] : `${count} ${pluralize(count, 'File')}`;
931
- 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}`;
932
1458
  },
933
1459
  });
934
1460
  if (update.file) metric.files.add(update.file);