mixdog 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,607 @@
1
+ /**
2
+ * use-transcript-window.mjs — transcript row-index/window memo chain + the
3
+ * measured-height harvest and reading-anchor lock effects.
4
+ *
5
+ * Extracted verbatim from App.jsx: structure-signature/row-index/render-window
6
+ * memos, the same-frame anchor lock + capture, the per-commit Yoga height
7
+ * harvest, bottom-follow/anchor post-commit sync, selection layout shift and
8
+ * the scroll clamp. Scroll/anchor/drag refs stay App-owned (injected); this
9
+ * hook owns the measurement maps, measuredRowsVersion and the totalRows/
10
+ * preserved-delta bookkeeping refs.
11
+ */
12
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
13
+ import {
14
+ TRANSCRIPT_MEASURED_ROWS,
15
+ transcriptMeasuredRowsCache,
16
+ transcriptItemVariantKey,
17
+ buildTranscriptRowIndex,
18
+ transcriptStructureSignature,
19
+ transcriptRenderWindow,
20
+ resolveAnchorScrollOffset,
21
+ upperBound,
22
+ shiftSelectionRectY,
23
+ } from './transcript-window.mjs';
24
+ import { shouldSuppressFullyFailedToolItem } from '../transcript-tool-failures.mjs';
25
+
26
+ export function useTranscriptWindow({
27
+ items,
28
+ themeEpoch,
29
+ frameColumns,
30
+ toolOutputExpanded,
31
+ transcriptContentHeight,
32
+ transcriptBottomSlackRows,
33
+ transcriptGuardRows,
34
+ floatingPanelRows,
35
+ overlayHintRequested,
36
+ scrollOffset,
37
+ setScrollOffset,
38
+ transcriptAnchorRef,
39
+ transcriptAnchorDirtyRef,
40
+ scrollTargetRef,
41
+ scrollPositionRef,
42
+ maxScrollRowsRef,
43
+ transcriptGeomRef,
44
+ followingRef,
45
+ dragRef,
46
+ transcriptViewportRef,
47
+ selectionLayoutRef,
48
+ withSelectionClip,
49
+ paintSelectionRect,
50
+ stopSmoothScroll,
51
+ measuredRowsVersion,
52
+ setMeasuredRowsVersion,
53
+ }) {
54
+ const transcriptTotalRowsRef = useRef(0);
55
+ const preservedScrollDeltaRef = useRef(0);
56
+ // Previous frame's viewport-only geometry (content height + floating-panel
57
+ // reservation). A floating panel / view (picker/context/usage/text-entry)
58
+ // open-close changes transcriptContentHeight WITHOUT changing `items`, so the
59
+ // transcript window would re-slice against the new viewport height while the
60
+ // scroll offset still reflects the old one — the visible top row jumps, then
61
+ // the post-commit anchor effect snaps it back a frame later (the 2-frame
62
+ // re-settle). We detect that viewport-only change at render time and freeze
63
+ // the visible top anchor from the PREVIOUS geometry, resolving the scroll
64
+ // offset for THIS render so the anchored row stays put in a single frame.
65
+ const prevViewportGeomRef = useRef({ contentHeight: 0, floatingPanelRows: 0 });
66
+ // App-level measured row heights (real per-item height cache). The map of
67
+ // mounted item id → ink DOM element is read every commit to harvest each
68
+ // row's REAL Yoga height into transcriptMeasuredRowsCache. measuredRowsVersion
69
+ // is bumped whenever a height actually changes so the row-index/window memos
70
+ // recompute against the corrected heights (one-frame lag, absorbed by the
71
+ // overscan band).
72
+ const transcriptItemElsRef = useRef(new Map());
73
+ const transcriptMeasureRefCache = useRef(new Map());
74
+ // id → latest item object for this render. The callback ref reads from here so
75
+ // a reused (stable) callback never captures a stale item across patches.
76
+ const transcriptMeasureItemsRef = useRef(new Map());
77
+ // Stable per-item callback-ref factory: storing the element under the item id
78
+ // (and reading the live item object from transcriptMeasureItemsRef) avoids the
79
+ // ref-swap churn React would otherwise cause with an inline closure each
80
+ // render, while never serving a stale item: the callback resolves the current
81
+ // item by id at call time. The ref(null) path drops the element; the harvest
82
+ // reads getComputedHeight from whatever is mounted, so an unmount simply stops
83
+ // contributing (its last measurement stays cached on the item object).
84
+ const transcriptMeasureRef = useCallback((item) => {
85
+ if (!TRANSCRIPT_MEASURED_ROWS || !item || item.id == null) return undefined;
86
+ if (shouldSuppressFullyFailedToolItem(item)) {
87
+ transcriptMeasuredRowsCache.delete(item);
88
+ transcriptItemElsRef.current.delete(item.id);
89
+ transcriptMeasureItemsRef.current.delete(item.id);
90
+ return undefined;
91
+ }
92
+ const key = item.id;
93
+ transcriptMeasureItemsRef.current.set(key, item);
94
+ let fn = transcriptMeasureRefCache.current.get(key);
95
+ if (!fn) {
96
+ fn = (el) => {
97
+ if (el) {
98
+ transcriptItemElsRef.current.set(key, el);
99
+ } else {
100
+ transcriptItemElsRef.current.delete(key);
101
+ }
102
+ };
103
+ transcriptMeasureRefCache.current.set(key, fn);
104
+ }
105
+ return fn;
106
+ }, []);
107
+
108
+ // Key the heavy O(n) row-index + windowing memos on a STRUCTURE signature
109
+ // instead of the `items` array identity. The engine swaps `items`
110
+ // for a new array on every streaming flush (~8ms) while only the final
111
+ // assistant item's text grows; depending on array identity re-ran both memos
112
+ // each delta frame and visibly throttled the stream. The signature changes
113
+ // only when transcript structure or the streaming item's estimated height
114
+ // changes, so steady per-character growth keeps both memos warm.
115
+ //
116
+ // The signature itself is memoized on `items` identity (+columns/
117
+ // expanded) so re-renders that DO NOT touch items — drag motion, scroll,
118
+ // input typing — skip the O(n) signature walk entirely. During streaming the
119
+ // engine hands us a fresh `items` array each flush, so this memo
120
+ // recomputes and still tracks the streaming item's height correctly.
121
+ const transcriptStructureSig = useMemo(
122
+ () => transcriptStructureSignature(items, frameColumns, toolOutputExpanded),
123
+ [items, frameColumns, toolOutputExpanded],
124
+ );
125
+ const transcriptStreamingActive = (items || []).some(
126
+ (item) => item?.kind === 'assistant' && item?.streaming,
127
+ );
128
+ const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
129
+ const transcriptPinnedForStreaming = followingRef.current
130
+ || scrolledUpRowsForPin <= transcriptBottomSlackRows;
131
+ const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > transcriptBottomSlackRows;
132
+ const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
133
+ const hasStreamingReadingAnchor = !!transcriptAnchorRef.current
134
+ || transcriptAnchorDirtyRef.current;
135
+ const suppressMeasuredRowHeights = transcriptStreamingActive && (
136
+ transcriptPinnedForStreaming
137
+ || (scrolledUpForStreamingMeasure && hasStreamingReadingAnchor)
138
+ || (scrolledUpForStreamingMeasure && prevEstimateGeometry && !transcriptPinnedForStreaming)
139
+ );
140
+ const transcriptRowIndex = useMemo(() => buildTranscriptRowIndex(items, {
141
+ columns: frameColumns,
142
+ toolOutputExpanded,
143
+ suppressMeasuredRowHeights,
144
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: sig captures the relevant item changes; measuredRowsVersion folds in app-level measured height corrections
145
+ }), [transcriptStructureSig, measuredRowsVersion, suppressMeasuredRowHeights]);
146
+ // ── Same-frame anchor lock ───────────────────────────────────────────────
147
+ // While the user reads older transcript (anchor captured, not dirty), resolve
148
+ // the scroll offset that keeps the anchored viewport-top row fixed for THIS
149
+ // frame's prefix table — SYNCHRONOUSLY, before windowing/marginBottom use it.
150
+ // Previously this correction lived only in the post-commit layout effect, so
151
+ // the frame that grew the streaming tail rendered with the STALE offset first
152
+ // (a blank band opened at the top) and only the NEXT frame snapped it shut.
153
+ // Computing it here folds the new totalRows and the corrected offset into the
154
+ // SAME render, so the completed line fills its space with no one-frame gap.
155
+ // Falls back to the live scrollOffset state when there is no active anchor
156
+ // (bottom-follow / pinned) or it cannot be aligned (anchor item gone).
157
+ const hasReadingAnchor = !!transcriptAnchorRef.current && !transcriptAnchorDirtyRef.current;
158
+ const scrolledUpRows = Math.max(0, Number(scrollTargetRef.current) || 0);
159
+ // "Genuinely scrolled up" = the viewport is above the bottom slack band. The
160
+ // bottom-follow / pinned path owns everything at-or-below the slack; anything
161
+ // above it is the user reading older transcript.
162
+ const scrolledUp = scrolledUpRows > transcriptBottomSlackRows;
163
+ // A genuine reading anchor wins even if followingRef is stale-true while the
164
+ // user is scrolled up. The follow-arm SHOULD have been cleared when the anchor
165
+ // was captured, but if it lingers true we must not fall through to the stale-
166
+ // offset render path (that is one half of the newline-jump bug). Keep the
167
+ // plain !following gate for the anchor-less follow case.
168
+ const anchorLockActive = hasReadingAnchor && !followingRef.current && scrolledUp;
169
+ const targetNearBottom = followingRef.current || !scrolledUp;
170
+ const nearBottomWithoutAnchor = !transcriptAnchorRef.current
171
+ && !transcriptAnchorDirtyRef.current
172
+ && targetNearBottom;
173
+ let renderScrollOffset = targetNearBottom ? 0 : scrollOffset;
174
+ const lockViewRows = Math.max(1, Number(transcriptContentHeight) || 1);
175
+ const lockTotalRows = Math.max(0, Number(transcriptRowIndex?.totalRows) || 0);
176
+ const lockMaxRows = Math.max(0, lockTotalRows - lockViewRows);
177
+ const curPrefixForLock = transcriptRowIndex?.prefixRows || null;
178
+ if (anchorLockActive) {
179
+ const locked = resolveAnchorScrollOffset({
180
+ anchor: transcriptAnchorRef.current,
181
+ items: items,
182
+ curPrefix: curPrefixForLock,
183
+ totalRows: lockTotalRows,
184
+ viewRows: lockViewRows,
185
+ maxRows: lockMaxRows,
186
+ });
187
+ if (locked != null) renderScrollOffset = locked;
188
+ } else if (!followingRef.current && !nearBottomWithoutAnchor && scrolledUp) {
189
+ // ── Same-frame anchor CAPTURE for the missing/dirty-anchor case ─────────
190
+ // The viewport is genuinely scrolled up but there is NO usable same-frame
191
+ // anchor: it is missing, or dirtied by a manual scroll whose synchronous
192
+ // capture failed, or a stale-true follow-arm already dropped it. Without an
193
+ // anchor this frame would render with the STALE bottom-relative
194
+ // scrollOffset, so any row growth THIS frame (a streaming newline
195
+ // completing a line, a transcript row expanding) shifts the visible top by
196
+ // the delta BEFORE the post-commit rowDelta effect can capture/correct — and
197
+ // that effect would then capture from the ALREADY-shifted totalRows. That is
198
+ // the confirmed one-frame jump/jitter.
199
+ //
200
+ // Fix it at render time: capture an anchor from the PREVIOUS published
201
+ // geometry (transcriptGeomRef still holds the prior frame here — THIS
202
+ // frame's geom is published a few lines below), identifying the item id +
203
+ // row offset that sat at the previous visible-TOP edge, then resolve the
204
+ // offset against the CURRENT prefix table with the same pure helper so that
205
+ // exact top row stays put THIS frame. Persist the captured anchor so the
206
+ // post-commit effect keeps the identical anchor stable instead of
207
+ // re-deriving one from the already-shifted totalRows.
208
+ const geom = transcriptGeomRef.current || {};
209
+ const prevPrefix = geom.prefixRows;
210
+ if (Array.isArray(prevPrefix) && prevPrefix.length > 1) {
211
+ const prevTotal = Math.max(0, Number(geom.totalRows) || 0);
212
+ const prevView = Math.max(1, Number(geom.viewRows) || 1);
213
+ const prevOffset = Math.max(0, Number(geom.renderOffset) || 0);
214
+ const prevItems = geom.items || [];
215
+ // Bottom-relative window math (same as transcriptRenderWindow): the top
216
+ // edge sits `offset + viewRows` rows up from the previous total.
217
+ const prevTopRow = Math.max(0, Math.min(prevTotal, prevTotal - prevOffset - prevView));
218
+ let idx = upperBound(prevPrefix, prevTopRow) - 1;
219
+ if (idx < 0) idx = 0;
220
+ if (idx > prevPrefix.length - 2) idx = prevPrefix.length - 2;
221
+ const anchorItem = prevItems[idx];
222
+ if (anchorItem && anchorItem.id != null) {
223
+ const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
224
+ const locked = resolveAnchorScrollOffset({
225
+ anchor: captured,
226
+ items: items,
227
+ curPrefix: curPrefixForLock,
228
+ totalRows: lockTotalRows,
229
+ viewRows: lockViewRows,
230
+ maxRows: lockMaxRows,
231
+ });
232
+ if (locked != null) {
233
+ renderScrollOffset = locked;
234
+ transcriptAnchorRef.current = captured;
235
+ transcriptAnchorDirtyRef.current = false;
236
+ }
237
+ }
238
+ }
239
+ }
240
+ // ── Viewport-only transition freeze (floating panel / view open-close) ─────
241
+ // When a picker/context/usage/text-entry panel opens or closes, only the
242
+ // transcript viewport height changes (transcriptContentHeight and/or the
243
+ // floating-panel reservation) — `items` and the row-index are unchanged. The
244
+ // bottom-follow / anchor-lock paths above only fire when the user is reading
245
+ // older transcript; a bottom-pinned transcript falls through with
246
+ // renderScrollOffset resolved against the NEW viewport while the on-screen
247
+ // top edge still reflects the OLD one, producing the visible jump that the
248
+ // post-commit effect then corrects one frame later. Freeze it here: when the
249
+ // ONLY thing that changed is the viewport (not the anchor state, not items),
250
+ // reconstruct the previous visible-top row from the prior published geometry
251
+ // and resolve the offset against the current prefix table so the same top row
252
+ // renders THIS frame. Bottom-pinned/following views are already stable
253
+ // (flex-end owns them), so restrict the freeze to the scrolled-up, no-active-
254
+ // lock case that the branches above did not already resolve.
255
+ const prevViewport = prevViewportGeomRef.current || {};
256
+ const viewportOnlyChanged = (Number(prevViewport.contentHeight) || 0) !== lockViewRows
257
+ || (Number(prevViewport.floatingPanelRows) || 0) !== (Number(floatingPanelRows) || 0);
258
+ if (viewportOnlyChanged
259
+ && !anchorLockActive
260
+ && !followingRef.current
261
+ && !nearBottomWithoutAnchor
262
+ && scrolledUp) {
263
+ const geom = transcriptGeomRef.current || {};
264
+ const prevPrefix = geom.prefixRows;
265
+ if (Array.isArray(prevPrefix) && prevPrefix.length > 1) {
266
+ const prevTotal = Math.max(0, Number(geom.totalRows) || 0);
267
+ // Use the PREVIOUS viewport rows to reconstruct where the top edge sat on
268
+ // screen before the panel changed the height.
269
+ const prevView = Math.max(1, Number(prevViewport.contentHeight) || Number(geom.viewRows) || 1);
270
+ const prevOffset = Math.max(0, Number(geom.renderOffset) || 0);
271
+ const prevItems = geom.items || [];
272
+ const prevTopRow = Math.max(0, Math.min(prevTotal, prevTotal - prevOffset - prevView));
273
+ let idx = upperBound(prevPrefix, prevTopRow) - 1;
274
+ if (idx < 0) idx = 0;
275
+ if (idx > prevPrefix.length - 2) idx = prevPrefix.length - 2;
276
+ const anchorItem = prevItems[idx];
277
+ if (anchorItem && anchorItem.id != null) {
278
+ const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
279
+ const locked = resolveAnchorScrollOffset({
280
+ anchor: captured,
281
+ items: items,
282
+ curPrefix: curPrefixForLock,
283
+ totalRows: lockTotalRows,
284
+ viewRows: lockViewRows,
285
+ maxRows: lockMaxRows,
286
+ });
287
+ if (locked != null) {
288
+ renderScrollOffset = locked;
289
+ // Persist so the post-commit effect keeps this exact anchor stable
290
+ // rather than re-deriving one from the already-resized geometry.
291
+ transcriptAnchorRef.current = captured;
292
+ transcriptAnchorDirtyRef.current = false;
293
+ }
294
+ }
295
+ }
296
+ }
297
+ prevViewportGeomRef.current = {
298
+ contentHeight: lockViewRows,
299
+ floatingPanelRows: Number(floatingPanelRows) || 0,
300
+ };
301
+ const transcriptWindow = useMemo(() => transcriptRenderWindow(items, {
302
+ scrollOffset: renderScrollOffset,
303
+ viewportHeight: transcriptContentHeight,
304
+ columns: frameColumns,
305
+ toolOutputExpanded,
306
+ rowIndex: transcriptRowIndex,
307
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: sig+scroll/viewport capture the relevant changes
308
+ }), [transcriptStructureSig, renderScrollOffset, transcriptContentHeight, transcriptRowIndex]);
309
+ maxScrollRowsRef.current = transcriptWindow.maxScrollRows;
310
+ // Publish this frame's geometry so a manual scroll can capture the reading
311
+ // anchor synchronously (see captureTranscriptAnchorAt).
312
+ transcriptGeomRef.current = {
313
+ prefixRows: transcriptRowIndex?.prefixRows || null,
314
+ totalRows: Math.max(0, Number(transcriptWindow.totalRows) || 0),
315
+ viewRows: Math.max(1, Number(transcriptContentHeight) || 1),
316
+ items: items || null,
317
+ // The offset THIS frame actually rendered with. The same-frame anchor
318
+ // CAPTURE for a missing/dirty anchor (above) reads this from the PREVIOUS
319
+ // frame to reconstruct the exact top-edge row that was on screen, so the
320
+ // capture matches what the user saw rather than the stale scrollOffset
321
+ // state. Bottom-relative, matching transcriptRenderWindow's window math.
322
+ renderOffset: Math.max(0, Number(renderScrollOffset) || 0),
323
+ suppressMeasuredRowHeights,
324
+ };
325
+ // The window memo is keyed on a structure signature that intentionally
326
+ // ignores per-character growth of the streaming assistant text, so its
327
+ // `items` slice can hold a STALE reference to the streaming item between
328
+ // height changes. Re-slice the live `items` over the memo's stable
329
+ // [startIndex, endIndex) bounds so the on-screen text is always current
330
+ // while the expensive indexing/windowing stays warm.
331
+ const transcriptVisibleItems = (items || []).slice(
332
+ transcriptWindow.startIndex,
333
+ transcriptWindow.endIndex,
334
+ );
335
+ // The bottom meta band is spinner-only, so nothing is pulled out of the
336
+ // transcript for it. A finished turn's done row (turndone/statusdone) renders
337
+ // inline in scrollback like any other item — no filtering, no double-paint.
338
+ const renderedTranscriptItems = transcriptVisibleItems;
339
+ let overlayHintAttachItemIndex = -1;
340
+ for (let i = renderedTranscriptItems.length - 1; i >= 0; i--) {
341
+ const item = renderedTranscriptItems[i];
342
+ if (item?.kind === 'tool' && shouldSuppressFullyFailedToolItem(item)) continue;
343
+ overlayHintAttachItemIndex = i;
344
+ break;
345
+ }
346
+ const transcriptTailPinned = Math.max(0, Number(transcriptWindow.effectiveScrollOffset) || 0) <= transcriptBottomSlackRows;
347
+ const overlayHintOnLastItem = overlayHintRequested
348
+ && floatingPanelRows <= 0
349
+ && transcriptWindow.bottomSpacerRows === 0
350
+ && transcriptTailPinned
351
+ && overlayHintAttachItemIndex >= 0;
352
+ const overlayHintFallbackRow = overlayHintRequested
353
+ && floatingPanelRows <= 0
354
+ && transcriptGuardRows > 0
355
+ && !overlayHintOnLastItem;
356
+ // ── App-level measured height harvest ───────────────────────────────────
357
+ // Runs after EVERY commit (no deps): Yoga has just laid out the mounted rows,
358
+ // so each tracked item Box's getComputedHeight() is its REAL terminal height.
359
+ // Fold those into transcriptMeasuredRowsCache (validated on the same variant
360
+ // key the estimate caches use) and bump measuredRowsVersion only when a height
361
+ // actually changed — that re-runs the row-index/window memos against corrected
362
+ // heights, then the harvest finds nothing new and the loop settles (one frame,
363
+ // overscan-absorbed). Streaming
364
+ // assistant rows are skipped: their height churns every flush and the bottom-
365
+ // follow path already keeps them visually stable.
366
+ useLayoutEffect(() => {
367
+ if (!TRANSCRIPT_MEASURED_ROWS) return;
368
+ // Skip the per-row Yoga harvest while a drag is in progress. Edge auto-
369
+ // scroll commits setScrollOffset on every pointer motion, but the mounted
370
+ // rows' real heights do not change during a drag — only their scroll
371
+ // position does. Re-measuring every motion ran this O(mounted) loop (plus a
372
+ // variantKey check per row) for no height change, which is pure drag
373
+ // overhead on a tall transcript. The cached measurements stay authoritative
374
+ // for the row-index math; a single re-measure is forced on release below.
375
+ if (dragRef.current.active) return;
376
+ if (transcriptStreamingActive && transcriptTailPinned) return;
377
+ const els = transcriptItemElsRef.current;
378
+ if (!els || els.size === 0) return;
379
+ const liveItems = transcriptMeasureItemsRef.current;
380
+ const toolExpandedFlag = toolOutputExpanded ? 1 : 0;
381
+ let changed = false;
382
+ for (const [key, el] of els.entries()) {
383
+ const item = liveItems.get(key);
384
+ const yoga = el?.yogaNode;
385
+ if (!item || !yoga) continue;
386
+ if (shouldSuppressFullyFailedToolItem(item)) {
387
+ if (transcriptMeasuredRowsCache.delete(item)) changed = true;
388
+ continue;
389
+ }
390
+ if (item.kind === 'assistant' && item.streaming) continue;
391
+ // Width 0 = Yoga has not laid this node out yet this frame; skip so a
392
+ // transient 0 never poisons the cache (guard on a real positive width).
393
+ if (typeof yoga.getComputedWidth === 'function' && yoga.getComputedWidth() <= 0) continue;
394
+ const rawMeasured = Math.round(Number(yoga.getComputedHeight?.()) || 0);
395
+ if (rawMeasured <= 0) {
396
+ if (transcriptMeasuredRowsCache.delete(item)) changed = true;
397
+ continue;
398
+ }
399
+ const measured = Math.max(1, rawMeasured);
400
+ const variantKey = transcriptItemVariantKey(item);
401
+ const prev = transcriptMeasuredRowsCache.get(item);
402
+ if (prev
403
+ && prev.rows === measured
404
+ && prev.columns === frameColumns
405
+ && prev.toolExpanded === toolExpandedFlag
406
+ && prev.variantKey === variantKey) {
407
+ continue;
408
+ }
409
+ transcriptMeasuredRowsCache.set(item, {
410
+ rows: measured,
411
+ columns: frameColumns,
412
+ toolExpanded: toolExpandedFlag,
413
+ variantKey,
414
+ });
415
+ changed = true;
416
+ }
417
+ if (changed) setMeasuredRowsVersion((v) => (v + 1) % 1000000);
418
+ // Prune the id→item / id→callback maps to the currently-mounted set so they
419
+ // do not grow unbounded over a long session. `els` is the live mounted set
420
+ // (ref(null) deletes on unmount), so anything not in it is gone.
421
+ if (liveItems.size > els.size) {
422
+ for (const key of liveItems.keys()) {
423
+ if (!els.has(key)) liveItems.delete(key);
424
+ }
425
+ }
426
+ const refCache = transcriptMeasureRefCache.current;
427
+ if (refCache.size > els.size) {
428
+ for (const key of refCache.keys()) {
429
+ if (!els.has(key)) refCache.delete(key);
430
+ }
431
+ }
432
+ });
433
+ useLayoutEffect(() => {
434
+ const totalRows = Math.max(0, Number(transcriptWindow.totalRows) || 0);
435
+ const previousTotalRows = Math.max(0, Number(transcriptTotalRowsRef.current) || 0);
436
+ transcriptTotalRowsRef.current = totalRows;
437
+ const rowDelta = totalRows - previousTotalRows;
438
+ const curPrefix = transcriptRowIndex?.prefixRows || null;
439
+ if (previousTotalRows <= 0 || dragRef.current.active) return;
440
+
441
+ const currentTarget = Math.max(0, Number(scrollTargetRef.current) || 0);
442
+ const currentPosition = Math.max(0, Number(scrollPositionRef.current) || 0);
443
+ const currentOffset = Math.max(0, Number(scrollOffset) || 0);
444
+ const maxRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
445
+ const nearBottom = followingRef.current || currentTarget <= transcriptBottomSlackRows;
446
+ const pinnedToBottom = nearBottom;
447
+ // A genuine reading anchor must win over ordinary stream growth, but an
448
+ // explicit follow arm (prompt submit / pinned bottom) must win over stale
449
+ // anchor state. Manual scroll cancels followingRef before anchor capture, so
450
+ // deliberate reading still stays anchored while automatic bottom-follow
451
+ // stays armed across spinner/tool/stream height corrections.
452
+ const activeReadingAnchor = !!transcriptAnchorRef.current
453
+ && !transcriptAnchorDirtyRef.current
454
+ && !followingRef.current
455
+ && !nearBottom;
456
+ const followOnGrowth = followingRef.current && rowDelta > 0 && !activeReadingAnchor;
457
+ const shouldFollowBottom = rowDelta > 0 && (followOnGrowth || pinnedToBottom);
458
+ if (shouldFollowBottom) {
459
+ // Bottom follow: while pinned to the newest output,
460
+ // do NOT animate row growth. The viewport is already bottom-aligned by
461
+ // justifyContent:flex-end; injecting a temporary positive scroll offset
462
+ // during streaming makes the transcript jump down/up and can clip the
463
+ // currently generated assistant text. Keep all scroll refs at zero so
464
+ // character generation stays visually stable.
465
+ stopSmoothScroll();
466
+ scrollTargetRef.current = 0;
467
+ scrollPositionRef.current = 0;
468
+ transcriptAnchorRef.current = null;
469
+ transcriptAnchorDirtyRef.current = false;
470
+ if (currentOffset !== 0) setScrollOffset(0);
471
+ return;
472
+ }
473
+
474
+ // Viewport-only changes, such as swapping TextEntryPanel for a picker, must
475
+ // not turn a bottom-pinned transcript into a reading-anchor lock.
476
+ if (rowDelta <= 0 && nearBottom) {
477
+ stopSmoothScroll();
478
+ scrollTargetRef.current = 0;
479
+ scrollPositionRef.current = 0;
480
+ transcriptAnchorRef.current = null;
481
+ transcriptAnchorDirtyRef.current = false;
482
+ if (currentOffset !== 0) setScrollOffset(0);
483
+ return;
484
+ }
485
+
486
+ // ── ABSOLUTE ANCHOR LOCK ──────────────────────────────────────────────
487
+ // User is reading older transcript. Instead of folding the per-frame row
488
+ // DELTA into scrollOffset (which accumulated streaming-tail estimate jitter
489
+ // and could fall back to the whole-total delta on a misalignment — the
490
+ // "newline makes the screen jump" bug), we pin an ABSOLUTE anchor: the item
491
+ // id + row offset sitting at the viewport TOP edge. Every commit we look that
492
+ // item up in the CURRENT prefix table and re-derive scrollOffset so the
493
+ // anchored row stays at the same screen position. Any height change BELOW the
494
+ // anchor (streaming tail growth, a result landing under the reading position)
495
+ // only moves the bottom — the top is rock-stable. Changes ABOVE the anchor
496
+ // move the item's prefix start and are absorbed the same way. No deltas, no
497
+ // fallback, no drift.
498
+ const viewRows = Math.max(1, Number(transcriptContentHeight) || 1);
499
+ const itemList = items || [];
500
+ let anchor = transcriptAnchorRef.current;
501
+ // (Re)capture the anchor from the current viewport-top edge when missing or
502
+ // invalidated by a manual scroll. anchorRow = absolute row at the top edge.
503
+ if (!followingRef.current && (!anchor || transcriptAnchorDirtyRef.current)) {
504
+ if (curPrefix && curPrefix.length > 1) {
505
+ const anchorRow = Math.max(0, Math.min(totalRows, totalRows - currentTarget - viewRows));
506
+ let idx = upperBound(curPrefix, anchorRow) - 1;
507
+ if (idx < 0) idx = 0;
508
+ if (idx > itemList.length - 1) idx = itemList.length - 1;
509
+ const anchorItem = itemList[idx];
510
+ if (anchorItem && anchorItem.id != null) {
511
+ anchor = { id: anchorItem.id, offset: Math.max(0, anchorRow - (curPrefix[idx] || 0)) };
512
+ transcriptAnchorRef.current = anchor;
513
+ }
514
+ }
515
+ transcriptAnchorDirtyRef.current = false;
516
+ // Just captured at the current offset; nothing to correct this frame.
517
+ return;
518
+ }
519
+ // Resolve the anchored item's CURRENT position with the SAME pure helper the
520
+ // render path uses, so the post-commit state sync can never disagree with the
521
+ // synchronous render correction (which already fixed the screen this frame).
522
+ const desired = resolveAnchorScrollOffset({
523
+ anchor,
524
+ items: itemList,
525
+ curPrefix,
526
+ totalRows,
527
+ viewRows,
528
+ maxRows,
529
+ });
530
+ if (desired == null) {
531
+ // Anchor item gone (rare: removal/compaction). Re-capture next frame.
532
+ transcriptAnchorDirtyRef.current = true;
533
+ return;
534
+ }
535
+ const appliedDelta = desired - currentTarget;
536
+ if (appliedDelta === 0) return;
537
+
538
+ stopSmoothScroll();
539
+ scrollTargetRef.current = desired;
540
+ scrollPositionRef.current = Math.max(0, Math.min(maxRows, currentPosition + appliedDelta));
541
+ preservedScrollDeltaRef.current += appliedDelta;
542
+ setScrollOffset(Math.max(0, Math.round(desired)));
543
+ }, [transcriptWindow.totalRows, transcriptWindow.maxScrollRows, transcriptRowIndex, transcriptContentHeight, transcriptBottomSlackRows, scrollOffset, stopSmoothScroll]);
544
+ useLayoutEffect(() => {
545
+ if (transcriptBottomSlackRows <= 0) return;
546
+ if (transcriptAnchorRef.current || transcriptAnchorDirtyRef.current || followingRef.current) return;
547
+ const currentTarget = Math.max(0, Number(scrollTargetRef.current) || 0);
548
+ const currentPosition = Math.max(0, Number(scrollPositionRef.current) || 0);
549
+ const currentOffset = Math.max(0, Number(scrollOffset) || 0);
550
+ if (Math.max(currentTarget, currentPosition, currentOffset) === 0) return;
551
+ if (Math.max(currentTarget, currentPosition, currentOffset) > transcriptBottomSlackRows) return;
552
+ stopSmoothScroll();
553
+ scrollTargetRef.current = 0;
554
+ scrollPositionRef.current = 0;
555
+ setScrollOffset(0);
556
+ }, [transcriptBottomSlackRows, scrollOffset, stopSmoothScroll]);
557
+ useLayoutEffect(() => {
558
+ const top = Math.max(0, Number(transcriptViewportRef.current?.top) || 0);
559
+ const next = {
560
+ top,
561
+ height: Math.max(1, Number(transcriptContentHeight) || 1),
562
+ totalRows: Math.max(0, Number(transcriptWindow.totalRows) || 0),
563
+ scrollOffset: Math.max(0, Number(transcriptWindow.effectiveScrollOffset) || 0),
564
+ };
565
+ const preservedDelta = Number(preservedScrollDeltaRef.current) || 0;
566
+ if (preservedDelta !== 0) {
567
+ next.scrollOffset = Math.max(0, next.scrollOffset + preservedDelta);
568
+ preservedScrollDeltaRef.current = 0;
569
+ }
570
+ const previous = selectionLayoutRef.current;
571
+ selectionLayoutRef.current = next;
572
+ if (!previous || !dragRef.current.rect || dragRef.current.active) return;
573
+ const deltaY = (next.top - previous.top)
574
+ + (next.height - previous.height)
575
+ - (next.totalRows - previous.totalRows)
576
+ + (next.scrollOffset - previous.scrollOffset);
577
+ if (deltaY === 0) return;
578
+ const clippedRect = withSelectionClip(shiftSelectionRectY(dragRef.current.rect, deltaY));
579
+ dragRef.current = { ...dragRef.current, rect: clippedRect };
580
+ paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
581
+ }, [transcriptContentHeight, transcriptWindow.totalRows, transcriptWindow.effectiveScrollOffset, withSelectionClip, paintSelectionRect]);
582
+ useEffect(() => {
583
+ if (!dragRef.current.rect) return;
584
+ const clippedRect = withSelectionClip(dragRef.current.rect);
585
+ dragRef.current = { ...dragRef.current, rect: clippedRect };
586
+ paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
587
+ }, [themeEpoch, withSelectionClip, paintSelectionRect]);
588
+ useEffect(() => {
589
+ const maxRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
590
+ if (scrollTargetRef.current <= maxRows && scrollPositionRef.current <= maxRows && scrollOffset <= maxRows) return;
591
+ stopSmoothScroll();
592
+ const next = Math.max(0, Math.min(maxRows, scrollTargetRef.current));
593
+ scrollTargetRef.current = next;
594
+ scrollPositionRef.current = next;
595
+ setScrollOffset(Math.round(next));
596
+ }, [transcriptWindow.maxScrollRows, scrollOffset, stopSmoothScroll]);
597
+
598
+ return {
599
+ transcriptWindow,
600
+ renderedTranscriptItems,
601
+ transcriptTailPinned,
602
+ overlayHintAttachItemIndex,
603
+ overlayHintOnLastItem,
604
+ overlayHintFallbackRow,
605
+ transcriptMeasureRef,
606
+ };
607
+ }
@@ -26,20 +26,23 @@ export function ConfirmBar({ buttons = [], focusedIndex = -1 }) {
26
26
  const list = Array.isArray(buttons) ? buttons.filter(Boolean) : [];
27
27
  if (list.length === 0) return null;
28
28
  return (
29
- <Box flexDirection="row" width="100%" justifyContent="flex-end">
29
+ <Box flexDirection="row" flexShrink={0} justifyContent="flex-start" alignItems="center">
30
30
  {list.map((button, index) => {
31
31
  const isFocused = index === focusedIndex;
32
32
  return (
33
- <Text key={button.value ?? index}>
34
- {index > 0 ? <Text color={theme.subtle}>{' '}</Text> : null}
33
+ <Box
34
+ key={button.value ?? index}
35
+ marginLeft={index > 0 ? 1 : 0}
36
+ paddingX={1}
37
+ backgroundColor={isFocused ? theme.selectionBackground : undefined}
38
+ >
35
39
  <Text
36
- color={isFocused ? theme.selectionText : theme.text}
37
- backgroundColor={isFocused ? theme.selectionBackground : undefined}
40
+ color={isFocused ? theme.selectionText : theme.subtle}
38
41
  bold={isFocused}
39
42
  >
40
- {` ${button.label} `}
43
+ {`[ ${button.label} ]`}
41
44
  </Text>
42
- </Text>
45
+ </Box>
43
46
  );
44
47
  })}
45
48
  </Box>