mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,133 @@
1
+ /**
2
+ * src/tui/engine/agent-job-feed.mjs — agent-job / runtime-notification plumbing
3
+ * extracted from createEngineSession (engine.mjs) as a dependency-injection
4
+ * factory.
5
+ *
6
+ * Owns: the agent-job card result patch (updateAgentJobCard), the
7
+ * execution-pending-resume kick trio (kick / flushDeferred / schedule), and the
8
+ * runtime.onNotification subscription that routes runtime notifications into the
9
+ * store (status refresh, execution-ui synthetic items, model-visible enqueue).
10
+ *
11
+ * These handlers mutate live session state and drive the queue, so
12
+ * state/set/enqueue/drain/pushUserOrSyntheticItem/patchItem/etc are threaded via
13
+ * the factory argument (getters/callbacks) — never stale snapshots. Every body
14
+ * is the original engine.mjs logic verbatim.
15
+ */
16
+ import {
17
+ parseAgentJob,
18
+ agentJobResultText,
19
+ agentArgsWithResultMetadata,
20
+ } from './agent-envelope.mjs';
21
+ import { toolErrorDisplay } from './tool-result-text.mjs';
22
+ import {
23
+ notificationQueueKey,
24
+ resolveTuiRuntimeNotificationDelivery,
25
+ } from './notification-plan.mjs';
26
+
27
+ export function createAgentJobFeed({
28
+ runtime,
29
+ getState,
30
+ set,
31
+ nextId,
32
+ getDisposed,
33
+ patchItem,
34
+ enqueue,
35
+ drain,
36
+ pushUserOrSyntheticItem,
37
+ makeQueueEntry,
38
+ getPending,
39
+ agentStatusState,
40
+ displayedExecutionNotificationKeys,
41
+ }) {
42
+ let executionResumeKickDeferred = false;
43
+
44
+ function kickExecutionPendingResume() {
45
+ if (getDisposed()) return;
46
+ if (getState().busy) {
47
+ executionResumeKickDeferred = true;
48
+ return;
49
+ }
50
+ const pending = getPending();
51
+ if (pending.some((entry) => entry.mode === 'pending-resume')) {
52
+ executionResumeKickDeferred = true;
53
+ return;
54
+ }
55
+ executionResumeKickDeferred = false;
56
+ pending.push(makeQueueEntry('', { mode: 'pending-resume', priority: 'next' }));
57
+ void drain();
58
+ }
59
+
60
+ function flushDeferredExecutionPendingResumeKick() {
61
+ if (!executionResumeKickDeferred || getDisposed() || getState().busy) return;
62
+ kickExecutionPendingResume();
63
+ }
64
+
65
+ function scheduleExecutionPendingResumeKick() {
66
+ // notifyFnForSession enqueues the model-visible body after onNotification
67
+ // returns; defer the kick so askSession pre-drain sees session pending.
68
+ queueMicrotask(() => kickExecutionPendingResume());
69
+ }
70
+
71
+ function updateAgentJobCard(itemId, text, isError = false) {
72
+ const parsed = parseAgentJob(text);
73
+ const current = getState().items.find((it) => it.id === itemId);
74
+ const rawDisplayText = agentJobResultText(text, parsed) || String(text ?? '').trim();
75
+ const displayText = isError ? toolErrorDisplay(rawDisplayText, 'agent') : rawDisplayText;
76
+ patchItem(itemId, {
77
+ result: displayText,
78
+ text: displayText,
79
+ isError,
80
+ errorCount: isError ? 1 : 0,
81
+ ...(parsed ? { args: agentArgsWithResultMetadata(current?.args, parsed) } : {}),
82
+ });
83
+ }
84
+
85
+ function subscribeRuntimeNotifications() {
86
+ if (typeof runtime.onNotification !== 'function') return null;
87
+ return runtime.onNotification((event) => {
88
+ if (getDisposed()) return;
89
+ const text = String(event?.content ?? event?.text ?? event ?? '').trim();
90
+ if (!text) return;
91
+ const parsed = parseAgentJob(text);
92
+ const notificationKey = notificationQueueKey(event, text, parsed);
93
+ const delivery = resolveTuiRuntimeNotificationDelivery(event, text);
94
+ if (delivery.action === 'ignore') return;
95
+ if (delivery.action === 'status-only') {
96
+ if (parsed?.taskId) set(agentStatusState({ force: true }));
97
+ return true;
98
+ }
99
+ if (delivery.action === 'execution-ui') {
100
+ const firstDelivery = !notificationKey || !displayedExecutionNotificationKeys.has(notificationKey);
101
+ if (firstDelivery) {
102
+ if (notificationKey) displayedExecutionNotificationKeys.add(notificationKey);
103
+ pushUserOrSyntheticItem(delivery.displayText, nextId());
104
+ }
105
+ if (parsed?.taskId) set(agentStatusState({ force: true }));
106
+ if (String(delivery.modelContent || '').trim()) {
107
+ scheduleExecutionPendingResumeKick();
108
+ }
109
+ return true;
110
+ }
111
+ if (parsed?.taskId) {
112
+ set(agentStatusState({ force: true }));
113
+ }
114
+ const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
115
+ if (!modelContent) return true;
116
+ enqueue(modelContent, {
117
+ mode: 'task-notification',
118
+ priority: 'next',
119
+ key: notificationKey || undefined,
120
+ displayText: delivery.displayText || text,
121
+ });
122
+ return true;
123
+ });
124
+ }
125
+
126
+ return {
127
+ kickExecutionPendingResume,
128
+ flushDeferredExecutionPendingResumeKick,
129
+ scheduleExecutionPendingResumeKick,
130
+ updateAgentJobCard,
131
+ subscribeRuntimeNotifications,
132
+ };
133
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * src/tui/engine/notification-plan.mjs - pure runtime.onNotification delivery plan.
3
+ *
4
+ * Extracted from engine.mjs (no behavior change). These are stateless helpers
5
+ * that decide how a runtime notification envelope should be delivered to the
6
+ * TUI store (dedupe key + display/model routing). No closures over engine state.
7
+ */
8
+ import {
9
+ isStatusOnlyAgentCompletionNotification,
10
+ parseAgentJob,
11
+ parseAgentResultEnvelope,
12
+ parseBackgroundTaskEnvelope,
13
+ parseSyntheticAgentMessage,
14
+ } from './agent-envelope.mjs';
15
+ import { shortTextFingerprint } from './queue-helpers.mjs';
16
+ import { modelVisibleToolCompletionMessage } from '../../runtime/shared/tool-execution-contract.mjs';
17
+
18
+ export function notificationQueueKey(event, text, parsed) {
19
+ const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
20
+ const synthetic = parseSyntheticAgentMessage(text);
21
+ if (synthetic?.name === 'agent' && String(synthetic.args?.type || '').toLowerCase() === 'result') {
22
+ const taskId = String(synthetic.args?.task_id || '').trim();
23
+ const executionId = String(meta.execution_id || '').trim();
24
+ const tag = String(synthetic.args?.tag || '').trim();
25
+ const resultId = taskId
26
+ ? `task:${taskId}`
27
+ : executionId
28
+ ? `exec:${executionId}`
29
+ : tag
30
+ ? `tag:${tag}:${shortTextFingerprint(synthetic.result || text)}`
31
+ : '';
32
+ const agent = String(synthetic.args?.agent || '').trim();
33
+ if (resultId || agent) return ['agent-result', resultId, agent].filter(Boolean).join(':');
34
+ }
35
+ const id = String(meta.execution_id || parsed?.taskId || '').trim();
36
+ if (!id) return '';
37
+ const type = String(meta.type || '').trim();
38
+ const status = String(meta.status || parsed?.status || '').trim();
39
+ const fallbackKind = String(text || '').split('\n', 1)[0]?.trim() || 'notification';
40
+ // Distinguish a body-carrying completion from a header-only preview that
41
+ // shares the same id/type/status. An early agent preview can arrive before
42
+ // the session is persisted (no result body); the canonical notification that
43
+ // follows DOES carry the body. Without this dimension the bodyless preview
44
+ // would claim the dedupe key and suppress the real result. A blank-line gap
45
+ // separates the task header block from the result body in the envelope.
46
+ const hasBody = /\n\s*\n[\s\S]*\S/.test(String(text || '')) ? 'b1' : 'b0';
47
+ return [id, type || fallbackKind, status, hasBody].filter(Boolean).join(':');
48
+ }
49
+
50
+ export function isExecutionNotification(event, text, parsed) {
51
+ const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
52
+ if (meta.execution_id || meta.execution_surface) return true;
53
+ if (parseAgentResultEnvelope(text)) return true;
54
+ if (parseBackgroundTaskEnvelope(text)) return true;
55
+ return Boolean(parsed?.taskId && /^(?:agent task:|task_id:)/mi.test(String(text || '')));
56
+ }
57
+
58
+ /** Pure delivery plan for runtime.onNotification execution envelopes (tests + handler). */
59
+ export function resolveTuiRuntimeNotificationDelivery(event, text) {
60
+ const trimmed = String(text ?? '').trim();
61
+ if (!trimmed) return { action: 'ignore' };
62
+ const parsed = parseAgentJob(trimmed);
63
+ const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
64
+ if (!isExecutionNotification(event, trimmed, parsed)) {
65
+ return { action: 'enqueue', displayText: trimmed, modelContent: trimmed };
66
+ }
67
+ if (isStatusOnlyAgentCompletionNotification(trimmed)) {
68
+ return { action: 'status-only', displayText: trimmed, modelContent: '' };
69
+ }
70
+ const modelContent = modelVisibleToolCompletionMessage(trimmed, meta);
71
+ return {
72
+ action: 'execution-ui',
73
+ displayText: trimmed,
74
+ modelContent,
75
+ };
76
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * src/tui/engine/render-timing.mjs - render-throttle timing helper.
3
+ *
4
+ * Extracted from engine.mjs (no behavior change).
5
+ *
6
+ * Ink renders through a maxFps throttle (120fps in index.jsx, ≈8.3ms). A plain
7
+ * setImmediate only yields to the event loop; if Ink already painted within the
8
+ * current throttle window, the next paint may still be queued and our following
9
+ * transcript mutation can coalesce into the same visible frame. Wait just past
10
+ * one render window when we intentionally split transcript commits for visual
11
+ * stability (preamble frame → tool-card frame).
12
+ */
13
+ export const RENDER_THROTTLE_FLUSH_MS = 12;
14
+
15
+ export const yieldToRenderer = () => new Promise((resolve) => {
16
+ setTimeout(resolve, RENDER_THROTTLE_FLUSH_MS);
17
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * src/tui/engine/tool-approval.mjs — tool-approval queue plumbing extracted
3
+ * from createEngineSession (engine.mjs) as a dependency-injection factory.
4
+ *
5
+ * These handlers drive a small FIFO of hook approval requests and reflect the
6
+ * active one into store state (state.toolApproval). They need live session
7
+ * state, so the closure identifiers (state/set/nextId/disposed/timeout) are
8
+ * threaded through the factory argument via getters — never stale snapshots.
9
+ * Every body is the original engine.mjs logic verbatim.
10
+ */
11
+ export function createToolApproval({ getState, set, nextId, getDisposed, timeoutMs }) {
12
+ const toolApprovalQueue = [];
13
+ let activeToolApproval = null;
14
+
15
+ function normalizeToolApprovalRequest(input = {}, id = nextId()) {
16
+ const now = Date.now();
17
+ const requestTimeoutMs = timeoutMs;
18
+ return {
19
+ id,
20
+ name: String(input?.name || input?.tool_name || 'tool'),
21
+ args: input?.args ?? input?.tool_input ?? null,
22
+ cwd: input?.cwd || null,
23
+ sessionId: input?.sessionId || input?.session_id || null,
24
+ toolCallId: input?.toolCallId || input?.tool_use_id || null,
25
+ reason: String(input?.reason || input?.message || 'approval requested by hook').trim(),
26
+ requestedAt: now,
27
+ timeoutMs: requestTimeoutMs,
28
+ expiresAt: now + requestTimeoutMs,
29
+ };
30
+ }
31
+ function presentNextToolApproval() {
32
+ if (activeToolApproval || getDisposed()) return;
33
+ const entry = toolApprovalQueue.shift();
34
+ if (!entry) {
35
+ if (getState().toolApproval) set({ toolApproval: null });
36
+ return;
37
+ }
38
+ activeToolApproval = entry;
39
+ entry.timer = setTimeout(() => {
40
+ finishToolApproval(entry.id, false, 'approval timed out');
41
+ }, entry.request.timeoutMs);
42
+ entry.timer.unref?.();
43
+ set({ toolApproval: entry.request });
44
+ }
45
+ function finishToolApproval(id, approved, reason = '') {
46
+ const targetId = String(id || '');
47
+ if (activeToolApproval && activeToolApproval.id === targetId) {
48
+ const entry = activeToolApproval;
49
+ activeToolApproval = null;
50
+ if (entry.timer) clearTimeout(entry.timer);
51
+ set({ toolApproval: null });
52
+ try { entry.resolve({ approved: approved === true, reason: String(reason || '') }); } catch {}
53
+ presentNextToolApproval();
54
+ return true;
55
+ }
56
+ const index = toolApprovalQueue.findIndex((entry) => entry.id === targetId);
57
+ if (index >= 0) {
58
+ const [entry] = toolApprovalQueue.splice(index, 1);
59
+ if (entry?.timer) clearTimeout(entry.timer);
60
+ try { entry.resolve({ approved: approved === true, reason: String(reason || '') }); } catch {}
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ function denyAllToolApprovals(reason = 'approval cancelled') {
66
+ if (activeToolApproval) {
67
+ const entry = activeToolApproval;
68
+ activeToolApproval = null;
69
+ if (entry.timer) clearTimeout(entry.timer);
70
+ try { entry.resolve({ approved: false, reason }); } catch {}
71
+ }
72
+ while (toolApprovalQueue.length > 0) {
73
+ const entry = toolApprovalQueue.shift();
74
+ if (entry?.timer) clearTimeout(entry.timer);
75
+ try { entry.resolve({ approved: false, reason }); } catch {}
76
+ }
77
+ if (getState().toolApproval) set({ toolApproval: null });
78
+ }
79
+ function requestToolApproval(input = {}) {
80
+ if (getDisposed()) return Promise.resolve({ approved: false, reason: 'runtime disposed' });
81
+ return new Promise((resolve) => {
82
+ const id = nextId();
83
+ toolApprovalQueue.push({ id, request: normalizeToolApprovalRequest(input, id), resolve, timer: null });
84
+ presentNextToolApproval();
85
+ });
86
+ }
87
+
88
+ return {
89
+ presentNextToolApproval,
90
+ finishToolApproval,
91
+ denyAllToolApprovals,
92
+ requestToolApproval,
93
+ };
94
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * src/tui/engine/tool-card-results.mjs — the tool-card result state machine
3
+ * (patchToolCardResult + flushToolResults) extracted from createEngineSession
4
+ * (engine.mjs) as a dependency-injection factory.
5
+ *
6
+ * These handlers own the per-turn accounting that reflects tool results into
7
+ * store items: aggregate cards, non-aggregate/legacy agent-job cards, grouped
8
+ * fallbacks, and the finalize/cancelled sweeps. They mutate live session state,
9
+ * so state/set/patchItem/markToolCallDone/updateAgentJobCard are threaded via
10
+ * the factory argument (getters/callbacks) — never stale snapshots. Every body
11
+ * is the original engine.mjs logic verbatim.
12
+ */
13
+ import { summarizeToolResult } from '../../runtime/shared/tool-surface.mjs';
14
+ import { toolResultText, toolErrorDisplay, toolGroupedDisplayFallback } from './tool-result-text.mjs';
15
+ import { toolResultCallId } from './tool-call-fields.mjs';
16
+ import { parseAgentJob, agentArgsWithResultMetadata, toolResultStatus, isErrorToolStatus } from './agent-envelope.mjs';
17
+ import {
18
+ withCancelledResultMarker,
19
+ groupedToolResultText,
20
+ aggregateRawResult,
21
+ aggregateSummaries,
22
+ assignAggregateSummaryOrder,
23
+ } from './tool-result-status.mjs';
24
+ import { formatAggregateDetail } from '../../runtime/shared/tool-surface.mjs';
25
+
26
+ export function createToolCardResults({
27
+ getState,
28
+ set,
29
+ patchItem,
30
+ markToolCallDone,
31
+ updateAgentJobCard,
32
+ agentStatusState,
33
+ }) {
34
+ function patchToolCardResult(card, message, toolGroups, done) {
35
+ if (!card || card.done) return false;
36
+ const callId = toolResultCallId(message) || card.callId;
37
+ if (callId && done.has(callId)) return false;
38
+ // Any resolving call clears its active-summary entry (keyed by the same
39
+ // callKey used at markToolCallActive; card.callId holds it for both branches).
40
+ markToolCallDone(card.callId);
41
+ // A result for this card arrived (possibly before its deferred push delay
42
+ // elapsed) — surface the card now so the patch below has a live item and the
43
+ // fast tool paints a completed card directly, no pending placeholder stage.
44
+ // ensureVisible flushes this card AND every earlier-created still-deferred
45
+ // card in order, so transcript order always matches call order.
46
+ (card.aggregate?.ensureVisible || card.ensureVisible)?.();
47
+ const rawText = toolResultText(message?.content);
48
+ const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText));
49
+ const text = isError ? toolErrorDisplay(rawText, card?.name || 'tool') : rawText;
50
+
51
+ // Aggregate card handling — collect semantic summaries per call
52
+ const aggregate = card.aggregate;
53
+ if (aggregate && card.itemId === aggregate.itemId) {
54
+ const callRec = callId ? aggregate.calls.get(callId) : null;
55
+ if (!callRec) return false;
56
+ if (callRec.resolved) {
57
+ card.done = true;
58
+ if (callId) done.add(callId);
59
+ return false;
60
+ }
61
+ callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
62
+ assignAggregateSummaryOrder(aggregate, callRec);
63
+ callRec.isError = isError;
64
+ callRec.resultText = text;
65
+ callRec.resolved = true;
66
+ const allCalls = [...aggregate.calls.values()];
67
+ const completed = allCalls.filter((r) => r.resolved).length;
68
+ const errors = allCalls.filter((r) => r.isError).length;
69
+ // Collapsed detail carries the merged per-call count summary
70
+ // ("512 lines, 6 matches, 3 files") so the finished card answers "how
71
+ // much" without ctrl+o. Failures keep a bare 'N Ok · N Failed' status so
72
+ // an error stays visible while collapsed.
73
+ const succeeded = completed - errors;
74
+ const detailText = errors > 0
75
+ ? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
76
+ : formatAggregateDetail(aggregateSummaries(aggregate));
77
+ const currentItem = getState().items.find((it) => it.id === card.itemId);
78
+ const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
79
+ const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
80
+ const rawResult = aggregateRawResult(allCalls);
81
+ // The numbered+labelled raw (rawResult) is preserved for ctrl+o expansion.
82
+ const displayDetail = detailText;
83
+ patchItem(card.itemId, {
84
+ result: displayDetail,
85
+ text: displayDetail,
86
+ rawResult: rawResult || null,
87
+ isError: errors > 0,
88
+ errorCount: errors,
89
+ count: allCalls.length,
90
+ completedCount: visualCompleted,
91
+ completedAt: Number(currentItem?.completedAt) || Date.now(),
92
+ });
93
+ card.done = true;
94
+ if (callId) done.add(callId);
95
+ return true;
96
+ }
97
+
98
+ // Non-aggregate (legacy agent-job cards, etc.)
99
+ const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
100
+ group.completed = Math.min(group.count, group.completed + 1);
101
+ group.errors += isError ? 1 : 0;
102
+ group.results.push({ text, isError });
103
+ toolGroups.set(card.itemId, group);
104
+ const resultText = groupedToolResultText(group);
105
+ const displayResult = toolGroupedDisplayFallback(resultText, text, rawText);
106
+ const patch = {
107
+ result: displayResult,
108
+ text: displayResult,
109
+ isError: group.errors > 0,
110
+ errorCount: group.errors,
111
+ count: group.count,
112
+ completedCount: group.completed,
113
+ completedAt: Date.now(),
114
+ };
115
+ if (group.count <= 1) {
116
+ const body = String(text || rawText || '').trim();
117
+ if (body) patch.rawResult = text || rawText;
118
+ const parsedAgent = parseAgentJob(rawText);
119
+ if (parsedAgent) {
120
+ patch.args = agentArgsWithResultMetadata(getState().items.find((it) => it.id === card.itemId)?.args, parsedAgent);
121
+ set(agentStatusState({ force: true }));
122
+ }
123
+ }
124
+ patchItem(card.itemId, patch);
125
+ if (group.count <= 1) updateAgentJobCard(card.itemId, rawText, isError);
126
+ card.done = true;
127
+ if (callId) done.add(callId);
128
+ return true;
129
+ }
130
+
131
+ const flushToolResults = (messages, toolCards, cardByCallId, toolGroups, done, { finalize = false, cancelled = false } = {}) => {
132
+ const results = [];
133
+ for (const m of messages || []) {
134
+ if (!m || m.role !== 'tool') continue;
135
+ const callId = toolResultCallId(m);
136
+ results.push({ message: m, callId, used: false });
137
+ if (!callId || done.has(callId)) continue;
138
+ const card = cardByCallId.get(callId);
139
+ if (patchToolCardResult(card, m, toolGroups, done)) {
140
+ results[results.length - 1].used = true;
141
+ }
142
+ }
143
+
144
+ const openCards = (toolCards || []).filter((card) => !card.done);
145
+ if (openCards.length === 0) return;
146
+
147
+ const unusedResults = results.filter((result) => !result.used);
148
+ const fallbackResults = unusedResults.slice(-openCards.length);
149
+ for (let i = 0; i < fallbackResults.length; i++) {
150
+ const card = openCards[i];
151
+ const result = fallbackResults[i];
152
+ if (!card || !result || card.done) continue;
153
+ if (patchToolCardResult(card, result.message, toolGroups, done)) {
154
+ if (result.callId) done.add(result.callId);
155
+ result.used = true;
156
+ }
157
+ }
158
+
159
+ if (!finalize) return;
160
+ for (const card of toolCards || []) {
161
+ if (card.done) continue;
162
+ // Finalize must surface any still-deferred card before patching its result
163
+ // so the completed/cancelled card is never silently dropped.
164
+ (card.aggregate?.ensureVisible || card.ensureVisible)?.();
165
+ // Aggregate finalize — mark any remaining calls as done
166
+ const aggregate = card.aggregate;
167
+ if (aggregate && card.itemId === aggregate.itemId) {
168
+ const allCalls = [...aggregate.calls.values()];
169
+ // Never let a call that truly never resolved be presented as a real
170
+ // completion. Stamp it resolved so completedCount reflects an honest
171
+ // (if degenerate) accounting instead of manufacturing success out of
172
+ // a call that never came back. A record already marked completedEarly
173
+ // (via __earlyNotify) already carries a real isError/resultText/summary
174
+ // from its actual result — preserve those; only blank-fill for calls
175
+ // truly never heard from (no completedEarly, no resolved).
176
+ for (const rec of allCalls) {
177
+ if (rec.resolved) continue;
178
+ rec.resolved = true;
179
+ if (!rec.completedEarly) {
180
+ rec.isError = false;
181
+ rec.resultText = rec.resultText || '';
182
+ }
183
+ }
184
+ const completed = allCalls.filter((r) => r.resolved).length;
185
+ const totalCompleted = completed;
186
+ const errors = allCalls.filter((r) => r.isError).length;
187
+ const succeeded = completed - errors;
188
+ const rawResult = aggregateRawResult(allCalls);
189
+ // Collapsed detail carries the merged per-call count summary; failures
190
+ // keep a bare 'N Ok · N Failed' status. Raw is kept for ctrl+o.
191
+ let displayDetail = errors > 0
192
+ ? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
193
+ : formatAggregateDetail(aggregateSummaries(aggregate));
194
+ if (cancelled) {
195
+ // Cancelled aggregates MUST keep the [status: cancelled] marker on the
196
+ // result so terminalStatus parsing resolves to 'cancelled'. Only normal
197
+ // completions drop the summary; cancelled ones prepend the marker.
198
+ const currentItem = getState().items.find((it) => it.id === card.itemId);
199
+ displayDetail = withCancelledResultMarker(displayDetail, currentItem);
200
+ }
201
+ patchItem(card.itemId, {
202
+ result: displayDetail,
203
+ text: displayDetail,
204
+ rawResult: rawResult || null,
205
+ isError: errors > 0,
206
+ errorCount: errors,
207
+ count: allCalls.length,
208
+ completedCount: totalCompleted,
209
+ completedAt: Date.now(),
210
+ });
211
+ for (const sibling of toolCards || []) {
212
+ if (sibling.itemId !== card.itemId) continue;
213
+ sibling.done = true;
214
+ if (sibling.callId) done.add(sibling.callId);
215
+ }
216
+ continue;
217
+ }
218
+ // Non-aggregate finalize
219
+ const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
220
+ group.completed = Math.min(group.count, group.completed + 1);
221
+ toolGroups.set(card.itemId, group);
222
+ let resultText = groupedToolResultText(group);
223
+ if (cancelled) {
224
+ const currentItem = getState().items.find((it) => it.id === card.itemId);
225
+ resultText = withCancelledResultMarker(resultText, currentItem);
226
+ }
227
+ patchItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, count: group.count, completedCount: group.completed, completedAt: Date.now() });
228
+ card.done = true;
229
+ if (card.callId) done.add(card.callId);
230
+ }
231
+ };
232
+
233
+ return { patchToolCardResult, flushToolResults };
234
+ }