mixdog 0.9.3 → 0.9.4

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 (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. 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
+ }