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,512 @@
1
+ /**
2
+ * use-transcript-scroll.mjs — transcript scroll + ink-grid selection engine.
3
+ *
4
+ * Extracted verbatim from App.jsx: smooth scroll animation, reading-anchor
5
+ * capture, selection painting/throttling/clipping, word-line span extension,
6
+ * keyboard selection focus movement and the wheel/edge-drag coalescer.
7
+ * Scroll/drag state refs stay App-owned and are injected; this hook owns only
8
+ * its internal timers (animation interval, selection paint throttle, text
9
+ * capture defer, scroll coalescer).
10
+ */
11
+ import { useCallback, useEffect, useRef } from 'react';
12
+ import { theme } from '../theme.mjs';
13
+ import {
14
+ SELECTION_PAINT_INTERVAL_MS,
15
+ SCROLL_COALESCE_MS,
16
+ selectionRectsEqual,
17
+ shiftSelectionRectY,
18
+ comparePoints,
19
+ upperBound,
20
+ } from './transcript-window.mjs';
21
+
22
+ export function useTranscriptScroll({
23
+ store,
24
+ frameColumns,
25
+ statuslineBandRows,
26
+ setScrollOffset,
27
+ scrollPositionRef,
28
+ scrollTargetRef,
29
+ maxScrollRowsRef,
30
+ transcriptBottomSlackRowsRef,
31
+ followingRef,
32
+ transcriptAnchorRef,
33
+ transcriptAnchorDirtyRef,
34
+ transcriptGeomRef,
35
+ dragRef,
36
+ frameRowsRef,
37
+ transcriptViewportRef,
38
+ selectionLayoutRef,
39
+ selectionTextRef,
40
+ }) {
41
+ const scrollAnimationRef = useRef(null);
42
+ const selectionPaintRef = useRef({ t: 0, rect: null, pending: null, timer: null });
43
+ // Coalescer for edge-drag auto-scroll + wheel scroll deltas (see
44
+ // SCROLL_COALESCE_MS). Both call sites accumulate into pendingRows.
45
+ const scrollCoalesceRef = useRef({ pendingRows: 0, timer: null });
46
+ const selectionTextTimerRef = useRef(null);
47
+
48
+ const stopSmoothScroll = useCallback(() => {
49
+ if (!scrollAnimationRef.current) return;
50
+ clearInterval(scrollAnimationRef.current);
51
+ scrollAnimationRef.current = null;
52
+ }, []);
53
+
54
+ const startSmoothScroll = useCallback(() => {
55
+ if (scrollAnimationRef.current) return;
56
+ scrollAnimationRef.current = setInterval(() => {
57
+ const current = scrollPositionRef.current;
58
+ const target = scrollTargetRef.current;
59
+ const next = current + (target - current) * 0.32;
60
+ if (Math.abs(target - next) < 0.12) {
61
+ scrollPositionRef.current = target;
62
+ setScrollOffset(Math.max(0, Math.round(target)));
63
+ followingRef.current = false;
64
+ stopSmoothScroll();
65
+ return;
66
+ }
67
+ scrollPositionRef.current = Math.max(0, next);
68
+ setScrollOffset(Math.max(0, Math.round(scrollPositionRef.current)));
69
+ }, 16);
70
+ scrollAnimationRef.current.unref?.();
71
+ }, [stopSmoothScroll]);
72
+
73
+ const cancelTranscriptFollow = useCallback(() => {
74
+ followingRef.current = false;
75
+ }, []);
76
+
77
+ const resetTranscriptScroll = useCallback(() => {
78
+ cancelTranscriptFollow();
79
+ stopSmoothScroll();
80
+ scrollPositionRef.current = 0;
81
+ scrollTargetRef.current = 0;
82
+ transcriptAnchorRef.current = null;
83
+ transcriptAnchorDirtyRef.current = false;
84
+ setScrollOffset(0);
85
+ }, [stopSmoothScroll, cancelTranscriptFollow]);
86
+
87
+ const armTranscriptFollow = useCallback(() => {
88
+ // Do not mutate scrollOffset here. During prompt submit the transcript rows
89
+ // have not necessarily been committed yet; resetting immediately makes a
90
+ // long transcript jump to the bottom, then jump again when the new row is
91
+ // appended. Keep the current viewport stable and let the row-delta effect
92
+ // perform the single bottom-follow when the transcript actually grows.
93
+ transcriptAnchorRef.current = null;
94
+ transcriptAnchorDirtyRef.current = false;
95
+ followingRef.current = true;
96
+ stopSmoothScroll();
97
+ }, [stopSmoothScroll]);
98
+
99
+ const rememberSelectionTextSoon = useCallback(() => {
100
+ if (selectionTextTimerRef.current) return;
101
+ selectionTextTimerRef.current = setTimeout(() => {
102
+ selectionTextTimerRef.current = null;
103
+ const text = store.getRenderSelectionText?.();
104
+ if (text && text.trim()) selectionTextRef.current = text;
105
+ }, 0);
106
+ }, [store]);
107
+
108
+ const selectionClip = useCallback(() => {
109
+ // The status-bar grid selection lives in the bottom statusline band, not the
110
+ // transcript viewport — clip there so the highlight cannot spill into the
111
+ // prompt/transcript rows. Everything else (transcript, word-select) keeps the
112
+ // transcript-viewport clip.
113
+ if (dragRef.current.region === 'status') {
114
+ const rows = Math.max(1, Number(frameRowsRef.current) || 24);
115
+ const top = Math.max(0, rows - statuslineBandRows);
116
+ return { y1: top, y2: Math.max(top, rows - 1) };
117
+ }
118
+ return {
119
+ y1: Math.max(0, Number(transcriptViewportRef.current?.top) || 0),
120
+ y2: Math.max(0, Number(transcriptViewportRef.current?.bottom) || 0),
121
+ };
122
+ }, []);
123
+
124
+ const withSelectionClip = useCallback((rect, options = {}) => {
125
+ if (!rect) return null;
126
+ const clip = selectionClip();
127
+ const clipped = {
128
+ ...rect,
129
+ clipY1: clip.y1,
130
+ clipY2: Math.max(clip.y1, clip.y2),
131
+ selectionForeground: theme.selectionHighlightText || theme.selectionText,
132
+ selectionBackground: theme.selectionHighlightBackground || theme.selectionBackground,
133
+ };
134
+ if (options.captureText === false) clipped.captureText = false;
135
+ return clipped;
136
+ }, [selectionClip]);
137
+
138
+ const paintSelectionRect = useCallback((clippedRect, { rememberText = true, immediate = false } = {}) => {
139
+ const nextRect = clippedRect || null;
140
+ const state = selectionPaintRef.current;
141
+ if (selectionRectsEqual(state.rect, nextRect)) {
142
+ const needsCapture = nextRect && rememberText && nextRect.captureText !== false;
143
+ if (!immediate && !needsCapture) return false;
144
+ if (immediate || needsCapture) {
145
+ store.setRenderSelection?.(nextRect, { immediate: true });
146
+ }
147
+ if (needsCapture) rememberSelectionTextSoon();
148
+ return true;
149
+ }
150
+ state.rect = nextRect;
151
+ state.t = Date.now();
152
+ store.setRenderSelection?.(nextRect, immediate ? { immediate: true } : undefined);
153
+ if (nextRect && rememberText && nextRect.captureText !== false) rememberSelectionTextSoon();
154
+ return true;
155
+ }, [store, rememberSelectionTextSoon]);
156
+
157
+ const applySelectionRect = useCallback((rect) => {
158
+ const clippedRect = withSelectionClip(rect);
159
+ dragRef.current.rect = clippedRect || null;
160
+ if (!clippedRect) selectionTextRef.current = '';
161
+ const state = selectionPaintRef.current;
162
+ if (state.timer) {
163
+ clearTimeout(state.timer);
164
+ state.timer = null;
165
+ state.pending = null;
166
+ }
167
+ paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
168
+ }, [paintSelectionRect, withSelectionClip]);
169
+
170
+ const applySelectionRectThrottled = useCallback((rect) => {
171
+ const clippedRect = withSelectionClip(rect, { captureText: false });
172
+ if (selectionRectsEqual(dragRef.current.rect, clippedRect)) return;
173
+ dragRef.current.rect = clippedRect || null;
174
+ const state = selectionPaintRef.current;
175
+ if (selectionRectsEqual(state.rect, clippedRect)) return;
176
+ const now = Date.now();
177
+ const elapsed = now - state.t;
178
+ if (elapsed >= SELECTION_PAINT_INTERVAL_MS) {
179
+ if (state.timer) {
180
+ clearTimeout(state.timer);
181
+ state.timer = null;
182
+ state.pending = null;
183
+ }
184
+ paintSelectionRect(clippedRect, { rememberText: false });
185
+ return;
186
+ }
187
+ state.pending = clippedRect || null;
188
+ if (!state.timer) {
189
+ state.timer = setTimeout(() => {
190
+ const current = selectionPaintRef.current;
191
+ const pending = current.pending;
192
+ current.timer = null;
193
+ current.pending = null;
194
+ paintSelectionRect(pending, { rememberText: false });
195
+ }, Math.max(1, SELECTION_PAINT_INTERVAL_MS - elapsed));
196
+ state.timer.unref?.();
197
+ }
198
+ }, [paintSelectionRect, withSelectionClip]);
199
+
200
+ const selectionPointAtCurrentScroll = useCallback((point, pointScroll = 0) => {
201
+ if (!point) return null;
202
+ return {
203
+ x: point.x,
204
+ y: point.y + (Number(scrollTargetRef.current) || 0) - (Number(pointScroll) || 0),
205
+ };
206
+ }, []);
207
+
208
+ // Port of selection.ts extendSelection onto the linear-rect model, hoisted to
209
+ // component scope so BOTH the mouse handler (motion/release) AND the
210
+ // auto-scroll path (scrollTranscriptRows) can rebuild a span-aware rect. Grows
211
+ // a word/line multi-click selection from its anchor span to the word/line under
212
+ // the cursor: target ends before the span → extend backward (span.hi→targetLo);
213
+ // target starts after → extend forward (span.lo→targetHi); overlapping → the
214
+ // span. The moving end snaps to the word (getWordRectAt) or line (getLineRectAt)
215
+ // at the cursor; a miss (blank/gutter) falls back to the raw cell. spanScroll
216
+ // re-anchors the span to the current transcript scroll (status never scrolls) so
217
+ // the original word/line tracks the content while dragging/auto-scrolling.
218
+ const buildSpanRect = useCallback((span, x, y, region, spanScroll = 0) => {
219
+ const conv = (pt) => (region === 'status' ? pt : selectionPointAtCurrentScroll(pt, spanScroll));
220
+ const spanLo = conv(span.lo);
221
+ const spanHi = conv(span.hi);
222
+ let mLo;
223
+ let mHi;
224
+ if (span.kind === 'word') {
225
+ const wr = store.getWordRectAt?.(x, y);
226
+ if (wr) { mLo = { x: wr.x1, y: wr.y1 }; mHi = { x: wr.x2, y: wr.y2 }; }
227
+ else { mLo = { x, y }; mHi = { x, y }; }
228
+ } else {
229
+ const lr = store.getLineRectAt?.(y);
230
+ if (lr) { mLo = { x: lr.x1, y: lr.y1 }; mHi = { x: lr.x2, y: lr.y2 }; }
231
+ else { mLo = { x: 0, y }; mHi = { x, y }; }
232
+ }
233
+ const rect = (a, b) => ({ mode: 'linear', x1: a.x, y1: a.y, x2: b.x, y2: b.y });
234
+ if (comparePoints(mHi, spanLo) < 0) return rect(spanHi, mLo);
235
+ if (comparePoints(mLo, spanHi) > 0) return rect(spanLo, mHi);
236
+ return rect(spanLo, spanHi);
237
+ }, [store, selectionPointAtCurrentScroll]);
238
+
239
+ const transcriptViewportRows = useCallback(() => {
240
+ const top = Math.max(0, Number(transcriptViewportRef.current?.top) || 0);
241
+ const bottom = Math.max(top, Number(transcriptViewportRef.current?.bottom) || top);
242
+ return { top, bottom };
243
+ }, []);
244
+
245
+ const statusBandRows = useCallback(() => {
246
+ const rows = Math.max(1, Number(frameRowsRef.current) || 24);
247
+ const top = Math.max(0, rows - statuslineBandRows);
248
+ return { top, bottom: Math.max(top, rows - 1) };
249
+ }, []);
250
+
251
+ const selectionMaxColAtRow = useCallback((row) => {
252
+ const lr = store.getLineRectAt?.(row);
253
+ if (lr != null && Number.isFinite(lr.x2)) return Math.max(0, lr.x2);
254
+ return Math.max(0, frameColumns - 1);
255
+ }, [store, frameColumns]);
256
+
257
+ // Synchronous predicate: is a transcript/status ink-grid selection live? Used
258
+ // both by App (to consume Shift+Arrow even when focus clamps at an edge) and
259
+ // by PromptInput (via prop) to gate its own Shift+Arrow at event time — a flag
260
+ // set inside App's parent handler would be one event stale (parent handler
261
+ // runs AFTER the child prompt handler for the same key).
262
+ const gridSelectionActiveRef = useRef(() => {
263
+ const drag = dragRef.current;
264
+ if (!drag || drag.active) return false;
265
+ if (drag.region !== 'transcript' && drag.region !== 'status') return false;
266
+ const rect = drag.rect;
267
+ return Boolean(rect) && !(rect.x1 === rect.x2 && rect.y1 === rect.y2);
268
+ });
269
+
270
+ useEffect(() => () => {
271
+ const paintState = selectionPaintRef.current;
272
+ if (paintState.timer) clearTimeout(paintState.timer);
273
+ paintState.timer = null;
274
+ paintState.pending = null;
275
+ if (selectionTextTimerRef.current) clearTimeout(selectionTextTimerRef.current);
276
+ selectionTextTimerRef.current = null;
277
+ const coalesceState = scrollCoalesceRef.current;
278
+ if (coalesceState.timer) clearTimeout(coalesceState.timer);
279
+ coalesceState.timer = null;
280
+ coalesceState.pendingRows = 0;
281
+ }, []);
282
+
283
+ const scrollTranscriptRows = useCallback((deltaRows, options = {}) => {
284
+ const maxTarget = Math.max(0, Number(maxScrollRowsRef.current) || 0);
285
+ const target = Math.max(0, Math.min(maxTarget, scrollTargetRef.current + deltaRows));
286
+ const appliedDelta = target - scrollTargetRef.current;
287
+ // Any manual wheel/keyboard scroll takes precedence over an in-flight
288
+ // transcript follow: drop the glide so the user's intent wins.
289
+ if (appliedDelta !== 0) cancelTranscriptFollow();
290
+ scrollTargetRef.current = target;
291
+ // A manual scroll moves the reading position. Capture the new reading anchor
292
+ // SYNCHRONOUSLY from the latest published geometry so the very next render
293
+ // already locks to it — no one-frame "dirty" window where concurrent
294
+ // streaming growth could lurch the view. At/over the bottom, drop the anchor
295
+ // so the bottom-follow path owns the viewport again.
296
+ if (appliedDelta !== 0) {
297
+ if (target <= Math.max(0, Number(transcriptBottomSlackRowsRef.current) || 0)) {
298
+ transcriptAnchorRef.current = null;
299
+ transcriptAnchorDirtyRef.current = false;
300
+ } else {
301
+ const geom = transcriptGeomRef.current || {};
302
+ const prefixRows = geom.prefixRows;
303
+ if (Array.isArray(prefixRows) && prefixRows.length > 1) {
304
+ const gTotal = Math.max(0, Number(geom.totalRows) || 0);
305
+ const gView = Math.max(1, Number(geom.viewRows) || 1);
306
+ const anchorRow = Math.max(0, Math.min(gTotal, gTotal - target - gView));
307
+ let idx = upperBound(prefixRows, anchorRow) - 1;
308
+ if (idx < 0) idx = 0;
309
+ if (idx > prefixRows.length - 2) idx = prefixRows.length - 2;
310
+ const items = geom.items || [];
311
+ const anchorItem = items[idx];
312
+ if (anchorItem && anchorItem.id != null) {
313
+ transcriptAnchorRef.current = { id: anchorItem.id, offset: Math.max(0, anchorRow - (prefixRows[idx] || 0)) };
314
+ transcriptAnchorDirtyRef.current = false;
315
+ } else {
316
+ transcriptAnchorDirtyRef.current = true;
317
+ }
318
+ } else {
319
+ transcriptAnchorDirtyRef.current = true;
320
+ }
321
+ }
322
+ }
323
+ if (appliedDelta !== 0 && selectionLayoutRef.current) {
324
+ selectionLayoutRef.current = { ...selectionLayoutRef.current, scrollOffset: target };
325
+ }
326
+ if (appliedDelta !== 0 && dragRef.current.rect) {
327
+ let rect;
328
+ if (dragRef.current.active) {
329
+ const { anchor, anchorScroll, last, anchorSpan, region } = dragRef.current;
330
+ if (anchorSpan && last) {
331
+ // Word/line multi-click drag that reached the edge: keep extending by
332
+ // whole words/lines from the span to the word/line at the current cell,
333
+ // NOT collapsing to a char {anchor->last} rect. Mirrors the motion path.
334
+ rect = buildSpanRect(anchorSpan, last.x, last.y, region, anchorScroll);
335
+ } else {
336
+ const currentAnchor = selectionPointAtCurrentScroll(anchor, anchorScroll);
337
+ rect = currentAnchor && last ? { mode: 'linear', x1: currentAnchor.x, y1: currentAnchor.y, x2: last.x, y2: last.y } : null;
338
+ }
339
+ } else {
340
+ rect = shiftSelectionRectY(dragRef.current.rect, appliedDelta);
341
+ }
342
+ const clippedRect = withSelectionClip(rect);
343
+ dragRef.current = { ...dragRef.current, rect: clippedRect };
344
+ paintSelectionRect(clippedRect, { rememberText: !dragRef.current.active });
345
+ }
346
+ if (options.smooth) {
347
+ startSmoothScroll();
348
+ return;
349
+ }
350
+ stopSmoothScroll();
351
+ scrollPositionRef.current = target;
352
+ setScrollOffset(Math.round(target));
353
+ }, [startSmoothScroll, stopSmoothScroll, paintSelectionRect, selectionPointAtCurrentScroll, withSelectionClip, cancelTranscriptFollow, buildSpanRect]);
354
+
355
+ // Leading-edge coalescer for edge-drag auto-scroll + wheel deltas: the first
356
+ // delta after an idle period flushes immediately (single wheel ticks/short
357
+ // drags stay responsive), while a flood of deltas within SCROLL_COALESCE_MS
358
+ // accumulates into one scrollTranscriptRows call per tick instead of one per
359
+ // mousemove/wheel event. Both call sites below route through this instead of
360
+ // calling scrollTranscriptRows directly.
361
+ const queueScrollCoalesced = useCallback((deltaRows) => {
362
+ const state = scrollCoalesceRef.current;
363
+ state.pendingRows += deltaRows;
364
+ if (state.timer) return;
365
+ const rows = state.pendingRows;
366
+ state.pendingRows = 0;
367
+ scrollTranscriptRows(rows);
368
+ state.timer = setTimeout(() => {
369
+ state.timer = null;
370
+ if (state.pendingRows !== 0) {
371
+ const remaining = state.pendingRows;
372
+ state.pendingRows = 0;
373
+ scrollTranscriptRows(remaining);
374
+ }
375
+ }, SCROLL_COALESCE_MS);
376
+ state.timer.unref?.();
377
+ }, [scrollTranscriptRows]);
378
+
379
+ // NOTE: declared AFTER scrollTranscriptRows — it appears in the deps array
380
+ // below, and useCallback deps are evaluated at render time, so referencing
381
+ // it before its const initializer would throw a TDZ ReferenceError.
382
+ const moveSelectionFocus = useCallback((move) => {
383
+ const drag = dragRef.current;
384
+ if (drag.active) return false;
385
+ const region = drag.region;
386
+ if (region !== 'transcript' && region !== 'status') return false;
387
+ const rect = drag.rect;
388
+ if (!rect) return false;
389
+ if (rect.x1 === rect.x2 && rect.y1 === rect.y2) return false;
390
+
391
+ let anchor = { x: rect.x1, y: rect.y1 };
392
+ let col = rect.x2;
393
+ let row = rect.y2;
394
+ const beforeCol = col;
395
+ const beforeRow = row;
396
+ // Set when Shift+Up/Down hits the viewport edge and we scroll the
397
+ // transcript to reveal a new row rather than moving within the current
398
+ // viewport (mirrors the mouse edge-drag auto-scroll at the drag-motion
399
+ // handler). In that case row/col numerically equal their "before" values
400
+ // (both clamp to the same viewport edge index) even though the
401
+ // underlying content changed, so the generic before/after guard below
402
+ // must not treat it as a no-op.
403
+ let scrolledEdge = false;
404
+
405
+ const { top, bottom } = region === 'status' ? statusBandRows() : transcriptViewportRows();
406
+
407
+ switch (move) {
408
+ case 'left':
409
+ if (col > 0) col -= 1;
410
+ else if (row > top) {
411
+ row -= 1;
412
+ col = selectionMaxColAtRow(row);
413
+ }
414
+ break;
415
+ case 'right': {
416
+ const maxCol = selectionMaxColAtRow(row);
417
+ if (col < maxCol) col += 1;
418
+ else if (row < bottom) {
419
+ row += 1;
420
+ col = 0;
421
+ }
422
+ break;
423
+ }
424
+ case 'up':
425
+ if (row > top) {
426
+ row -= 1;
427
+ } else if (region === 'transcript') {
428
+ // Already at the top visible row: scroll the transcript up by one
429
+ // row (same direction as the mouse edge-drag auto-scroll at the
430
+ // top edge, App.jsx ~3060) instead of clamping the selection in
431
+ // place, then extend the focus onto the newly revealed top row.
432
+ const beforeTarget = scrollTargetRef.current;
433
+ const slack = Math.max(0, Number(transcriptBottomSlackRowsRef.current) || 0);
434
+ const deltaRows = beforeTarget <= slack ? (slack + 1 - beforeTarget) : 1;
435
+ scrollTranscriptRows(deltaRows);
436
+ if (scrollTargetRef.current !== beforeTarget) {
437
+ scrolledEdge = true;
438
+ // scrollTranscriptRows REPLACES dragRef.current with a shifted
439
+ // copy — re-read it; the local `drag` binding is stale here.
440
+ const shiftedRect = dragRef.current.rect;
441
+ if (shiftedRect) anchor = { x: shiftedRect.x1, y: shiftedRect.y1 };
442
+ row = top;
443
+ }
444
+ }
445
+ break;
446
+ case 'down':
447
+ if (row < bottom) {
448
+ row += 1;
449
+ } else if (region === 'transcript') {
450
+ // Already at the bottom visible row: scroll down by one row (mirrors
451
+ // the mouse edge-drag auto-scroll at the bottom edge, App.jsx
452
+ // ~3062) instead of clamping, then extend the focus onto the newly
453
+ // revealed bottom row.
454
+ const beforeTarget = scrollTargetRef.current;
455
+ scrollTranscriptRows(-1);
456
+ if (scrollTargetRef.current !== beforeTarget) {
457
+ scrolledEdge = true;
458
+ // Re-read post-scroll dragRef.current (see 'up' case).
459
+ const shiftedRect = dragRef.current.rect;
460
+ if (shiftedRect) anchor = { x: shiftedRect.x1, y: shiftedRect.y1 };
461
+ row = bottom;
462
+ }
463
+ }
464
+ break;
465
+ case 'lineStart':
466
+ col = 0;
467
+ break;
468
+ case 'lineEnd':
469
+ col = selectionMaxColAtRow(row);
470
+ break;
471
+ default:
472
+ return false;
473
+ }
474
+
475
+ row = Math.max(top, Math.min(bottom, row));
476
+ col = Math.max(0, Math.min(selectionMaxColAtRow(row), col));
477
+
478
+ if (!scrolledEdge && col === beforeCol && row === beforeRow) return false;
479
+
480
+ // After an edge scroll dragRef.current is a NEW object; mutate the live
481
+ // one, not the stale entry binding.
482
+ const dragNow = dragRef.current;
483
+ if (dragNow.anchorSpan) dragNow.anchorSpan = null;
484
+
485
+ const focus = { x: col, y: row };
486
+ applySelectionRect({
487
+ mode: 'linear',
488
+ x1: anchor.x,
489
+ y1: anchor.y,
490
+ x2: focus.x,
491
+ y2: focus.y,
492
+ });
493
+ dragNow.last = { x: focus.x, y: focus.y };
494
+ return true;
495
+ }, [applySelectionRect, statusBandRows, transcriptViewportRows, selectionMaxColAtRow, scrollTranscriptRows]);
496
+
497
+ return {
498
+ stopSmoothScroll,
499
+ resetTranscriptScroll,
500
+ armTranscriptFollow,
501
+ withSelectionClip,
502
+ paintSelectionRect,
503
+ applySelectionRect,
504
+ applySelectionRectThrottled,
505
+ selectionPointAtCurrentScroll,
506
+ buildSpanRect,
507
+ gridSelectionActiveRef,
508
+ scrollTranscriptRows,
509
+ queueScrollCoalesced,
510
+ moveSelectionFocus,
511
+ };
512
+ }