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,671 @@
1
+ /**
2
+ * transcript-window.mjs — the transcript row-estimate + virtual-window engine,
3
+ * extracted verbatim from App.jsx. Owns ALL module-level caches for this
4
+ * cluster (variant-key, estimated-rows, measured-rows, sig-part). Pure module:
5
+ * no React, no App closures. The App imports the functions and (for the
6
+ * measured-height cache it writes from a layout effect) the shared cache +
7
+ * variant-key helper by name.
8
+ *
9
+ * Behavior is byte-for-byte the same as when these lived in App.jsx; only the
10
+ * home moved. The env-tunable constants and their comments are preserved.
11
+ */
12
+ import {
13
+ measureMarkdownRenderedRows,
14
+ measureStreamingMarkdownRenderedRows,
15
+ } from '../markdown/measure-rendered-rows.mjs';
16
+ import { streamingLayoutText } from '../markdown/streaming-markdown.mjs';
17
+ import { displayWidth } from '../display-width.mjs';
18
+ import { formatToolSurface, normalizeToolName, parseToolArgs, summarizeAgentSurfaceBrief } from '../../runtime/shared/tool-surface.mjs';
19
+ import { isBackgroundErrorOnlyBody } from '../../runtime/shared/err-text.mjs';
20
+ import { formatExpandedResult, wrapExpandedResultLines } from '../components/tool-output-format.mjs';
21
+ import {
22
+ formatHookDenialDetail,
23
+ isHookApprovalDenialToolItem,
24
+ shouldSuppressFullyFailedToolItem,
25
+ toolItemResultText,
26
+ } from '../transcript-tool-failures.mjs';
27
+
28
+ function positiveIntEnv(name, fallback) {
29
+ const value = Number(process.env[name]);
30
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
31
+ }
32
+
33
+ // Per-keystroke render cost is proportional to the number of MOUNTED transcript
34
+ // items: ink's renderNodeToOutput still serializes (squashTextNodes/wrapText/
35
+ // output.write) every child even when an overflow:hidden viewport clips it
36
+ // off-screen — clipping only trims write coordinates, not the serialization. So
37
+ // the only lever for typing latency on a tall transcript is mounting fewer
38
+ // rows. The window keeps a small ITEM floor (so a few items stay mounted for
39
+ // stable scroll/overscan) but is otherwise driven by the viewport+overscan ROW
40
+ // span, not a large fixed item count. All three are env-tunable for A/B / revert.
41
+ export const TRANSCRIPT_WINDOW_MIN_ITEMS = positiveIntEnv('MIXDOG_TUI_TRANSCRIPT_WINDOW_MIN_ITEMS', 12);
42
+ export const TRANSCRIPT_WINDOW_OVERSCAN_ROWS = positiveIntEnv('MIXDOG_TUI_TRANSCRIPT_OVERSCAN_ROWS', 16);
43
+
44
+ // Hard cap on simultaneously MOUNTED transcript items. Every mounted child is
45
+ // fully serialized by ink each frame (clipping only trims write coords, not the
46
+ // serialize pass), so this cap is the dominant lever for per-frame render cost
47
+ // on a tall transcript. The viewport+overscan ROW span already drives the
48
+ // window; this cap only bounds the worst case (many short rows). 180 mounted
49
+ // rows is far more than any viewport needs and made each frame serialize a long
50
+ // tail of off-screen rows, so lower it to a value that still comfortably covers
51
+ // viewport + overscan on a large terminal. Env-tunable for A/B / revert.
52
+ export const TRANSCRIPT_WINDOW_MAX_ITEMS = positiveIntEnv('MIXDOG_TUI_TRANSCRIPT_WINDOW_ITEMS', 80);
53
+ export const SELECTION_PAINT_INTERVAL_MS = positiveIntEnv('MIXDOG_TUI_SELECTION_PAINT_MS', 24);
54
+ // Frame-coalesce edge-drag auto-scroll + wheel scroll: both paths accumulate
55
+ // deltas into one pending total and flush via a single scrollTranscriptRows
56
+ // call per this interval, instead of firing the (expensive: anchor recompute +
57
+ // selection repaint) scrollTranscriptRows on every mousemove/wheel tick.
58
+ export const SCROLL_COALESCE_MS = positiveIntEnv('MIXDOG_TUI_SCROLL_COALESCE_MS', 16);
59
+ export const PROMPT_HISTORY_LIMIT = 50;
60
+
61
+ // Parse a boolean env var that DEFAULTS ON. Any of 0/false/off/no (case-
62
+ // insensitive, trimmed) disables it; everything else (including unset) leaves it
63
+ // on. Used as the kill switch for the app-level measured-height feature below.
64
+ function boolEnvDefaultTrue(name) {
65
+ const raw = process.env[name];
66
+ if (raw == null) return true;
67
+ const v = String(raw).trim().toLowerCase();
68
+ return !(v === '0' || v === 'false' || v === 'off' || v === 'no');
69
+ }
70
+
71
+ export const TRANSCRIPT_MEASURED_ROWS = boolEnvDefaultTrue('MIXDOG_TUI_TRANSCRIPT_MEASURED');
72
+
73
+ export function selectionRectsEqual(a, b) {
74
+ if (a === b) return true;
75
+ if (!a || !b) return false;
76
+ return a.mode === b.mode
77
+ && a.x1 === b.x1
78
+ && a.y1 === b.y1
79
+ && a.x2 === b.x2
80
+ && a.y2 === b.y2
81
+ && a.clipY1 === b.clipY1
82
+ && a.clipY2 === b.clipY2
83
+ && a.captureText === b.captureText;
84
+ }
85
+
86
+ export function shiftSelectionRectY(rect, deltaY) {
87
+ const dy = Math.round(Number(deltaY) || 0);
88
+ if (!rect || dy === 0) return rect || null;
89
+ return { ...rect, y1: rect.y1 + dy, y2: rect.y2 + dy };
90
+ }
91
+
92
+ // Reading-order compare (row then col): -1 if a<b, 1 if a>b, 0 equal.
93
+ export function comparePoints(a, b) {
94
+ if (a.y !== b.y) return a.y < b.y ? -1 : 1;
95
+ if (a.x !== b.x) return a.x < b.x ? -1 : 1;
96
+ return 0;
97
+ }
98
+
99
+ // Count how many terminal rows ONE logical line (no '\n') occupies once ink
100
+ // word-wraps it. Mirror the greedy word-wrap so the row estimate is never lower
101
+ // than what ink actually renders.
102
+ function wrappedLineRows(line, width) {
103
+ const text = String(line);
104
+ const full = displayWidth(text);
105
+ if (full === 0) return 1;
106
+ if (full <= width) return 1;
107
+ let rows = 1;
108
+ let col = 0;
109
+ for (const token of text.split(/(\s+)/)) {
110
+ if (!token) continue;
111
+ const tw = displayWidth(token);
112
+ if (tw === 0) continue;
113
+ if (tw > width) {
114
+ // Over-long unbreakable token: ink hard-splits it across rows.
115
+ if (col > 0) { rows++; col = 0; }
116
+ rows += Math.ceil(tw / width) - 1;
117
+ col = tw % width || width;
118
+ continue;
119
+ }
120
+ if (col + tw > width) { rows++; col = tw; }
121
+ else { col += tw; }
122
+ }
123
+ return Math.max(1, rows);
124
+ }
125
+
126
+ function estimateWrappedRows(text, columns, reserve = 4) {
127
+ const width = Math.max(8, Number(columns || 80) - reserve);
128
+ const lines = String(text ?? '').split('\n');
129
+ return Math.max(1, lines.reduce((sum, line) => sum + wrappedLineRows(line, width), 0));
130
+ }
131
+
132
+ const SKILL_SURFACE_NAMES = new Set([
133
+ 'skill', 'skill_execute', 'skill_view', 'skills_list', 'use_skill',
134
+ ]);
135
+
136
+ function isAgentResponseResultText(text) {
137
+ const value = String(text || '').trim();
138
+ if (!value) return false;
139
+ if (/^status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled)(?:\s*·\s*task_id:\s*\S+)?$/i.test(value)) return false;
140
+ if (/^(?:background task\b|agent task:|task_id:)/i.test(value) && !/\n\s*\n[\s\S]*\S/.test(value)) return false;
141
+ return true;
142
+ }
143
+
144
+ const BACKGROUND_TASK_TOOL_NAMES = new Set(['explore', 'search', 'shell', 'bash', 'bash_session', 'shell_command', 'task']);
145
+
146
+ function isBackgroundTaskToolName(normalizedName) {
147
+ return BACKGROUND_TASK_TOOL_NAMES.has(String(normalizedName || '').toLowerCase());
148
+ }
149
+
150
+ function parseBackgroundTaskResultForRows(value) {
151
+ const text = String(value || '').trim();
152
+ if (!text) return null;
153
+ const allLines = text.split('\n');
154
+ const start = allLines.findIndex((line) => line.trim() === 'background task');
155
+ if (start < 0) return null;
156
+ const rest = allLines.slice(start + 1);
157
+ const blank = rest.findIndex((line) => !line.trim());
158
+ const headLines = blank >= 0 ? rest.slice(0, blank) : rest;
159
+ const body = blank >= 0 ? rest.slice(blank + 1).join('\n').trim() : '';
160
+ const fields = {};
161
+ for (const line of headLines) {
162
+ const match = /^([a-zA-Z][\w-]*):\s*(.*)$/.exec(line.trim());
163
+ if (match) fields[match[1].toLowerCase()] = match[2].trim();
164
+ }
165
+ const status = String(fields.status || '').toLowerCase();
166
+ const error = String(fields.error || '').trim();
167
+ const errorOnlyBody = isBackgroundErrorOnlyBody(body, error);
168
+ return {
169
+ status,
170
+ body,
171
+ error,
172
+ errorOnlyBody,
173
+ hasResponse: Boolean(body) && !errorOnlyBody && !/^(running|pending|queued)$/i.test(status),
174
+ };
175
+ }
176
+
177
+ function isBackgroundTaskResponseArgsForRows(normalizedName, args = {}) {
178
+ if (!isBackgroundTaskToolName(normalizedName)) return false;
179
+ const type = String(args?.type || args?.action || '').toLowerCase();
180
+ const status = String(args?.status || '').toLowerCase();
181
+ return type === 'result' || type === 'completion' || (/^(completed|cancelled|canceled)$/i.test(status) && Boolean(args?.task_id));
182
+ }
183
+
184
+ // ToolExecution derives its background-task classification from
185
+ // formatToolSurface(name, args).args — parseToolArgs(args). The row estimate /
186
+ // variant key must read the SAME parsed shape. parseToolArgs already guards
187
+ // malformed input (returns {} / { value } without throwing).
188
+ function backgroundArgsForRows(rawArgs) {
189
+ const parsed = parseToolArgs(rawArgs);
190
+ return parsed && typeof parsed === 'object' ? parsed : {};
191
+ }
192
+
193
+ function toolItemPendingForRows(item) {
194
+ const count = Math.max(1, Number(item?.count || 1));
195
+ const done = Math.max(0, Math.min(count, Number(item?.completedCount || (item?.result == null ? 0 : count))));
196
+ return done < count;
197
+ }
198
+
199
+ const LEADING_STATUS_MARKER_LINE_RE = /^\[status:\s*[^\]]*\]\s*$/i;
200
+
201
+ function stripLeadingStatusMarkerFromTextForRows(text) {
202
+ const lines = String(text || '').split('\n');
203
+ if (lines.length > 0 && LEADING_STATUS_MARKER_LINE_RE.test(String(lines[0] ?? '').trim())) lines.shift();
204
+ return lines.join('\n');
205
+ }
206
+
207
+ function toolDisplayedResultTextForRows(item) {
208
+ const rt = item?.result == null ? '' : String(item.result).replace(/\s+$/, '');
209
+ const bgArgs = backgroundArgsForRows(item?.args);
210
+ const backgroundError = String(bgArgs.error || '');
211
+ const errorOnlyResult = Boolean(rt) && isBackgroundErrorOnlyBody(rt, backgroundError);
212
+ const normalizedName = String(normalizeToolName(item?.name) || '').toLowerCase();
213
+ if (!toolItemPendingForRows(item) && isBackgroundTaskToolName(normalizedName)) {
214
+ const meta = parseBackgroundTaskResultForRows(rt);
215
+ if (meta?.hasResponse && String(meta.body || '').trim()) {
216
+ return stripLeadingStatusMarkerFromTextForRows(String(meta.body));
217
+ }
218
+ }
219
+ return stripLeadingStatusMarkerFromTextForRows(errorOnlyResult ? '' : (rt || ''));
220
+ }
221
+
222
+ function toolHasDisplayResultForRows(item) {
223
+ const rt = item.result == null ? '' : String(item.result).replace(/\s+$/, '');
224
+ const trimmed = String(rt || '').trim();
225
+ if (!trimmed) return false;
226
+ const bgArgs = backgroundArgsForRows(item.args);
227
+ if (isBackgroundErrorOnlyBody(trimmed, bgArgs.error || '')) return false;
228
+ const normalizedName = String(normalizeToolName(item.name) || '').toLowerCase();
229
+ if (isBackgroundTaskToolName(normalizedName)) {
230
+ const meta = parseBackgroundTaskResultForRows(trimmed);
231
+ if (meta) return Boolean(meta.hasResponse && String(meta.body || '').trim());
232
+ }
233
+ return true;
234
+ }
235
+
236
+ function toolExpandedRawTextForRows(item, rawRt) {
237
+ if (item?.aggregate) return rawRt;
238
+ const hasDisplayResult = toolHasDisplayResultForRows(item);
239
+ if (hasDisplayResult) return toolDisplayedResultTextForRows(item);
240
+ return stripLeadingStatusMarkerFromTextForRows(rawRt || '');
241
+ }
242
+
243
+ function toolHeaderFailureOnlyForRows(item, normalizedName, hasDisplayResult) {
244
+ if (hasDisplayResult) return false;
245
+ const bgArgs = backgroundArgsForRows(item.args);
246
+ const error = String(bgArgs.error || '').trim();
247
+ if (!error) return false;
248
+ if (normalizedName === 'agent') {
249
+ const pending = toolItemPendingForRows(item);
250
+ const isError = Boolean(item.isError);
251
+ const agentHeaderFailure = !pending && isError && error && !hasDisplayResult;
252
+ if (!agentHeaderFailure) return false;
253
+ const displayedResultText = toolDisplayedResultTextForRows(item);
254
+ const rt = item.result == null ? '' : String(item.result).replace(/\s+$/, '');
255
+ const isAgentResult = !pending && hasDisplayResult;
256
+ const isAgentResponse = isAgentResult && isAgentResponseResultText(rt);
257
+ const briefRaw = summarizeAgentSurfaceBrief(item.name, bgArgs, displayedResultText, {
258
+ isError,
259
+ isResponse: isAgentResponse,
260
+ });
261
+ const agentSurfaceBriefNonempty = Boolean(String(briefRaw || '').trim());
262
+ return !agentSurfaceBriefNonempty;
263
+ }
264
+ if (!isBackgroundTaskToolName(normalizedName) || !bgArgs.task_id) return false;
265
+ if (isBackgroundTaskResponseArgsForRows(normalizedName, bgArgs)) return false;
266
+ const status = String(bgArgs.status || '').toLowerCase();
267
+ return /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(status);
268
+ }
269
+
270
+ function toolArgPathForRows(item) {
271
+ const a = backgroundArgsForRows(item?.args);
272
+ return a?.path ?? a?.file_path ?? a?.file ?? '';
273
+ }
274
+
275
+ function isShellSurfaceForRows(normalizedName, label = '') {
276
+ const n = String(normalizedName || '').toLowerCase();
277
+ const l = String(label || '').toLowerCase();
278
+ return n === 'shell' || n === 'bash' || n === 'bash_session'
279
+ || n === 'shell_command' || n === 'job_wait' || l === 'run';
280
+ }
281
+
282
+ function isShellSurfaceForToolItem(item, normalizedName) {
283
+ const label = formatToolSurface(item?.name, item?.args)?.label || '';
284
+ return isShellSurfaceForRows(normalizedName, label);
285
+ }
286
+
287
+ // EXPANDED tool bodies are post-processed by formatExpandedResult; the row
288
+ // estimate MUST run the SAME pipeline. Pass pathArg/isShell so the count matches.
289
+ function estimateToolRenderedResultRows(value, { pathArg = '', isShell = false, columns = 80 } = {}) {
290
+ const text = String(value ?? '').replace(/\s+$/, '');
291
+ if (!text) return 1;
292
+ try {
293
+ const logical = formatExpandedResult(text, { pathArg, isShell });
294
+ const rows = wrapExpandedResultLines(logical, columns, { isShell }).length;
295
+ return Math.max(1, rows);
296
+ } catch {
297
+ return Math.max(1, text.split('\n').length);
298
+ }
299
+ }
300
+
301
+ function estimateTranscriptItemRows(item, columns, toolOutputExpanded) {
302
+ if (!item) return 1;
303
+ switch (item.kind) {
304
+ case 'user':
305
+ return 1 + estimateWrappedRows(item.text, columns, 4);
306
+ case 'assistant':
307
+ // marginTop={1} (AssistantMessage <Box>) + rendered body height.
308
+ return 1 + (item.streaming
309
+ ? measureStreamingMarkdownRenderedRows(item.text, columns, item.id)
310
+ : measureMarkdownRenderedRows(item.text, columns));
311
+ case 'tool': {
312
+ const TOOL_MARGIN_TOP = 1;
313
+ if (shouldSuppressFullyFailedToolItem(item)) return 0;
314
+ if (isHookApprovalDenialToolItem(item)) {
315
+ const detail = formatHookDenialDetail(toolItemResultText(item));
316
+ return TOOL_MARGIN_TOP + 1 + (detail ? 1 : 0);
317
+ }
318
+ const normalizedName = String(normalizeToolName(item.name) || '').toLowerCase();
319
+ const count = Math.max(1, Number(item.count || 1));
320
+ const done = Math.max(0, Math.min(count, Number(item.completedCount || (item.result == null ? 0 : count))));
321
+ const pending = done < count;
322
+ const isSkillSurface = !item.aggregate && SKILL_SURFACE_NAMES.has(normalizedName);
323
+ const isAgentSurface = normalizedName === 'agent';
324
+ const rt = item.result == null ? null : String(item.result).replace(/\s+$/, '');
325
+ const rawRt = item.rawResult == null ? null : String(item.rawResult).replace(/\s+$/, '');
326
+ const hasResult = item.result != null && Boolean(String(rt || '').trim());
327
+ const hasRawResult = item.rawResult != null && Boolean(String(rawRt || '').trim());
328
+ const expanded = toolOutputExpanded || item.expanded;
329
+ if (!expanded || pending) {
330
+ if (isSkillSurface) return TOOL_MARGIN_TOP + 1;
331
+ const hasDisplayResult = toolHasDisplayResultForRows(item);
332
+ if (toolHeaderFailureOnlyForRows(item, normalizedName, hasDisplayResult)) {
333
+ return TOOL_MARGIN_TOP + 1;
334
+ }
335
+ return TOOL_MARGIN_TOP + 1 + 1;
336
+ }
337
+ if (hasRawResult) {
338
+ const estimateText = toolExpandedRawTextForRows(item, rawRt);
339
+ const rawOpts = item.aggregate
340
+ ? {}
341
+ : { pathArg: toolArgPathForRows(item), isShell: isShellSurfaceForToolItem(item, normalizedName) };
342
+ return TOOL_MARGIN_TOP + 1 + estimateToolRenderedResultRows(estimateText, { ...rawOpts, columns });
343
+ }
344
+ if (isAgentSurface && !hasResult) return TOOL_MARGIN_TOP + 1 + 1;
345
+ if (isSkillSurface && !hasResult) return TOOL_MARGIN_TOP + 1;
346
+ if (item.aggregate) {
347
+ if (hasRawResult) {
348
+ const resultRows = estimateToolRenderedResultRows(rawRt, { columns });
349
+ return TOOL_MARGIN_TOP + 1 + resultRows;
350
+ }
351
+ return TOOL_MARGIN_TOP + 1 + 1;
352
+ } else {
353
+ const backgroundMeta = hasResult && isBackgroundTaskToolName(normalizedName)
354
+ ? parseBackgroundTaskResultForRows(rt)
355
+ : null;
356
+ const isBackgroundResult = hasResult && isBackgroundTaskToolName(normalizedName);
357
+ const isBackgroundResponse = isBackgroundResult
358
+ && (backgroundMeta?.hasResponse || isBackgroundTaskResponseArgsForRows(normalizedName, backgroundArgsForRows(item.args)));
359
+ const isBackgroundMetadataResult = isBackgroundResult && !isBackgroundResponse && Boolean(backgroundMeta);
360
+ if (isBackgroundMetadataResult) {
361
+ const hasDisplayResult = toolHasDisplayResultForRows(item);
362
+ if (toolHeaderFailureOnlyForRows(item, normalizedName, hasDisplayResult)) {
363
+ return TOOL_MARGIN_TOP + 1;
364
+ }
365
+ return isSkillSurface ? TOOL_MARGIN_TOP + 1 : TOOL_MARGIN_TOP + 1 + 1;
366
+ }
367
+ const resultText = backgroundMeta?.hasResponse ? backgroundMeta.body : rt;
368
+ const resultRows = estimateToolRenderedResultRows(resultText, {
369
+ pathArg: toolArgPathForRows(item),
370
+ isShell: isShellSurfaceForToolItem(item, normalizedName),
371
+ columns,
372
+ });
373
+ return TOOL_MARGIN_TOP + 1 + resultRows;
374
+ }
375
+ }
376
+ case 'notice':
377
+ return 1 + estimateWrappedRows(item.text, columns, 6);
378
+ case 'turndone':
379
+ case 'statusdone':
380
+ return 2;
381
+ default:
382
+ return 1;
383
+ }
384
+ }
385
+
386
+ export function lowerBound(values, target) {
387
+ let lo = 0;
388
+ let hi = values.length;
389
+ while (lo < hi) {
390
+ const mid = Math.floor((lo + hi) / 2);
391
+ if (values[mid] < target) lo = mid + 1;
392
+ else hi = mid;
393
+ }
394
+ return lo;
395
+ }
396
+
397
+ export function upperBound(values, target) {
398
+ let lo = 0;
399
+ let hi = values.length;
400
+ while (lo < hi) {
401
+ const mid = Math.floor((lo + hi) / 2);
402
+ if (values[mid] <= target) lo = mid + 1;
403
+ else hi = mid;
404
+ }
405
+ return lo;
406
+ }
407
+
408
+ // Resolve the absolute scroll offset that keeps a captured reading anchor at the
409
+ // same screen position for the CURRENT prefix table. Pure so it can run during
410
+ // render AND in the post-commit layout effect.
411
+ export function resolveAnchorScrollOffset({ anchor, items, curPrefix, totalRows, viewRows, maxRows }) {
412
+ if (!anchor || anchor.id == null) return null;
413
+ if (!Array.isArray(curPrefix) || curPrefix.length <= 1) return null;
414
+ const list = Array.isArray(items) ? items : [];
415
+ let idx = -1;
416
+ for (let i = list.length - 1; i >= 0; i--) {
417
+ if (list[i] && list[i].id === anchor.id) { idx = i; break; }
418
+ }
419
+ if (idx < 0 || idx > curPrefix.length - 2) return null;
420
+ const itemHeight = Math.max(0, (curPrefix[idx + 1] || 0) - (curPrefix[idx] || 0));
421
+ const clampedOffset = Math.max(0, Math.min(Number(anchor.offset) || 0, itemHeight));
422
+ const anchorRowCur = (curPrefix[idx] || 0) + clampedOffset;
423
+ return Math.max(0, Math.min(maxRows, totalRows - viewRows - anchorRowCur));
424
+ }
425
+
426
+ // Cheap, stable height fingerprint for a text blob (length + newline count +
427
+ // FNV-1a hash), so a same-length edit that changes wrap/newline shape still
428
+ // invalidates the row/signature caches.
429
+ function fnv1a32(str) {
430
+ let h = 0x811c9dc5;
431
+ for (let i = 0; i < str.length; i++) {
432
+ h ^= str.charCodeAt(i);
433
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
434
+ }
435
+ return h >>> 0;
436
+ }
437
+
438
+ // Two INDEPENDENT 32-bit rolling-hash steps folded into a 64-bit signature.
439
+ // fnvStepA is plain FNV-1a; fnvStepB uses a distinct seed/prime + xorshift
440
+ // finalizer so the two chains are decorrelated (see the App.jsx history note).
441
+ export function fnvStepA(hash, str) {
442
+ let h = hash >>> 0;
443
+ for (let i = 0; i < str.length; i++) {
444
+ h ^= str.charCodeAt(i);
445
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
446
+ }
447
+ return h >>> 0;
448
+ }
449
+
450
+ export function fnvStepB(hash, str) {
451
+ let h = hash >>> 0;
452
+ for (let i = 0; i < str.length; i++) {
453
+ h = Math.imul(h ^ str.charCodeAt(i), 0x85ebca77) >>> 0;
454
+ h = (h ^ (h >>> 13)) >>> 0;
455
+ }
456
+ return h >>> 0;
457
+ }
458
+
459
+ function textShapeFingerprint(value) {
460
+ if (value == null) return 'z';
461
+ const text = String(value);
462
+ const len = text.length;
463
+ if (len === 0) return 'e';
464
+ let newlines = 0;
465
+ for (let i = 0; i < len; i++) {
466
+ if (text.charCodeAt(i) === 10) newlines++;
467
+ }
468
+ return `${len}.${newlines}.${fnv1a32(text).toString(36)}`;
469
+ }
470
+
471
+ // Identity-keyed memo for the variant key. Exported: the App's measured-height
472
+ // layout effect validates its cache writes on the SAME variant key.
473
+ const transcriptVariantKeyCache = new WeakMap();
474
+
475
+ export function transcriptItemVariantKey(item) {
476
+ if (item && typeof item === 'object') {
477
+ const cached = transcriptVariantKeyCache.get(item);
478
+ if (cached !== undefined) return cached;
479
+ const key = computeTranscriptItemVariantKey(item);
480
+ transcriptVariantKeyCache.set(item, key);
481
+ return key;
482
+ }
483
+ return computeTranscriptItemVariantKey(item);
484
+ }
485
+
486
+ function computeTranscriptItemVariantKey(item) {
487
+ const expanded = item.expanded ? 1 : 0;
488
+ if (item.kind === 'tool') {
489
+ const resultShape = textShapeFingerprint(item.result);
490
+ const rawShape = textShapeFingerprint(item.rawResult);
491
+ const count = Number(item.count ?? 0);
492
+ const completed = item.completedCount === undefined ? 'u' : Number(item.completedCount);
493
+ const errors = item.errorCount === undefined ? 'u' : Number(item.errorCount);
494
+ const isError = item.isError ? 1 : 0;
495
+ const normalizedName = String(normalizeToolName(item.name) || '').toLowerCase();
496
+ const aggregate = item.aggregate ? 1 : 0;
497
+ const bgArgs = backgroundArgsForRows(item.args);
498
+ const bgType = String(bgArgs.type || bgArgs.action || '');
499
+ const bgStatus = String(bgArgs.status || '');
500
+ const bgTaskId = bgArgs.task_id ? 1 : 0;
501
+ const bgPrompt = textShapeFingerprint(bgArgs.prompt);
502
+ const bgMessage = textShapeFingerprint(bgArgs.message);
503
+ const bgError = textShapeFingerprint(bgArgs.error);
504
+ return `x${expanded}:n${normalizedName}:g${aggregate}:r${resultShape}:R${rawShape}:c${count}:d${completed}:e${errors}:E${isError}:bt${bgType}:bs${bgStatus}:bk${bgTaskId}:bp${bgPrompt}:bm${bgMessage}:be${bgError}`;
505
+ }
506
+ return `x${expanded}:s${textShapeFingerprint(item.text ?? item.result ?? '')}`;
507
+ }
508
+
509
+ // Per-item ESTIMATED ROW COUNT cache for buildTranscriptRowIndex.
510
+ const transcriptRowsCache = new WeakMap();
511
+
512
+ // App-level MEASURED row heights (real per-item height cache). Exported so the
513
+ // App's per-commit layout effect can write/read/prune it; validated on the same
514
+ // (variantKey + columns + toolExpanded) tuple as the estimate caches.
515
+ export const transcriptMeasuredRowsCache = new WeakMap();
516
+
517
+ export function measuredTranscriptRows(item, columns, toolOutputExpanded) {
518
+ if (!TRANSCRIPT_MEASURED_ROWS || !item) return null;
519
+ if (shouldSuppressFullyFailedToolItem(item)) return 0;
520
+ if (item.kind === 'assistant' && item.streaming) return null;
521
+ const entry = transcriptMeasuredRowsCache.get(item);
522
+ if (!entry) return null;
523
+ if (entry.rows <= 0) return null;
524
+ if (entry.columns !== columns) return null;
525
+ if (entry.toolExpanded !== (toolOutputExpanded ? 1 : 0)) return null;
526
+ if (entry.variantKey !== transcriptItemVariantKey(item)) return null;
527
+ return entry.rows;
528
+ }
529
+
530
+ const STREAMING_ROW_QUANTUM = 1;
531
+
532
+ function assistantTextForStreamingRowEstimate(text) {
533
+ return streamingLayoutText(text);
534
+ }
535
+
536
+ function streamingEstimateRows(item, columns, toolOutputExpanded) {
537
+ const trimmedText = assistantTextForStreamingRowEstimate(item.text);
538
+ const estimateItem = trimmedText === item.text ? item : { ...item, text: trimmedText };
539
+ const raw = Math.max(1, Math.ceil(estimateTranscriptItemRows(estimateItem, columns, toolOutputExpanded)));
540
+ return Math.ceil(raw / STREAMING_ROW_QUANTUM) * STREAMING_ROW_QUANTUM;
541
+ }
542
+
543
+ function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
544
+ if (!item) return Math.max(1, Math.ceil(estimateTranscriptItemRows(item, columns, toolOutputExpanded)));
545
+ if (shouldSuppressFullyFailedToolItem(item)) return 0;
546
+ if (item.kind === 'assistant' && item.streaming) {
547
+ return streamingEstimateRows(item, columns, toolOutputExpanded);
548
+ }
549
+ const variantKey = transcriptItemVariantKey(item);
550
+ const toolExpanded = toolOutputExpanded ? 1 : 0;
551
+ const cached = transcriptRowsCache.get(item);
552
+ if (cached
553
+ && cached.columns === columns
554
+ && cached.toolExpanded === toolExpanded
555
+ && cached.variantKey === variantKey
556
+ && cached.id === item.id
557
+ && cached.kind === item.kind) {
558
+ return cached.rows;
559
+ }
560
+ const rows = Math.max(1, Math.ceil(estimateTranscriptItemRows(item, columns, toolOutputExpanded)));
561
+ transcriptRowsCache.set(item, { id: item.id, kind: item.kind, variantKey, columns, toolExpanded, rows });
562
+ return rows;
563
+ }
564
+
565
+ export function buildTranscriptRowIndex(items, { columns = 80, toolOutputExpanded = false } = {}) {
566
+ const allItems = Array.isArray(items) ? items : [];
567
+ const rows = new Array(allItems.length);
568
+ const prefixRows = new Array(allItems.length + 1);
569
+ prefixRows[0] = 0;
570
+ for (let i = 0; i < allItems.length; i++) {
571
+ const item = allItems[i];
572
+ const measured = measuredTranscriptRows(item, columns, toolOutputExpanded);
573
+ const rowCount = measured != null
574
+ ? measured
575
+ : estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded);
576
+ rows[i] = rowCount;
577
+ prefixRows[i + 1] = prefixRows[i] + rowCount;
578
+ }
579
+ return { rows, prefixRows, totalRows: prefixRows[allItems.length] || 0 };
580
+ }
581
+
582
+ // Stable signature for the transcript row-index / window memos. Changes only
583
+ // when transcript STRUCTURE changes or the streaming item's estimated height
584
+ // changes — not on every character. Per-item sigParts are identity-memoized.
585
+ const transcriptSigPartCache = new WeakMap();
586
+
587
+ export function transcriptStructureSignature(items, columns, toolOutputExpanded) {
588
+ const list = Array.isArray(items) ? items : [];
589
+ let hA = fnvStepA(0x811c9dc5, `${list.length}|${columns}|${toolOutputExpanded ? 1 : 0}`);
590
+ let hB = fnvStepB(0xcbf29ce4, `${list.length}|${columns}|${toolOutputExpanded ? 1 : 0}`);
591
+ for (let i = 0; i < list.length; i++) {
592
+ const it = list[i];
593
+ let sigPart;
594
+ if (!it) {
595
+ sigPart = '_';
596
+ } else if (it.kind === 'assistant' && it.streaming) {
597
+ sigPart = `a${it.id}:${streamingEstimateRows(it, columns, toolOutputExpanded)}`;
598
+ } else {
599
+ const variantKey = transcriptItemVariantKey(it);
600
+ const cached = transcriptSigPartCache.get(it);
601
+ if (cached
602
+ && cached.variantKey === variantKey
603
+ && cached.columns === columns
604
+ && cached.id === it.id
605
+ && cached.kind === it.kind) {
606
+ sigPart = cached.sigPart;
607
+ } else {
608
+ sigPart = `${it.kind?.[0] || '?'}${it.id}:${variantKey}`;
609
+ transcriptSigPartCache.set(it, { id: it.id, kind: it.kind, variantKey, columns, sigPart });
610
+ }
611
+ }
612
+ hA = fnvStepA(hA, `;${i};`);
613
+ hA = fnvStepA(hA, sigPart);
614
+ hB = fnvStepB(hB, `;${i};`);
615
+ hB = fnvStepB(hB, sigPart);
616
+ }
617
+ return `${hA.toString(36)}.${hB.toString(36)}`;
618
+ }
619
+
620
+ export function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight = 24, columns = 80, toolOutputExpanded = false, rowIndex = null } = {}) {
621
+ const allItems = Array.isArray(items) ? items : [];
622
+ const itemCount = allItems.length;
623
+ const fallbackIndex = rowIndex?.prefixRows?.length === itemCount + 1
624
+ ? rowIndex
625
+ : buildTranscriptRowIndex(allItems, { columns, toolOutputExpanded });
626
+ const totalRows = Math.max(0, fallbackIndex.totalRows || 0);
627
+ const viewRows = Math.max(1, Number(viewportHeight) || 24);
628
+ const maxScrollRows = Math.max(0, totalRows - viewRows);
629
+ const effectiveScrollOffset = Math.min(
630
+ maxScrollRows,
631
+ Math.max(0, Math.ceil(Number(scrollOffset) || 0)),
632
+ );
633
+
634
+ const bypassRowBudget = viewRows + TRANSCRIPT_WINDOW_OVERSCAN_ROWS * 2;
635
+ if (itemCount <= TRANSCRIPT_WINDOW_MIN_ITEMS || totalRows <= bypassRowBudget) {
636
+ return { startIndex: 0, endIndex: itemCount, items: allItems, bottomSpacerRows: 0, totalRows, maxScrollRows, effectiveScrollOffset };
637
+ }
638
+
639
+ const minItems = Math.min(TRANSCRIPT_WINDOW_MIN_ITEMS, itemCount);
640
+ const maxItems = Math.max(minItems, TRANSCRIPT_WINDOW_MAX_ITEMS);
641
+ const prefixRows = fallbackIndex.prefixRows;
642
+ const visibleTop = Math.max(0, totalRows - effectiveScrollOffset - viewRows);
643
+ const visibleBottom = Math.min(totalRows, totalRows - effectiveScrollOffset);
644
+ const desiredTop = Math.max(0, visibleTop - TRANSCRIPT_WINDOW_OVERSCAN_ROWS);
645
+ const desiredBottom = Math.min(totalRows, visibleBottom + TRANSCRIPT_WINDOW_OVERSCAN_ROWS);
646
+
647
+ let startIndex = Math.max(0, upperBound(prefixRows, desiredTop) - 1);
648
+ let endIndex = Math.min(itemCount, Math.max(startIndex + 1, lowerBound(prefixRows, Math.max(desiredBottom, desiredTop + 1))));
649
+
650
+ while (endIndex - startIndex < minItems && startIndex > 0) startIndex--;
651
+ while (endIndex - startIndex < minItems && endIndex < itemCount) endIndex++;
652
+
653
+ if (endIndex - startIndex > maxItems) {
654
+ const visibleStartIndex = Math.max(0, upperBound(prefixRows, visibleTop) - 1);
655
+ const visibleEndIndex = Math.min(itemCount, Math.max(visibleStartIndex + 1, lowerBound(prefixRows, Math.max(visibleBottom, visibleTop + 1))));
656
+ startIndex = Math.max(0, Math.min(visibleStartIndex, itemCount - maxItems));
657
+ endIndex = Math.min(itemCount, Math.max(visibleEndIndex, startIndex + maxItems));
658
+ if (endIndex - startIndex > maxItems) startIndex = Math.max(0, endIndex - maxItems);
659
+ }
660
+
661
+ const bottomSpacerRows = Math.max(0, totalRows - (prefixRows[endIndex] || totalRows));
662
+ return {
663
+ startIndex,
664
+ endIndex,
665
+ items: allItems.slice(startIndex, endIndex),
666
+ bottomSpacerRows,
667
+ totalRows,
668
+ maxScrollRows,
669
+ effectiveScrollOffset,
670
+ };
671
+ }