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