mixdog 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,510 @@
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
+ scrollTranscriptRows(1);
434
+ if (scrollTargetRef.current !== beforeTarget) {
435
+ scrolledEdge = true;
436
+ // scrollTranscriptRows REPLACES dragRef.current with a shifted
437
+ // copy — re-read it; the local `drag` binding is stale here.
438
+ const shiftedRect = dragRef.current.rect;
439
+ if (shiftedRect) anchor = { x: shiftedRect.x1, y: shiftedRect.y1 };
440
+ row = top;
441
+ }
442
+ }
443
+ break;
444
+ case 'down':
445
+ if (row < bottom) {
446
+ row += 1;
447
+ } else if (region === 'transcript') {
448
+ // Already at the bottom visible row: scroll down by one row (mirrors
449
+ // the mouse edge-drag auto-scroll at the bottom edge, App.jsx
450
+ // ~3062) instead of clamping, then extend the focus onto the newly
451
+ // revealed bottom row.
452
+ const beforeTarget = scrollTargetRef.current;
453
+ scrollTranscriptRows(-1);
454
+ if (scrollTargetRef.current !== beforeTarget) {
455
+ scrolledEdge = true;
456
+ // Re-read post-scroll dragRef.current (see 'up' case).
457
+ const shiftedRect = dragRef.current.rect;
458
+ if (shiftedRect) anchor = { x: shiftedRect.x1, y: shiftedRect.y1 };
459
+ row = bottom;
460
+ }
461
+ }
462
+ break;
463
+ case 'lineStart':
464
+ col = 0;
465
+ break;
466
+ case 'lineEnd':
467
+ col = selectionMaxColAtRow(row);
468
+ break;
469
+ default:
470
+ return false;
471
+ }
472
+
473
+ row = Math.max(top, Math.min(bottom, row));
474
+ col = Math.max(0, Math.min(selectionMaxColAtRow(row), col));
475
+
476
+ if (!scrolledEdge && col === beforeCol && row === beforeRow) return false;
477
+
478
+ // After an edge scroll dragRef.current is a NEW object; mutate the live
479
+ // one, not the stale entry binding.
480
+ const dragNow = dragRef.current;
481
+ if (dragNow.anchorSpan) dragNow.anchorSpan = null;
482
+
483
+ const focus = { x: col, y: row };
484
+ applySelectionRect({
485
+ mode: 'linear',
486
+ x1: anchor.x,
487
+ y1: anchor.y,
488
+ x2: focus.x,
489
+ y2: focus.y,
490
+ });
491
+ dragNow.last = { x: focus.x, y: focus.y };
492
+ return true;
493
+ }, [applySelectionRect, statusBandRows, transcriptViewportRows, selectionMaxColAtRow, scrollTranscriptRows]);
494
+
495
+ return {
496
+ stopSmoothScroll,
497
+ resetTranscriptScroll,
498
+ armTranscriptFollow,
499
+ withSelectionClip,
500
+ paintSelectionRect,
501
+ applySelectionRect,
502
+ applySelectionRectThrottled,
503
+ selectionPointAtCurrentScroll,
504
+ buildSpanRect,
505
+ gridSelectionActiveRef,
506
+ scrollTranscriptRows,
507
+ queueScrollCoalesced,
508
+ moveSelectionFocus,
509
+ };
510
+ }