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
@@ -19,7 +19,6 @@ import {
19
19
  } from '../runtime/shared/tool-surface.mjs';
20
20
  import {
21
21
  isModelVisibleToolCompletionWrapper,
22
- modelVisibleToolCompletionMessage,
23
22
  } from '../runtime/shared/tool-execution-contract.mjs';
24
23
  import { presentErrorText } from '../runtime/shared/err-text.mjs';
25
24
  import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
@@ -48,13 +47,8 @@ import {
48
47
  toolCallArgs,
49
48
  } from './engine/tool-call-fields.mjs';
50
49
  import {
51
- parseAgentJob,
52
- parseAgentResultEnvelope,
53
50
  parseBackgroundTaskEnvelope,
54
51
  parseSyntheticAgentMessage,
55
- isStatusOnlyAgentCompletionNotification,
56
- agentJobResultText,
57
- agentArgsWithResultMetadata,
58
52
  toolResultStatus,
59
53
  isErrorToolStatus,
60
54
  } from './engine/agent-envelope.mjs';
@@ -64,7 +58,6 @@ import {
64
58
  isQueuedEntryEditable,
65
59
  isQueuedEntryVisible,
66
60
  isSlashQueuedEntry,
67
- shortTextFingerprint,
68
61
  notificationDisplayText,
69
62
  sessionActivityTimestamp,
70
63
  promptDisplayText,
@@ -73,6 +66,19 @@ import {
73
66
  mergePastedTexts,
74
67
  callCommitCallbacks,
75
68
  } from './engine/queue-helpers.mjs';
69
+ import {
70
+ resolveTuiRuntimeNotificationDelivery,
71
+ } from './engine/notification-plan.mjs';
72
+ import { yieldToRenderer } from './engine/render-timing.mjs';
73
+ import {
74
+ aggregateRawResult,
75
+ aggregateBucketForCategory,
76
+ aggregateSummaries,
77
+ assignAggregateSummaryOrder,
78
+ } from './engine/tool-result-status.mjs';
79
+ import { createToolApproval } from './engine/tool-approval.mjs';
80
+ import { createToolCardResults } from './engine/tool-card-results.mjs';
81
+ import { createAgentJobFeed } from './engine/agent-job-feed.mjs';
76
82
 
77
83
  // Source tests resolve from src/tui/engine.mjs; the built bundle resolves from
78
84
  // src/tui/dist/index.mjs.
@@ -92,77 +98,9 @@ const nextId = () => `it_${++_idSeq}`;
92
98
  // keep resolving them from engine.mjs unchanged.
93
99
  export { toolResultText, toolAggregateDetailFallback, toolGroupedDisplayFallback };
94
100
  export { parseBackgroundTaskEnvelope };
95
-
96
- // Ink renders through a maxFps throttle (120fps in index.jsx, ≈8.3ms). A plain
97
- // setImmediate only yields to the event loop; if Ink already painted within the
98
- // current throttle window, the next paint may still be queued and our following
99
- // transcript mutation can coalesce into the same visible frame. Wait just past
100
- // one render window when we intentionally split transcript commits for visual
101
- // stability (preamble frame → tool-card frame).
102
- const RENDER_THROTTLE_FLUSH_MS = 12;
103
- const yieldToRenderer = () => new Promise((resolve) => {
104
- setTimeout(resolve, RENDER_THROTTLE_FLUSH_MS);
105
- });
106
-
107
- function notificationQueueKey(event, text, parsed) {
108
- const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
109
- const synthetic = parseSyntheticAgentMessage(text);
110
- if (synthetic?.name === 'agent' && String(synthetic.args?.type || '').toLowerCase() === 'result') {
111
- const taskId = String(synthetic.args?.task_id || '').trim();
112
- const executionId = String(meta.execution_id || '').trim();
113
- const tag = String(synthetic.args?.tag || '').trim();
114
- const resultId = taskId
115
- ? `task:${taskId}`
116
- : executionId
117
- ? `exec:${executionId}`
118
- : tag
119
- ? `tag:${tag}:${shortTextFingerprint(synthetic.result || text)}`
120
- : '';
121
- const agent = String(synthetic.args?.agent || '').trim();
122
- if (resultId || agent) return ['agent-result', resultId, agent].filter(Boolean).join(':');
123
- }
124
- const id = String(meta.execution_id || parsed?.taskId || '').trim();
125
- if (!id) return '';
126
- const type = String(meta.type || '').trim();
127
- const status = String(meta.status || parsed?.status || '').trim();
128
- const fallbackKind = String(text || '').split('\n', 1)[0]?.trim() || 'notification';
129
- // Distinguish a body-carrying completion from a header-only preview that
130
- // shares the same id/type/status. An early agent preview can arrive before
131
- // the session is persisted (no result body); the canonical notification that
132
- // follows DOES carry the body. Without this dimension the bodyless preview
133
- // would claim the dedupe key and suppress the real result. A blank-line gap
134
- // separates the task header block from the result body in the envelope.
135
- const hasBody = /\n\s*\n[\s\S]*\S/.test(String(text || '')) ? 'b1' : 'b0';
136
- return [id, type || fallbackKind, status, hasBody].filter(Boolean).join(':');
137
- }
138
-
139
- /** Pure delivery plan for runtime.onNotification execution envelopes (tests + handler). */
140
- export function resolveTuiRuntimeNotificationDelivery(event, text) {
141
- const trimmed = String(text ?? '').trim();
142
- if (!trimmed) return { action: 'ignore' };
143
- const parsed = parseAgentJob(trimmed);
144
- const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
145
- if (!isExecutionNotification(event, trimmed, parsed)) {
146
- return { action: 'enqueue', displayText: trimmed, modelContent: trimmed };
147
- }
148
- if (isStatusOnlyAgentCompletionNotification(trimmed)) {
149
- return { action: 'status-only', displayText: trimmed, modelContent: '' };
150
- }
151
- const modelContent = modelVisibleToolCompletionMessage(trimmed, meta);
152
- return {
153
- action: 'execution-ui',
154
- displayText: trimmed,
155
- modelContent,
156
- };
157
- }
158
-
159
- function isExecutionNotification(event, text, parsed) {
160
- const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
161
- if (meta.execution_id || meta.execution_surface) return true;
162
- if (parseAgentResultEnvelope(text)) return true;
163
- if (parseBackgroundTaskEnvelope(text)) return true;
164
- return Boolean(parsed?.taskId && /^(?:agent task:|task_id:)/mi.test(String(text || '')));
165
- }
101
+ // Re-export the pure notification delivery plan (moved to ./engine/notification-plan.mjs)
102
+ // so importers/tests keep resolving resolveTuiRuntimeNotificationDelivery from engine.mjs.
103
+ export { resolveTuiRuntimeNotificationDelivery };
166
104
 
167
105
  export async function createEngineSession({
168
106
  provider: providerName,
@@ -187,7 +125,7 @@ export async function createEngineSession({
187
125
  bootProfile('engine:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
188
126
  const cwd = runtime.cwd || process.cwd();
189
127
  const stateStartedAt = performance.now();
190
- const autoClearState = () => runtime.getAutoClear?.() || runtime.autoClear || { enabled: true, idleMs: 60 * 60 * 1000 };
128
+ const autoClearState = () => runtime.getAutoClear?.() || runtime.autoClear || { enabled: true, idleMs: 60 * 60 * 1000, custom: false, providerDefault: 60 * 60 * 1000, provider: null };
191
129
  const AGENT_STATUS_CACHE_MS = 250;
192
130
  let agentStatusCache = null;
193
131
  let agentStatusCacheAt = 0;
@@ -535,6 +473,16 @@ export async function createEngineSession({
535
473
  pushItem({ kind: 'notice', id, text: value, tone });
536
474
  return id;
537
475
  };
476
+ // Remove a transcript notice previously created via pushNotice(...,
477
+ // {transcript:true}). Used for transient-but-persistent notices (e.g. the
478
+ // manual OAuth URL) that must disappear once their flow concludes.
479
+ const removeNotice = (id) => {
480
+ if (id == null) return false;
481
+ const items = state.items.filter((it) => !(it?.kind === 'notice' && it?.id === id));
482
+ if (items.length === state.items.length) return false;
483
+ set({ items: replaceItems(items) });
484
+ return true;
485
+ };
538
486
  // Sticky (non-TTL) input-hint-line progress state, for long-running
539
487
  // installs (e.g. voice runtime download) that would otherwise spam the
540
488
  // 3s toast queue. Distinct from pushToast/pushNotice: it persists across
@@ -545,80 +493,18 @@ export async function createEngineSession({
545
493
  const value = String(text ?? '').trim();
546
494
  set({ progressHint: value ? { text: value, tone } : null });
547
495
  };
548
- const toolApprovalQueue = [];
549
- let activeToolApproval = null;
550
- function normalizeToolApprovalRequest(input = {}, id = nextId()) {
551
- const now = Date.now();
552
- const timeoutMs = TOOL_APPROVAL_TIMEOUT_MS;
553
- return {
554
- id,
555
- name: String(input?.name || input?.tool_name || 'tool'),
556
- args: input?.args ?? input?.tool_input ?? null,
557
- cwd: input?.cwd || null,
558
- sessionId: input?.sessionId || input?.session_id || null,
559
- toolCallId: input?.toolCallId || input?.tool_use_id || null,
560
- reason: String(input?.reason || input?.message || 'approval requested by hook').trim(),
561
- requestedAt: now,
562
- timeoutMs,
563
- expiresAt: now + timeoutMs,
564
- };
565
- }
566
- function presentNextToolApproval() {
567
- if (activeToolApproval || disposed) return;
568
- const entry = toolApprovalQueue.shift();
569
- if (!entry) {
570
- if (state.toolApproval) set({ toolApproval: null });
571
- return;
572
- }
573
- activeToolApproval = entry;
574
- entry.timer = setTimeout(() => {
575
- finishToolApproval(entry.id, false, 'approval timed out');
576
- }, entry.request.timeoutMs);
577
- entry.timer.unref?.();
578
- set({ toolApproval: entry.request });
579
- }
580
- function finishToolApproval(id, approved, reason = '') {
581
- const targetId = String(id || '');
582
- if (activeToolApproval && activeToolApproval.id === targetId) {
583
- const entry = activeToolApproval;
584
- activeToolApproval = null;
585
- if (entry.timer) clearTimeout(entry.timer);
586
- set({ toolApproval: null });
587
- try { entry.resolve({ approved: approved === true, reason: String(reason || '') }); } catch {}
588
- presentNextToolApproval();
589
- return true;
590
- }
591
- const index = toolApprovalQueue.findIndex((entry) => entry.id === targetId);
592
- if (index >= 0) {
593
- const [entry] = toolApprovalQueue.splice(index, 1);
594
- if (entry?.timer) clearTimeout(entry.timer);
595
- try { entry.resolve({ approved: approved === true, reason: String(reason || '') }); } catch {}
596
- return true;
597
- }
598
- return false;
599
- }
600
- function denyAllToolApprovals(reason = 'approval cancelled') {
601
- if (activeToolApproval) {
602
- const entry = activeToolApproval;
603
- activeToolApproval = null;
604
- if (entry.timer) clearTimeout(entry.timer);
605
- try { entry.resolve({ approved: false, reason }); } catch {}
606
- }
607
- while (toolApprovalQueue.length > 0) {
608
- const entry = toolApprovalQueue.shift();
609
- if (entry?.timer) clearTimeout(entry.timer);
610
- try { entry.resolve({ approved: false, reason }); } catch {}
611
- }
612
- if (state.toolApproval) set({ toolApproval: null });
613
- }
614
- function requestToolApproval(input = {}) {
615
- if (disposed) return Promise.resolve({ approved: false, reason: 'runtime disposed' });
616
- return new Promise((resolve) => {
617
- const id = nextId();
618
- toolApprovalQueue.push({ id, request: normalizeToolApprovalRequest(input, id), resolve, timer: null });
619
- presentNextToolApproval();
620
- });
621
- }
496
+ const {
497
+ presentNextToolApproval,
498
+ finishToolApproval,
499
+ denyAllToolApprovals,
500
+ requestToolApproval,
501
+ } = createToolApproval({
502
+ getState: () => state,
503
+ set,
504
+ nextId,
505
+ getDisposed: () => disposed,
506
+ timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
507
+ });
622
508
  const patchItem = (id, patch) => {
623
509
  let index = itemIndexById.get(id);
624
510
  if (!Number.isInteger(index) || state.items[index]?.id !== id) {
@@ -666,404 +552,51 @@ export async function createEngineSession({
666
552
  let autoClearRunning = false;
667
553
  const pendingNotificationKeys = new Set();
668
554
  const displayedExecutionNotificationKeys = new Set();
669
- let executionResumeKickDeferred = false;
670
-
671
- function kickExecutionPendingResume() {
672
- if (disposed) return;
673
- if (state.busy) {
674
- executionResumeKickDeferred = true;
675
- return;
676
- }
677
- if (pending.some((entry) => entry.mode === 'pending-resume')) {
678
- executionResumeKickDeferred = true;
679
- return;
680
- }
681
- executionResumeKickDeferred = false;
682
- pending.push(makeQueueEntry('', { mode: 'pending-resume', priority: 'next' }));
683
- void drain();
684
- }
685
-
686
- function flushDeferredExecutionPendingResumeKick() {
687
- if (!executionResumeKickDeferred || disposed || state.busy) return;
688
- kickExecutionPendingResume();
689
- }
690
-
691
- function scheduleExecutionPendingResumeKick() {
692
- // notifyFnForSession enqueues the model-visible body after onNotification
693
- // returns; defer the kick so askSession pre-drain sees session pending.
694
- queueMicrotask(() => kickExecutionPendingResume());
695
- }
696
-
697
- function updateAgentJobCard(itemId, text, isError = false) {
698
- const parsed = parseAgentJob(text);
699
- const current = state.items.find((it) => it.id === itemId);
700
- const rawDisplayText = agentJobResultText(text, parsed) || String(text ?? '').trim();
701
- const displayText = isError ? toolErrorDisplay(rawDisplayText, 'agent') : rawDisplayText;
702
- patchItem(itemId, {
703
- result: displayText,
704
- text: displayText,
705
- isError,
706
- errorCount: isError ? 1 : 0,
707
- ...(parsed ? { args: agentArgsWithResultMetadata(current?.args, parsed) } : {}),
708
- });
709
- }
710
-
711
- if (typeof runtime.onNotification === 'function') {
712
- unsubscribeRuntimeNotifications = runtime.onNotification((event) => {
555
+ const {
556
+ kickExecutionPendingResume,
557
+ flushDeferredExecutionPendingResumeKick,
558
+ scheduleExecutionPendingResumeKick,
559
+ updateAgentJobCard,
560
+ subscribeRuntimeNotifications,
561
+ } = createAgentJobFeed({
562
+ runtime,
563
+ getState: () => state,
564
+ set,
565
+ nextId,
566
+ getDisposed: () => disposed,
567
+ patchItem,
568
+ enqueue: (...args) => enqueue(...args),
569
+ drain: (...args) => drain(...args),
570
+ pushUserOrSyntheticItem,
571
+ makeQueueEntry: (...args) => makeQueueEntry(...args),
572
+ getPending: () => pending,
573
+ agentStatusState,
574
+ displayedExecutionNotificationKeys,
575
+ });
576
+ unsubscribeRuntimeNotifications = subscribeRuntimeNotifications();
577
+
578
+ // Remote seat superseded by another session: runtime already stopped its
579
+ // worker; sync the indicator and tell the user. Non-user-initiated, so a
580
+ // toast (not transcript) is right.
581
+ let unsubscribeRemoteState = null;
582
+ if (typeof runtime.onRemoteStateChange === 'function') {
583
+ unsubscribeRemoteState = runtime.onRemoteStateChange(({ enabled, reason }) => {
713
584
  if (disposed) return;
714
- const text = String(event?.content ?? event?.text ?? event ?? '').trim();
715
- if (!text) return;
716
- const parsed = parseAgentJob(text);
717
- const notificationKey = notificationQueueKey(event, text, parsed);
718
- const delivery = resolveTuiRuntimeNotificationDelivery(event, text);
719
- if (delivery.action === 'ignore') return;
720
- if (delivery.action === 'status-only') {
721
- if (parsed?.taskId) set(agentStatusState({ force: true }));
722
- return true;
723
- }
724
- if (delivery.action === 'execution-ui') {
725
- const firstDelivery = !notificationKey || !displayedExecutionNotificationKeys.has(notificationKey);
726
- if (firstDelivery) {
727
- if (notificationKey) displayedExecutionNotificationKeys.add(notificationKey);
728
- pushUserOrSyntheticItem(delivery.displayText, nextId());
729
- }
730
- if (parsed?.taskId) set(agentStatusState({ force: true }));
731
- if (String(delivery.modelContent || '').trim()) {
732
- scheduleExecutionPendingResumeKick();
733
- }
734
- return true;
735
- }
736
- if (parsed?.taskId) {
737
- set(agentStatusState({ force: true }));
585
+ set({ remoteEnabled: enabled === true });
586
+ if (reason === 'superseded') {
587
+ pushNotice('Remote mode OFF — another session took over remote.', 'warn');
738
588
  }
739
- const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
740
- if (!modelContent) return true;
741
- enqueue(modelContent, {
742
- mode: 'task-notification',
743
- priority: 'next',
744
- key: notificationKey || undefined,
745
- displayText: delivery.displayText || text,
746
- });
747
- return true;
748
589
  });
749
590
  }
750
591
 
751
- const CANCELLED_RESULT_STATUS_LINE = '[status: cancelled]';
752
-
753
- function normalizedResultStatusToken(value) {
754
- const raw = String(value || '').trim().toLowerCase();
755
- if (!raw) return '';
756
- if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
757
- if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
758
- if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
759
- if (/^(cancelled|canceled|cancel)$/.test(raw)) return 'cancelled';
760
- return '';
761
- }
762
-
763
- function resultTextTerminalStatus(text) {
764
- const body = String(text || '');
765
- const tagged = body.match(/<status[^>]*>([\s\S]*?)<\/status>/i)?.[1]?.trim();
766
- if (tagged) return normalizedResultStatusToken(tagged);
767
- const bracketed = body.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
768
- if (bracketed) return normalizedResultStatusToken(bracketed);
769
- const inline = body.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
770
- return normalizedResultStatusToken(inline);
771
- }
772
-
773
- function itemHasKnownTerminalStatus(item, texts = []) {
774
- const settled = (token) => token === 'completed' || token === 'failed' || token === 'cancelled';
775
- if (settled(normalizedResultStatusToken(item?.args?.status))) return true;
776
- for (const text of texts) {
777
- if (settled(resultTextTerminalStatus(text))) return true;
778
- }
779
- return false;
780
- }
781
-
782
- function withCancelledResultMarker(text, item) {
783
- const body = String(text || '');
784
- // Do NOT inspect item.rawResult here: aggregate rawResult is child tool
785
- // output (`1. grep\n<result>…`) that can incidentally contain a `status:`
786
- // line, which would false-positive as an already-terminal status and skip
787
- // the cancelled marker. Only result/text/body are engine-controlled
788
- // collapsed detail (empty / status word / an existing marker), so they are
789
- // the trustworthy terminal-status sources.
790
- const sources = [item?.result, item?.text, body];
791
- if (itemHasKnownTerminalStatus(item, sources)) return body;
792
- if (!body.trim()) return `${CANCELLED_RESULT_STATUS_LINE}\n`;
793
- return `${CANCELLED_RESULT_STATUS_LINE}\n${body}`;
794
- }
795
-
796
- function groupedToolResultText(group) {
797
- const completed = Math.min(group.count, group.completed);
798
- if (group.count <= 1) return group.results.at(-1)?.text ?? '';
799
- if (group.errors > 0) {
800
- const succeeded = Math.max(0, completed - group.errors);
801
- const reasons = group.results
802
- .filter((result) => result?.isError)
803
- .map((result) => firstErrorLine(result?.text))
804
- .filter(Boolean);
805
- const uniqueReasons = [...new Set(reasons)].slice(0, 2);
806
- const base = succeeded > 0
807
- ? `${succeeded} Ok · ${group.errors} Failed`
808
- : `${group.errors} Failed`;
809
- return [
810
- `${base}${uniqueReasons[0] ? ` · ${uniqueReasons[0]}` : ''}`,
811
- ...uniqueReasons.slice(1),
812
- ].join('\n');
813
- }
814
- for (const result of group.results || []) {
815
- const line = String(result?.text || '').trim();
816
- if (line) return result.text;
817
- }
818
- return '';
819
- }
820
-
821
- function firstErrorLine(text) {
822
- const clean = toolErrorDisplay(text, 'tool');
823
- if (clean) return clean;
824
- for (const line of String(text || '').split('\n')) {
825
- const trimmed = line.trim();
826
- if (!trimmed) continue;
827
- if (/^(Error|\[?error|FAIL\b)/i.test(trimmed)) return trimmed;
828
- }
829
- return String(text || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
830
- }
831
-
832
- function aggregateRawResult(calls) {
833
- const chunks = [];
834
- for (const rec of calls || []) {
835
- if (rec?.resolved !== true) continue;
836
- let text = String(rec?.resultText || '').replace(/\s+$/, '');
837
- if (!text.trim()) continue;
838
- const label = String(rec?.name || rec?.category || 'tool').trim() || 'tool';
839
- chunks.push(`${chunks.length + 1}. ${label}\n${text}`);
840
- }
841
- return chunks.join('\n\n');
842
- }
843
-
844
- function aggregateBucketForCategory(category) {
845
- // Merge consecutive tool calls of the SAME category into one aggregate card;
846
- // a different category opens a fresh card (no cross-category merge). The
847
- // bucket key is the category itself, so a run of Search calls collapses into
848
- // one Search card while an adjacent Read/Patch stays separate. Falls back to
849
- // 'default' when a call has no resolved category. Hook/approval denials keep
850
- // their dedicated ToolHookDenialCard path in App.jsx.
851
- const key = String(category || '').trim();
852
- return key ? `category:${key}` : 'default';
853
- }
854
-
855
- function aggregateSummaries(aggregate) {
856
- return [...(aggregate?.calls?.values?.() || [])]
857
- .filter((r) => r.summary)
858
- .sort((a, b) => Number(a.summarySeq ?? 0) - Number(b.summarySeq ?? 0))
859
- .map((r) => r.summary);
860
- }
861
-
862
- function assignAggregateSummaryOrder(aggregate, callRec) {
863
- if (!aggregate || !callRec?.summary || callRec.summarySeq != null) return;
864
- const next = Math.max(0, Number(aggregate.nextSummarySeq || 0));
865
- callRec.summarySeq = next;
866
- aggregate.nextSummarySeq = next + 1;
867
- }
868
-
869
- function patchToolCardResult(card, message, toolGroups, done) {
870
- if (!card || card.done) return false;
871
- const callId = toolResultCallId(message) || card.callId;
872
- if (callId && done.has(callId)) return false;
873
- // Any resolving call clears its active-summary entry (keyed by the same
874
- // callKey used at markToolCallActive; card.callId holds it for both branches).
875
- markToolCallDone(card.callId);
876
- // A result for this card arrived (possibly before its deferred push delay
877
- // elapsed) — surface the card now so the patch below has a live item and the
878
- // fast tool paints a completed card directly, no pending placeholder stage.
879
- // ensureVisible flushes this card AND every earlier-created still-deferred
880
- // card in order, so transcript order always matches call order.
881
- (card.aggregate?.ensureVisible || card.ensureVisible)?.();
882
- const rawText = toolResultText(message?.content);
883
- const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText));
884
- const text = isError ? toolErrorDisplay(rawText, card?.name || 'tool') : rawText;
885
-
886
- // Aggregate card handling — collect semantic summaries per call
887
- const aggregate = card.aggregate;
888
- if (aggregate && card.itemId === aggregate.itemId) {
889
- const callRec = callId ? aggregate.calls.get(callId) : null;
890
- if (!callRec) return false;
891
- if (callRec.resolved) {
892
- card.done = true;
893
- if (callId) done.add(callId);
894
- return false;
895
- }
896
- callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
897
- assignAggregateSummaryOrder(aggregate, callRec);
898
- callRec.isError = isError;
899
- callRec.resultText = text;
900
- callRec.resolved = true;
901
- const allCalls = [...aggregate.calls.values()];
902
- const completed = allCalls.filter((r) => r.resolved).length;
903
- const errors = allCalls.filter((r) => r.isError).length;
904
- // Collapsed detail carries the merged per-call count summary
905
- // ("512 lines, 6 matches, 3 files") so the finished card answers "how
906
- // much" without ctrl+o. Failures keep a bare 'N Ok · N Failed' status so
907
- // an error stays visible while collapsed.
908
- const succeeded = completed - errors;
909
- const detailText = errors > 0
910
- ? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
911
- : formatAggregateDetail(aggregateSummaries(aggregate));
912
- const currentItem = state.items.find((it) => it.id === card.itemId);
913
- const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
914
- const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
915
- const rawResult = aggregateRawResult(allCalls);
916
- // The numbered+labelled raw (rawResult) is preserved for ctrl+o expansion.
917
- const displayDetail = detailText;
918
- patchItem(card.itemId, {
919
- result: displayDetail,
920
- text: displayDetail,
921
- rawResult: rawResult || null,
922
- isError: errors > 0,
923
- errorCount: errors,
924
- count: allCalls.length,
925
- completedCount: visualCompleted,
926
- completedAt: Number(currentItem?.completedAt) || Date.now(),
927
- });
928
- card.done = true;
929
- if (callId) done.add(callId);
930
- return true;
931
- }
932
-
933
- // Non-aggregate (legacy agent-job cards, etc.)
934
- const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
935
- group.completed = Math.min(group.count, group.completed + 1);
936
- group.errors += isError ? 1 : 0;
937
- group.results.push({ text, isError });
938
- toolGroups.set(card.itemId, group);
939
- const resultText = groupedToolResultText(group);
940
- const displayResult = toolGroupedDisplayFallback(resultText, text, rawText);
941
- const patch = {
942
- result: displayResult,
943
- text: displayResult,
944
- isError: group.errors > 0,
945
- errorCount: group.errors,
946
- count: group.count,
947
- completedCount: group.completed,
948
- completedAt: Date.now(),
949
- };
950
- if (group.count <= 1) {
951
- const body = String(text || rawText || '').trim();
952
- if (body) patch.rawResult = text || rawText;
953
- const parsedAgent = parseAgentJob(rawText);
954
- if (parsedAgent) {
955
- patch.args = agentArgsWithResultMetadata(state.items.find((it) => it.id === card.itemId)?.args, parsedAgent);
956
- set(agentStatusState({ force: true }));
957
- }
958
- }
959
- patchItem(card.itemId, patch);
960
- if (group.count <= 1) updateAgentJobCard(card.itemId, rawText, isError);
961
- card.done = true;
962
- if (callId) done.add(callId);
963
- return true;
964
- }
965
-
966
- const flushToolResults = (messages, toolCards, cardByCallId, toolGroups, done, { finalize = false, cancelled = false } = {}) => {
967
- const results = [];
968
- for (const m of messages || []) {
969
- if (!m || m.role !== 'tool') continue;
970
- const callId = toolResultCallId(m);
971
- results.push({ message: m, callId, used: false });
972
- if (!callId || done.has(callId)) continue;
973
- const card = cardByCallId.get(callId);
974
- if (patchToolCardResult(card, m, toolGroups, done)) {
975
- results[results.length - 1].used = true;
976
- }
977
- }
978
-
979
- const openCards = (toolCards || []).filter((card) => !card.done);
980
- if (openCards.length === 0) return;
981
-
982
- const unusedResults = results.filter((result) => !result.used);
983
- const fallbackResults = unusedResults.slice(-openCards.length);
984
- for (let i = 0; i < fallbackResults.length; i++) {
985
- const card = openCards[i];
986
- const result = fallbackResults[i];
987
- if (!card || !result || card.done) continue;
988
- if (patchToolCardResult(card, result.message, toolGroups, done)) {
989
- if (result.callId) done.add(result.callId);
990
- result.used = true;
991
- }
992
- }
993
-
994
- if (!finalize) return;
995
- for (const card of toolCards || []) {
996
- if (card.done) continue;
997
- // Finalize must surface any still-deferred card before patching its result
998
- // so the completed/cancelled card is never silently dropped.
999
- (card.aggregate?.ensureVisible || card.ensureVisible)?.();
1000
- // Aggregate finalize — mark any remaining calls as done
1001
- const aggregate = card.aggregate;
1002
- if (aggregate && card.itemId === aggregate.itemId) {
1003
- const allCalls = [...aggregate.calls.values()];
1004
- // Never let a call that truly never resolved be presented as a real
1005
- // completion. Stamp it resolved so completedCount reflects an honest
1006
- // (if degenerate) accounting instead of manufacturing success out of
1007
- // a call that never came back. A record already marked completedEarly
1008
- // (via __earlyNotify) already carries a real isError/resultText/summary
1009
- // from its actual result — preserve those; only blank-fill for calls
1010
- // truly never heard from (no completedEarly, no resolved).
1011
- for (const rec of allCalls) {
1012
- if (rec.resolved) continue;
1013
- rec.resolved = true;
1014
- if (!rec.completedEarly) {
1015
- rec.isError = false;
1016
- rec.resultText = rec.resultText || '';
1017
- }
1018
- }
1019
- const completed = allCalls.filter((r) => r.resolved).length;
1020
- const totalCompleted = completed;
1021
- const errors = allCalls.filter((r) => r.isError).length;
1022
- const succeeded = completed - errors;
1023
- const rawResult = aggregateRawResult(allCalls);
1024
- // Collapsed detail carries the merged per-call count summary; failures
1025
- // keep a bare 'N Ok · N Failed' status. Raw is kept for ctrl+o.
1026
- let displayDetail = errors > 0
1027
- ? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
1028
- : formatAggregateDetail(aggregateSummaries(aggregate));
1029
- if (cancelled) {
1030
- // Cancelled aggregates MUST keep the [status: cancelled] marker on the
1031
- // result so terminalStatus parsing resolves to 'cancelled'. Only normal
1032
- // completions drop the summary; cancelled ones prepend the marker.
1033
- const currentItem = state.items.find((it) => it.id === card.itemId);
1034
- displayDetail = withCancelledResultMarker(displayDetail, currentItem);
1035
- }
1036
- patchItem(card.itemId, {
1037
- result: displayDetail,
1038
- text: displayDetail,
1039
- rawResult: rawResult || null,
1040
- isError: errors > 0,
1041
- errorCount: errors,
1042
- count: allCalls.length,
1043
- completedCount: totalCompleted,
1044
- completedAt: Date.now(),
1045
- });
1046
- for (const sibling of toolCards || []) {
1047
- if (sibling.itemId !== card.itemId) continue;
1048
- sibling.done = true;
1049
- if (sibling.callId) done.add(sibling.callId);
1050
- }
1051
- continue;
1052
- }
1053
- // Non-aggregate finalize
1054
- const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
1055
- group.completed = Math.min(group.count, group.completed + 1);
1056
- toolGroups.set(card.itemId, group);
1057
- let resultText = groupedToolResultText(group);
1058
- if (cancelled) {
1059
- const currentItem = state.items.find((it) => it.id === card.itemId);
1060
- resultText = withCancelledResultMarker(resultText, currentItem);
1061
- }
1062
- patchItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, count: group.count, completedCount: group.completed, completedAt: Date.now() });
1063
- card.done = true;
1064
- if (card.callId) done.add(card.callId);
1065
- }
1066
- };
592
+ const { patchToolCardResult, flushToolResults } = createToolCardResults({
593
+ getState: () => state,
594
+ set,
595
+ patchItem,
596
+ markToolCallDone,
597
+ updateAgentJobCard,
598
+ agentStatusState,
599
+ });
1067
600
 
1068
601
  async function runTurn(userText, options = {}) {
1069
602
  const turnIndex = state.stats.turns || 0;
@@ -1105,7 +638,7 @@ export async function createEngineSession({
1105
638
  // cards (send() still in flight). Hold those by callId until the batch lands.
1106
639
  const earlyResultBuffer = new Map();
1107
640
  const aggregateCards = []; // active aggregate cards in the current consecutive tool block
1108
- const aggregateByBucket = new Map(); // per-bucket reusable card for the current consecutive tool block; cleared on block seal
641
+ let tailAggregate = null; // most recently touched aggregate card; only the tail may absorb the next same-bucket call
1109
642
 
1110
643
  // ── Deferred tool-card push (scroll/text sync) ────────────────────────────
1111
644
  // A tool card used to enter the transcript the instant onToolCall fired,
@@ -1264,28 +797,25 @@ export async function createEngineSession({
1264
797
  aggregateCards.length = 0;
1265
798
  // Seal the block: same-bucket calls after this point must open a fresh
1266
799
  // card, never continue one from before the seal (assistant text/turn
1267
- // end boundary). Cross-block behavior is unchanged by the in-block
1268
- // reuse relaxation below.
1269
- aggregateByBucket.clear();
800
+ // end boundary).
801
+ tailAggregate = null;
1270
802
  };
1271
803
 
1272
804
  const rememberActiveAggregate = (aggregate) => {
1273
805
  if (!aggregate) return;
1274
806
  if (!aggregateCards.includes(aggregate)) aggregateCards.push(aggregate);
1275
- aggregateByBucket.set(aggregate.bucket, aggregate);
807
+ tailAggregate = aggregate;
1276
808
  };
1277
809
 
1278
810
  const ensureAggregateCard = (bucket) => {
1279
- // Reuse any same-bucket aggregate created earlier in the SAME consecutive
1280
- // tool block, even when a different-category card was interleaved after
1281
- // it (Read, Search, Read the two Reads merge into one card; the Search
1282
- // stays separate). The block is only sealedforcing a fresh card per
1283
- // bucket by clearAggregateContinuation (assistant text lands or the
1284
- // turn ends), which clears aggregateByBucket. Reusing a pushed, non-tail
1285
- // card is safe: the aggregate card body is a fixed header+1-detail-row
1286
- // height, so patching its count/completedCount later never reflows the
1287
- // cards that were pushed after it.
1288
- const cached = aggregateByBucket.get(bucket);
811
+ // Only the TAIL aggregate (most recent card) may absorb the next call,
812
+ // and only when the bucket matches. Any different-bucket aggregate or
813
+ // standalone card in between breaks the run, so Search, Memory, Search
814
+ // renders as three cards in call order — a new call never merges into
815
+ // an earlier card above the current tail (which read as out-of-order
816
+ // count changes in the transcript). clearAggregateContinuation seals
817
+ // the block at assistant-text/turn boundaries.
818
+ const cached = tailAggregate && tailAggregate.bucket === bucket ? tailAggregate : null;
1289
819
  if (cached) {
1290
820
  rememberActiveAggregate(cached);
1291
821
  return cached;
@@ -1632,7 +1162,11 @@ export async function createEngineSession({
1632
1162
  // Category drives the aggregate bucket so only same-category calls
1633
1163
  // merge into one card; classify first, then bucket by it.
1634
1164
  const category = classifyToolCategory(name, args);
1635
- const bucket = aggregateBucketForCategory(category);
1165
+ // Agent/bridge calls always get their own standalone card so
1166
+ // ToolExecution's isAgentTool/agentActionTitle path renders a
1167
+ // "Spawn <Agent> (model, tag)" header instead of being folded
1168
+ // into the "Called N agent(s)" category-aggregate card.
1169
+ const bucket = category === 'Agent' ? null : aggregateBucketForCategory(category);
1636
1170
  const callId = toolCallId(c);
1637
1171
  const callKey = callId || `__tool_${toolCards.length}_${i}`;
1638
1172
  // The old App scan counted multi-pattern calls via
@@ -1673,6 +1207,10 @@ export async function createEngineSession({
1673
1207
  cardByCallId.set(callId, card);
1674
1208
  }
1675
1209
  toolCards.push(card);
1210
+ // A standalone card (Agent) breaks the consecutive run too: a
1211
+ // later same-bucket call must open a fresh card BELOW it, not
1212
+ // merge into an aggregate above it.
1213
+ tailAggregate = null;
1676
1214
  continue;
1677
1215
  }
1678
1216
 
@@ -2856,6 +2394,16 @@ export async function createEngineSession({
2856
2394
  set({ remoteEnabled: next });
2857
2395
  return next;
2858
2396
  },
2397
+ // Force-claim remote for this session (single-holder, last-wins). Always
2398
+ // turns remote ON here and steals the bridge seat; the previous holder is
2399
+ // superseded and flips itself OFF via onRemoteStateChange. Used by the
2400
+ // `/remote` slash command — repeated /remote just re-claims (idempotent).
2401
+ claimRemote: () => {
2402
+ runtime.startRemote?.();
2403
+ const next = runtime.isRemoteEnabled?.() === true;
2404
+ set({ remoteEnabled: next });
2405
+ return next;
2406
+ },
2859
2407
  isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
2860
2408
  // Theme is a TUI-local concern (no runtime round-trip). listThemes returns
2861
2409
  // picker metadata; getTheme reports the active id; setTheme applies the
@@ -2938,6 +2486,15 @@ export async function createEngineSession({
2938
2486
  'info');
2939
2487
  return true;
2940
2488
  },
2489
+ loginOpenCodeGoUsage: async () => {
2490
+ if (state.commandBusy) throw new Error('command busy');
2491
+ set({ commandBusy: true });
2492
+ try {
2493
+ return await runtime.loginOpenCodeGoUsage();
2494
+ } finally {
2495
+ set({ commandBusy: false });
2496
+ }
2497
+ },
2941
2498
  saveOpenAIUsageSessionKey: (secret) => {
2942
2499
  runtime.saveOpenAIUsageSessionKey(secret);
2943
2500
  pushNotice('OpenAI usage auth saved', 'info');
@@ -3062,6 +2619,7 @@ export async function createEngineSession({
3062
2619
  }
3063
2620
  },
3064
2621
  pushNotice,
2622
+ removeNotice,
3065
2623
  setProgressHint,
3066
2624
  clear: async () => {
3067
2625
  if (state.commandBusy) return false;
@@ -3199,6 +2757,8 @@ export async function createEngineSession({
3199
2757
  try { clearInterval(runtimePulseTimer); } catch {}
3200
2758
  try { unsubscribeRuntimeNotifications?.(); } catch {}
3201
2759
  unsubscribeRuntimeNotifications = null;
2760
+ try { unsubscribeRemoteState?.(); } catch {}
2761
+ unsubscribeRemoteState = null;
3202
2762
  denyAllToolApprovals('runtime closing');
3203
2763
  await runtime.close(reason, options);
3204
2764
  listeners.clear();