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
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
19
19
  import { Box, Text, useInput, usePaste, useStdin } from 'ink';
20
- import stringWidth from 'string-width';
20
+ import { displayWidth } from '../display-width.mjs';
21
21
  import { theme, surfaceBackground } from '../theme.mjs';
22
22
  import {
23
23
  caretPosition,
@@ -38,14 +38,21 @@ import {
38
38
  replaceSelection,
39
39
  selectionRange,
40
40
  verticalOffset,
41
+ wordRangeAt,
41
42
  } from '../input-editing.mjs';
42
-
43
- function hintStyle(tone) {
44
- if (tone === 'error') return { textColor: theme.error };
45
- if (tone === 'warn' || tone === 'cancel') return { textColor: theme.warning };
46
- if (tone === 'plain') return { textColor: theme.subtle };
47
- return { textColor: theme.inactive };
48
- }
43
+ import {
44
+ hintStyle,
45
+ insertText,
46
+ normalizePastedText,
47
+ singleTrailingLineBreakPrefix,
48
+ draftStateEqual,
49
+ isModifiedEnterSequence,
50
+ isAnyModifiedEnterSequence,
51
+ } from './prompt-input/edit-helpers.mjs';
52
+ import {
53
+ VOICE_TICK_MS,
54
+ voiceIndicatorTextFor,
55
+ } from './prompt-input/voice-indicator.mjs';
49
56
 
50
57
  // Windows Terminal IME composition can clip a glyph that starts exactly at the
51
58
  // left edge of the editable text node. The rounded prompt box already adds a
@@ -56,11 +63,6 @@ const IME_LEFT_GUARD_COLUMNS = 0;
56
63
  // needs to immediate-render). Matches transcript selection paint cadence.
57
64
  const MOUSE_EXTEND_COALESCE_MS = 24;
58
65
 
59
- function insertText(draft, input) {
60
- if (!input) return draft;
61
- return replaceSelection(draft, input);
62
- }
63
-
64
66
  function renderSelectedText(displayValue, range, trailingSpace = false) {
65
67
  if (!range) return trailingSpace ? `${displayValue} ` : displayValue;
66
68
  const start = Math.max(0, Math.min(displayValue.length, range.start));
@@ -77,57 +79,6 @@ function renderSelectedText(displayValue, range, trailingSpace = false) {
77
79
  );
78
80
  }
79
81
 
80
- function normalizePastedText(text) {
81
- return String(text ?? '').replace(/\r\n?/g, '\n');
82
- }
83
-
84
- function singleTrailingLineBreakPrefix(text) {
85
- const normalized = normalizePastedText(text);
86
- if (!normalized.endsWith('\n')) return null;
87
- const prefix = normalized.slice(0, -1);
88
- return prefix.includes('\n') ? null : prefix;
89
- }
90
-
91
- function draftStateEqual(a, b) {
92
- return (
93
- a.value === b.value
94
- && a.cursor === b.cursor
95
- && a.selectionAnchor === b.selectionAnchor
96
- );
97
- }
98
-
99
- // Recognize a MODIFIED Enter delivered via the kitty keyboard protocol
100
- // (\x1b[13;<mod>u) or modifyOtherKeys (\x1b[27;<mod>;13~). The xterm modifier
101
- // param is (1 + bitmask) where the bitmask bits are shift=1, alt=2, ctrl=4. We
102
- // treat Ctrl+Enter AND Shift+Enter (the two common "insert newline" chords) as a
103
- // newline, so we match when the shift OR ctrl bit is set. Ctrl+J is handled
104
- // separately as the protocol-independent fallback.
105
- const MODIFIED_ENTER_SHIFT_OR_CTRL = 1 | 4;
106
- function isModifiedEnterSequence(input) {
107
- const text = String(input ?? '');
108
- const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
109
- if (!body) return false;
110
- const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
111
- if (kitty) return ((Number(kitty[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0;
112
- const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
113
- return Boolean(modifyOtherKeys && (((Number(modifyOtherKeys[1]) - 1) & MODIFIED_ENTER_SHIFT_OR_CTRL) !== 0));
114
- }
115
-
116
- // Recognize ANY modified Enter (any modifier bitmask, e.g. Alt+Enter \x1b[13;3u
117
- // / \x1b[27;3;13~). Used to CONSUME modified-Enter sequences we don't map to a
118
- // newline (Alt-only, etc.) so they aren't typed into the prompt as raw CSI text
119
- // under modifyOtherKeys. Plain Enter (mod param = 1, bitmask 0) is intentionally
120
- // NOT matched, so it still submits.
121
- function isAnyModifiedEnterSequence(input) {
122
- const text = String(input ?? '');
123
- const body = text.startsWith('\x1b[') ? text.slice(2) : text.startsWith('[') ? text.slice(1) : '';
124
- if (!body) return false;
125
- const kitty = /^13;(\d+)(?::\d+)?(?:;[\d:]+)?u$/.exec(body);
126
- if (kitty) return (Number(kitty[1]) - 1) !== 0;
127
- const modifyOtherKeys = /^27;(\d+);13~$/.exec(body);
128
- return Boolean(modifyOtherKeys && ((Number(modifyOtherKeys[1]) - 1) !== 0));
129
- }
130
-
131
82
  export function PromptInput({
132
83
  onSubmit,
133
84
  disabled = false,
@@ -150,10 +101,12 @@ export function PromptInput({
150
101
  onHistoryNavigate,
151
102
  onPasteText,
152
103
  onVoiceToggle,
104
+ voiceIndicatorMode = 'off',
153
105
  selectionRef,
154
106
  valueRef,
155
107
  boxRectRef,
156
108
  mouseSelectionRef,
109
+ suppressShiftNavRef,
157
110
  }) {
158
111
  const [draft, setDraft] = useState(() => {
159
112
  const value = String(initialValue || '');
@@ -171,8 +124,32 @@ export function PromptInput({
171
124
  const boxRef = useRef(null);
172
125
  const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
173
126
  const contentWidthRef = useRef(80);
127
+ // Cells reserved on the right for the voice indicator glyph(s) (0 when
128
+ // hidden, 1 idle/spinner-only, 2 recording's "◉"+braille). Read by the
129
+ // cursor-anchor closure below (installed ONCE, so it must read live state
130
+ // via a ref rather than close over a render-local variable) to keep the
131
+ // caret/wrap math in sync with the actual reserved column, same pattern as
132
+ // IME_LEFT_GUARD_COLUMNS on the left edge.
133
+ const voiceIndicatorWidthRef = useRef(0);
174
134
  const preferredColumnRef = useRef(null);
175
135
  const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
136
+ // Voice indicator animation tick. Runs a single ~120ms interval ONLY while
137
+ // an ANIMATED mode is active (installing/recording/transcribing) — 'idle'
138
+ // renders a static glyph and 'off' renders nothing, so neither keeps a
139
+ // timer alive (the old idle "breathing" pulse spun constantly whenever
140
+ // voice was merely enabled, reading as phantom activity).
141
+ const [voiceTick, setVoiceTick] = useState(0);
142
+ useEffect(() => {
143
+ if (voiceIndicatorMode === 'off' || voiceIndicatorMode === 'idle') return undefined;
144
+ const timer = setInterval(() => setVoiceTick((t) => (t + 1) % 1000000), VOICE_TICK_MS);
145
+ timer.unref?.();
146
+ return () => clearInterval(timer);
147
+ }, [voiceIndicatorMode]);
148
+ // Undo/redo snapshot stack. Each entry is a { value, cursor, selectionAnchor }
149
+ // draft snapshot. Continuous typing coalesces (see UNDO_COALESCE_MS) so a run
150
+ // of characters collapses into one undo step; cursor-only moves are NOT
151
+ // snapshotted. Reset on submit / draftOverride.
152
+ const undoRef = useRef({ past: [], future: [], lastPushAt: 0, lastValue: null });
176
153
  const { value, cursor } = draft;
177
154
  draftRef.current = draft;
178
155
  if (selectionRef) {
@@ -204,6 +181,7 @@ export function PromptInput({
204
181
  const sameDraft = draftStateEqual(draftRef.current, next);
205
182
  if (!options.keepPreferredColumn) preferredColumnRef.current = null;
206
183
  if (sameDraft) return;
184
+ if (!options.skipHistory) recordUndoSnapshot(draftRef.current, next, options);
207
185
  draftRef.current = next;
208
186
  setDraft(next);
209
187
  queueMicrotask(flushImmediate);
@@ -248,11 +226,20 @@ export function PromptInput({
248
226
  const d = draftRef.current;
249
227
  const w = yogaNode?.getComputedWidth?.() ?? 0;
250
228
  const guardColumns = w > IME_LEFT_GUARD_COLUMNS ? IME_LEFT_GUARD_COLUMNS : 0;
251
- const contentWidth = Math.max(1, (w ? w - guardColumns : contentWidthRef.current) || 80);
229
+ // Reserve the voice-indicator's own cells (+1 gap column when visible)
230
+ // out of the SAME width the caret/wrap math uses, mirroring the left
231
+ // guard above — otherwise a visible indicator would silently overlap
232
+ // the caret/typed text at the right edge instead of pushing content in.
233
+ const voiceReserve = voiceIndicatorWidthRef.current > 0 ? voiceIndicatorWidthRef.current + 1 : 0;
234
+ const contentWidth = Math.max(1, (w ? w - guardColumns - voiceReserve : contentWidthRef.current) || 80);
252
235
  contentWidthRef.current = contentWidth;
253
236
  const caret = w > 0
254
- ? caretPosition(d.value, d.cursor, contentWidth)
255
- : { row: 0, col: stringWidth(d.value.slice(0, d.cursor)) };
237
+ // PromptInput renders a trailing space cell when the cursor is at
238
+ // end-of-input, so a caret flush on the last column there still has a
239
+ // following cell — pass hasTrailingContent=true so it rolls to row N+1
240
+ // exactly as ink wraps the trailing space.
241
+ ? caretPosition(d.value, d.cursor, contentWidth, d.cursor >= d.value.length ? true : undefined)
242
+ : { row: 0, col: displayWidth(d.value.slice(0, d.cursor)) };
256
243
  return w > 0
257
244
  ? { ...caret, col: caret.col + guardColumns }
258
245
  : caret;
@@ -264,6 +251,70 @@ export function PromptInput({
264
251
  commitDraft(fn(draftRef.current), options);
265
252
  };
266
253
 
254
+ // --- undo/redo -----------------------------------------------------------
255
+ // Continuous-typing coalesce window: successive value-changing edits within
256
+ // this window collapse into a single undo step.
257
+ const UNDO_COALESCE_MS = 500;
258
+ const UNDO_MAX = 100;
259
+
260
+ const snapshotOf = (d) => ({ value: d.value, cursor: d.cursor, selectionAnchor: d.selectionAnchor ?? null });
261
+
262
+ const resetUndo = () => {
263
+ undoRef.current = { past: [], future: [], lastPushAt: 0, lastValue: null };
264
+ };
265
+
266
+ // Record a snapshot of the PREVIOUS state before applying `next`. Cursor-only
267
+ // moves (value unchanged) are never snapshotted. Consecutive value edits
268
+ // within UNDO_COALESCE_MS coalesce (we keep only the first snapshot of the
269
+ // run, so a single undo reverts the whole burst).
270
+ const recordUndoSnapshot = (prev, next, options = {}) => {
271
+ const stack = undoRef.current;
272
+ if (prev.value === next.value) {
273
+ // Cursor/selection-only move: don't snapshot, but BREAK the coalesce run
274
+ // so a following edit starts a fresh undo step (typing→move→typing must
275
+ // not collapse into one undo).
276
+ stack.lastPushAt = 0;
277
+ stack.lastValue = next.value;
278
+ return;
279
+ }
280
+ const now = Date.now();
281
+ const coalesce = !options.undoBreak
282
+ && stack.past.length > 0
283
+ && (now - stack.lastPushAt) < UNDO_COALESCE_MS
284
+ && stack.lastValue === prev.value;
285
+ if (!coalesce) {
286
+ stack.past.push(snapshotOf(prev));
287
+ if (stack.past.length > UNDO_MAX) stack.past.shift();
288
+ }
289
+ stack.lastPushAt = now;
290
+ stack.lastValue = next.value;
291
+ stack.future = [];
292
+ };
293
+
294
+ const performUndo = () => {
295
+ const stack = undoRef.current;
296
+ if (stack.past.length === 0) return false;
297
+ const prev = stack.past.pop();
298
+ stack.future.push(snapshotOf(draftRef.current));
299
+ stack.lastPushAt = 0;
300
+ stack.lastValue = prev.value;
301
+ commitDraft(prev, { skipHistory: true });
302
+ return true;
303
+ };
304
+
305
+ const performRedo = () => {
306
+ const stack = undoRef.current;
307
+ if (stack.future.length === 0) return false;
308
+ const next = stack.future.pop();
309
+ stack.past.push(snapshotOf(draftRef.current));
310
+ if (stack.past.length > UNDO_MAX) stack.past.shift();
311
+ stack.lastPushAt = 0;
312
+ stack.lastValue = next.value;
313
+ commitDraft(next, { skipHistory: true });
314
+ return true;
315
+ };
316
+ // -------------------------------------------------------------------------
317
+
267
318
  const cancelMouseExtendCoalesce = () => {
268
319
  const state = mouseExtendCoalesceRef.current;
269
320
  if (state.timer) {
@@ -332,6 +383,28 @@ export function PromptInput({
332
383
  const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
333
384
  queueMouseExtendCommit({ ...d, cursor: off, selectionAnchor: anchor }, immediate);
334
385
  },
386
+ hasSelection: () => selectionRange(draftRef.current) != null,
387
+ // Double-click word select: pick the word/punctuation run under the
388
+ // clicked offset and set selectionAnchor/cursor to its bounds so it
389
+ // paints via the normal selection highlight. Triple-click line select
390
+ // uses lineStart/lineEnd instead (see selectLineAt).
391
+ selectWordAt: (offset) => {
392
+ cancelMouseExtendCoalesce();
393
+ const value = draftRef.current.value;
394
+ const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
395
+ const { start, end } = wordRangeAt(value, off);
396
+ commitDraft({ ...draftRef.current, cursor: end, selectionAnchor: start });
397
+ mouseExtendCoalesceRef.current.t = Date.now();
398
+ },
399
+ selectLineAt: (offset) => {
400
+ cancelMouseExtendCoalesce();
401
+ const value = draftRef.current.value;
402
+ const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
403
+ const start = lineStart(value, off);
404
+ const end = lineEnd(value, off);
405
+ commitDraft({ ...draftRef.current, cursor: end, selectionAnchor: start });
406
+ mouseExtendCoalesceRef.current.t = Date.now();
407
+ },
335
408
  clear: () => {
336
409
  cancelMouseExtendCoalesce();
337
410
  if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
@@ -405,7 +478,8 @@ export function PromptInput({
405
478
 
406
479
  useEffect(() => {
407
480
  if (!draftOverride || typeof draftOverride.value !== 'string') return;
408
- commitDraft({ value: draftOverride.value, cursor: draftOverride.value.length, selectionAnchor: null });
481
+ commitDraft({ value: draftOverride.value, cursor: draftOverride.value.length, selectionAnchor: null }, { skipHistory: true });
482
+ resetUndo();
409
483
  }, [draftOverride?.id]);
410
484
 
411
485
  useEffect(() => () => {
@@ -419,7 +493,8 @@ export function PromptInput({
419
493
  commitDraft(next);
420
494
  return;
421
495
  }
422
- commitDraft({ value: '', cursor: 0, selectionAnchor: null });
496
+ commitDraft({ value: '', cursor: 0, selectionAnchor: null }, { skipHistory: true });
497
+ resetUndo();
423
498
  };
424
499
 
425
500
  const submitEnterChunk = (prefix = '') => {
@@ -450,6 +525,21 @@ export function PromptInput({
450
525
  const rawInput = String(input ?? '');
451
526
  const inputKey = rawInput.toLowerCase();
452
527
 
528
+ // App owns Shift+Arrow when a transcript/status ink-grid selection is live.
529
+ // Because the parent (App) useInput handler fires AFTER this child handler
530
+ // for the same event, a flag SET in App's handler is always one event stale.
531
+ // Instead call a synchronous predicate derived from dragRef at event time.
532
+ const gridSelectionActive = typeof suppressShiftNavRef === 'function'
533
+ ? suppressShiftNavRef()
534
+ : (typeof suppressShiftNavRef?.current === 'function'
535
+ ? suppressShiftNavRef.current()
536
+ : Boolean(suppressShiftNavRef?.current));
537
+ if (gridSelectionActive) {
538
+ const isShiftArrow = key.shift
539
+ && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.home || key.end);
540
+ if (isShiftArrow) return;
541
+ }
542
+
453
543
  // Drop SGR mouse-tracking sequences (wheel/click). When app mouse tracking
454
544
  // is explicitly enabled, App parses these off raw stdin itself;
455
545
  // ink still forwards the bytes here as "input", which would otherwise type
@@ -482,7 +572,22 @@ export function PromptInput({
482
572
  const rawShiftDown = rawInput === '\x1b[1;2B' || rawInput === '\x1b[b' || rawInput === '[1;2B';
483
573
  const rawShiftRight = rawInput === '\x1b[1;2C' || rawInput === '\x1b[c' || rawInput === '[1;2C';
484
574
  const rawShiftLeft = rawInput === '\x1b[1;2D' || rawInput === '\x1b[d' || rawInput === '[1;2D';
485
- const shiftHeld = key.shift || rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight;
575
+ // Ctrl+Shift+Arrow modifier sequences: xterm mod=6 (1 + shift(1) + ctrl(4))
576
+ // arrives as `\x1b[1;6<dir>`; kitty keyboard protocol reports the same chord
577
+ // as `\x1b[<code>;6<dir>` (also mod=6). Ink decodes neither the `;6` for
578
+ // arrows, so the bytes arrive raw. Fold into a ctrlShiftHeld signal used to
579
+ // drive whole-word selection-extend below. `\x1b[1;6<dir>` covers both the
580
+ // classic xterm form and kitty's default (which emits the legacy arrow form
581
+ // with the CSI-u modifier param for arrow keys).
582
+ const rawCtrlShiftUp = rawInput === '\x1b[1;6A' || rawInput === '[1;6A';
583
+ const rawCtrlShiftDown = rawInput === '\x1b[1;6B' || rawInput === '[1;6B';
584
+ const rawCtrlShiftRight = rawInput === '\x1b[1;6C' || rawInput === '[1;6C';
585
+ const rawCtrlShiftLeft = rawInput === '\x1b[1;6D' || rawInput === '[1;6D';
586
+ const ctrlShiftHeld =
587
+ rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight
588
+ || (key.shift && key.ctrl && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow));
589
+ const shiftHeld = key.shift || rawShiftUp || rawShiftDown || rawShiftLeft || rawShiftRight
590
+ || rawCtrlShiftUp || rawCtrlShiftDown || rawCtrlShiftLeft || rawCtrlShiftRight;
486
591
  const lineBreakIndex = rawInput.search(/[\r\n]/);
487
592
  const rawEnter = rawInput === '\r' || rawInput === '\n' || rawInput === '\r\n';
488
593
  const trailingEnterPrefix = singleTrailingLineBreakPrefix(rawInput);
@@ -566,25 +671,58 @@ export function PromptInput({
566
671
  return;
567
672
  }
568
673
 
569
- if (key.upArrow || rawUpArrow || rawShiftUp) {
674
+ // Ctrl+Shift+Left/Right → extend selection whole-word. Kept before the plain
675
+ // shift-arrow branches so the ctrl+shift chord never falls through to a
676
+ // char-wise extend. (Up/Down ctrl+shift extend to line-relative vertical
677
+ // move with extend — same as shift alone; handled in the vertical branch.)
678
+ if (ctrlShiftHeld && (rawCtrlShiftLeft || (key.ctrl && key.shift && key.leftArrow))) {
679
+ if (!commandPaletteActive) {
680
+ updateDraft((d) => moveCursor(d, previousWordOffset(d.value, d.cursor), { extend: true }));
681
+ }
682
+ return;
683
+ }
684
+ if (ctrlShiftHeld && (rawCtrlShiftRight || (key.ctrl && key.shift && key.rightArrow))) {
685
+ if (!commandPaletteActive) {
686
+ updateDraft((d) => moveCursor(d, nextWordOffset(d.value, d.cursor), { extend: true }));
687
+ }
688
+ return;
689
+ }
690
+
691
+ if (key.upArrow || rawUpArrow || rawShiftUp || rawCtrlShiftUp) {
570
692
  if (commandPaletteActive) {
571
693
  onCommandPaletteNavigate?.(-1);
572
694
  } else {
573
- const hasDraftText = String(draftRef.current.value || '').trim().length > 0;
574
- if (!hasDraftText) {
575
- if (!restoreQueuedToDraft()) applyHistoryNavigation('up', { emptyDraft: true });
576
- } else if (!moveDraftVertically(-1, { extend: shiftHeld })) {
577
- applyHistoryNavigation('up', { emptyDraft: false });
695
+ // A Shift-held Up is a SELECTION gesture, never history navigation:
696
+ // extend the selection up one visual line, and if already on the first
697
+ // line extend all the way to document start (offset 0). History
698
+ // navigation (restoreQueued / applyHistoryNavigation) MUST NOT fire.
699
+ if (shiftHeld) {
700
+ if (!moveDraftVertically(-1, { extend: true })) {
701
+ updateDraft((d) => moveCursor(d, 0, { extend: true }));
702
+ }
703
+ } else {
704
+ const hasDraftText = String(draftRef.current.value || '').trim().length > 0;
705
+ if (!hasDraftText) {
706
+ if (!restoreQueuedToDraft()) applyHistoryNavigation('up', { emptyDraft: true });
707
+ } else if (!moveDraftVertically(-1, { extend: false })) {
708
+ applyHistoryNavigation('up', { emptyDraft: false });
709
+ }
578
710
  }
579
711
  }
580
712
  return;
581
713
  }
582
714
 
583
- if (key.downArrow || rawDownArrow || rawShiftDown) {
715
+ if (key.downArrow || rawDownArrow || rawShiftDown || rawCtrlShiftDown) {
584
716
  if (commandPaletteActive) {
585
717
  onCommandPaletteNavigate?.(1);
586
718
  } else {
587
- if (!moveDraftVertically(1, { extend: shiftHeld })) {
719
+ // Shift-held Down: extend selection down one line, or to document end
720
+ // (value.length) when already on the last line. Never history nav.
721
+ if (shiftHeld) {
722
+ if (!moveDraftVertically(1, { extend: true })) {
723
+ updateDraft((d) => moveCursor(d, d.value.length, { extend: true }));
724
+ }
725
+ } else if (!moveDraftVertically(1, { extend: false })) {
588
726
  applyHistoryNavigation('down', { emptyDraft: String(draftRef.current.value || '').trim().length === 0 });
589
727
  }
590
728
  }
@@ -641,8 +779,8 @@ export function PromptInput({
641
779
  commitDraft({ value: '', cursor: 0, selectionAnchor: null });
642
780
  return;
643
781
  }
644
- // Active turn takes precedence over queue restore (matches claude-code
645
- // useCancelRequest priority): Esc during a running turn interrupts the
782
+ // Active turn takes precedence over queue restore: Esc during a
783
+ // running turn interrupts the
646
784
  // turn and leaves queued steering prompts intact to run afterward. Queued
647
785
  // messages only pop back into the draft when the turn is idle.
648
786
  if (interruptActive) {
@@ -702,6 +840,27 @@ export function PromptInput({
702
840
 
703
841
  const editingKey = String(input || '').toLowerCase();
704
842
 
843
+ // Undo / redo. Covered encodings:
844
+ // • kitty protocol: ctrl+z → input 'z' + key.ctrl; ctrl+y → 'y' + key.ctrl;
845
+ // ctrl+shift+z → 'z' + key.ctrl + key.shift (redo).
846
+ // • legacy control bytes: Ctrl+Z is \x1a (SUB, 0x1A), Ctrl+Y is \x19 (EM,
847
+ // 0x19). ink may deliver these as raw input without key.ctrl on some
848
+ // terminals, so match the byte directly too.
849
+ const isCtrlZ = (key.ctrl && editingKey === 'z') || rawInput === '\x1a';
850
+ const isCtrlY = (key.ctrl && editingKey === 'y') || rawInput === '\x19';
851
+ if (isCtrlZ && (key.shift || shiftHeld)) {
852
+ performRedo();
853
+ return;
854
+ }
855
+ if (isCtrlZ) {
856
+ performUndo();
857
+ return;
858
+ }
859
+ if (isCtrlY) {
860
+ performRedo();
861
+ return;
862
+ }
863
+
705
864
  // ctrl+a selects all like a normal text box; ctrl+e keeps readline line-end.
706
865
  if (key.ctrl && editingKey === 'a') {
707
866
  updateDraft((d) => (d.value ? { ...d, cursor: d.value.length, selectionAnchor: 0 } : clearSelection(d)));
@@ -834,6 +993,15 @@ export function PromptInput({
834
993
  const displayValue = mask ? value.replace(/[^\n]/g, '*') : value;
835
994
  const renderedValue = renderSelectedText(displayValue, selectionRange(draft), cursor === value.length);
836
995
  const hintMeta = hintStyle(hintTone);
996
+ // Monotone voice indicator glyph text — zero-width (rendered as null) when
997
+ // mode is 'off'. Braille frame drives installing/recording/transcribing;
998
+ // 'idle' is a static ○ (no animation, no timer).
999
+ const voiceIndicatorText = voiceIndicatorTextFor(voiceIndicatorMode, voiceTick);
1000
+ // stringWidth is unnecessary here — every glyph above is a single
1001
+ // 1-column codepoint (○ ● and the braille frames), and 'recording'
1002
+ // concatenates exactly two 1-cell glyphs, so plain .length gives the
1003
+ // correct cell count without pulling in emoji-width special-casing.
1004
+ voiceIndicatorWidthRef.current = voiceIndicatorText.length;
837
1005
 
838
1006
  return (
839
1007
  <Box ref={boxRef} flexDirection="row" width="100%" flexGrow={1} flexShrink={1} backgroundColor={surfaceBackground()}>
@@ -844,6 +1012,17 @@ export function PromptInput({
844
1012
  <Text color={hintMeta.textColor}>{hint}</Text>
845
1013
  </Box>
846
1014
  ) : null}
1015
+ {voiceIndicatorText ? (
1016
+ <>
1017
+ {/* flexGrow spacer pins the indicator to the RIGHT edge of the row
1018
+ even when the typed text is short; without it the glyph would
1019
+ ride directly after the caret instead of the box edge. */}
1020
+ <Box flexGrow={1} backgroundColor={surfaceBackground()} />
1021
+ <Box flexShrink={0} width={voiceIndicatorText.length + 1} justifyContent="flex-end" backgroundColor={surfaceBackground()}>
1022
+ <Text color={theme.subtle}>{voiceIndicatorText}</Text>
1023
+ </Box>
1024
+ </>
1025
+ ) : null}
847
1026
  </Box>
848
1027
  );
849
1028
  }
@@ -60,7 +60,11 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
60
60
  start = Math.max(0, end - MAX_VISIBLE);
61
61
  }
62
62
  const visible = commands.slice(start, end);
63
- const blankRows = Math.max(0, MAX_VISIBLE - visible.length);
63
+ // No-match frames keep the palette mounted at the SAME fixed height so the
64
+ // slash session never unmounts/remounts the 14-row panel per keystroke
65
+ // (Windows fullscreen repaint made that oscillation a hard flicker).
66
+ const noMatches = total === 0;
67
+ const blankRows = Math.max(0, MAX_VISIBLE - visible.length - (noMatches ? 1 : 0));
64
68
  const labelWidth = Math.max(12, Math.min(COMMAND_LABEL_WIDTH, Math.max(12, Math.floor(columns * 0.45))));
65
69
  const descriptionWidth = Math.max(0, columns - labelWidth - 12);
66
70
  // Standard panel rhythm: title row, blank, description/hint row, blank, content.
@@ -84,6 +88,9 @@ export function SlashCommandPalette({ commands, selectedIndex = 0, title = 'Comm
84
88
  <Text> </Text>
85
89
  <Text color={theme.subtle}>{description || ' '}</Text>
86
90
  <Text> </Text>
91
+ {noMatches ? (
92
+ <Text color={theme.subtle} wrap="truncate">No matching commands</Text>
93
+ ) : null}
87
94
  {visible.map((item, index) => (
88
95
  <CommandRow
89
96
  key={item.name}
@@ -482,6 +482,29 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
482
482
  Number(activeTools.search?.startedAt) || 0,
483
483
  ].join('|')
484
484
  : '';
485
+ // Stable primitive signatures so the render effect's dep array doesn't
486
+ // churn on new array/object identities carrying unchanged content — a new
487
+ // `agentWorkers`/`agentJobs` array (or `stats` object) reference on every
488
+ // render was clearing the pending 150ms debounce timer before it could
489
+ // fire, starving the boot full-render. Only tag|stage per worker/job and
490
+ // the numeric stats fields actually consumed here are covered.
491
+ const agentWorkersSignature = Array.isArray(agentWorkers)
492
+ ? agentWorkers.map((w) => `${w?.tag || w?.agent || w?.name || ''}|${w?.stage || w?.status || ''}|${localTimeMs(w?.startedAt || w?.startTime || w?.createdAt)}`).join(',')
493
+ : '';
494
+ const agentJobsSignature = Array.isArray(agentJobs)
495
+ ? agentJobs.map((j) => `${j?.tag || j?.agent || j?.name || ''}|${j?.status || j?.stage || ''}|${localTimeMs(j?.startedAt || j?.startTime || j?.createdAt)}`).join(',')
496
+ : '';
497
+ const statsForSignature = stats && typeof stats === 'object' ? stats : {};
498
+ const statsSignature = [
499
+ localNum(statsForSignature.currentContextTokens),
500
+ localNum(statsForSignature.currentEstimatedContextTokens),
501
+ localNum(statsForSignature.inputTokens),
502
+ localNum(statsForSignature.latestInputTokens),
503
+ localNum(statsForSignature.promptTokens),
504
+ localNum(statsForSignature.turns),
505
+ localNum(statsForSignature.contextTokens),
506
+ String(statsForSignature.currentContextSource || ''),
507
+ ].join('|');
485
508
  const refreshMs = hasActiveStatuslineWork(line, agentWorkers, agentJobs, activeTools) ? STATUSLINE_ACTIVE_REFRESH_MS : STATUSLINE_REFRESH_MS;
486
509
 
487
510
  useEffect(() => {
@@ -498,6 +521,7 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
498
521
 
499
522
  useEffect(() => {
500
523
  let alive = true;
524
+ let bootRetryTimer = null;
501
525
  const effectId = renderEffectIdRef.current + 1;
502
526
  renderEffectIdRef.current = effectId;
503
527
  const isCurrentEffect = () => alive && renderEffectIdRef.current === effectId;
@@ -508,23 +532,27 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
508
532
  lastImmediateArgsRef.current,
509
533
  );
510
534
  // ROUTE identity = the subset that actually changes the async full line's L1
511
- // usage/quota segment (provider/model/session/effort/fast + context windows).
512
- // agentRevision, compactBoundaryTokens, autoCompactTokenLimit and stats-reset
513
- // are NON-ROUTE: they churn while several agents run but DON'T invalidate the
514
- // L1 usage windows. Only a true route/session switch may wipe the cached full
515
- // line and snap to the usage-less local line; non-route churn must keep the
516
- // last good full line so `5H …/7D …` never blinks. (lastImmediateArgsRef holds
517
- // exactly these route fields, captured at the end of the previous effect run.)
535
+ // usage/quota segment (provider/model/session/effort/fast). agentRevision,
536
+ // compactBoundaryTokens, autoCompactTokenLimit, stats-reset AND the context
537
+ // window fields (contextWindow/displayContextWindow/rawContextWindow) are
538
+ // NON-ROUTE: the first prompt's API response publishes displayContextWindow
539
+ // 0→N (engine syncContextDisplayFields), which only moves the ctx% bar
540
+ // never the `5H …/7D …` usage windows. Treating it as route wiped the cached
541
+ // full line and snapped to the usage-less local line for one frame (the
542
+ // "usage blinks once on first prompt" bug). A real route switch that changes
543
+ // usage windows always changes provider/model/session anyway. Only a true
544
+ // route/session switch may wipe the cached full line; non-route churn keeps
545
+ // the last good full line (identityChanged still snaps, but via the cached
546
+ // graft path below, so usage survives and the async render refreshes ctx%
547
+ // within ~150ms). (lastImmediateArgsRef holds these fields, captured at the
548
+ // end of the previous effect run.)
518
549
  const prevImmediate = lastImmediateArgsRef.current;
519
550
  const routeChanged = !prevImmediate
520
551
  || prevImmediate.sessionId !== args.sessionId
521
552
  || prevImmediate.provider !== args.provider
522
553
  || prevImmediate.model !== args.model
523
554
  || prevImmediate.effort !== args.effort
524
- || prevImmediate.fast !== args.fast
525
- || prevImmediate.contextWindow !== args.contextWindow
526
- || prevImmediate.displayContextWindow !== args.displayContextWindow
527
- || prevImmediate.rawContextWindow !== args.rawContextWindow;
555
+ || prevImmediate.fast !== args.fast;
528
556
  // A theme switch must re-tone the footer immediately: the stored `line`
529
557
  // holds already-normalized ANSI with the OLD palette, so re-running
530
558
  // normalizeStatusLine on it is a no-op. Force a fresh local rebuild (new
@@ -607,9 +635,31 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
607
635
  const timer = setTimeout(() => {
608
636
  if (bootFullDoneRef.current !== true) {
609
637
  if (!bootFullRenderEligible(mountAtRef.current, lineRef.current, args.agentWorkers, args.agentJobs, args.activeTools)) {
638
+ // Not eligible yet (boot delay not elapsed). Don't just bail and wait
639
+ // for the next refreshTick interval (250ms/2000ms) — arm a follow-up
640
+ // timeout for the REMAINING boot delay so the first full render still
641
+ // fires on schedule instead of stalling/flickering until the next tick.
642
+ const active = hasActiveStatuslineWork(lineRef.current, args.agentWorkers, args.agentJobs, args.activeTools);
643
+ const bootDelay = active ? STATUSLINE_BOOT_FULL_DELAY_ACTIVE_MS : STATUSLINE_BOOT_FULL_DELAY_MS;
644
+ const elapsed = Date.now() - mountAtRef.current;
645
+ const remaining = Math.max(150, bootDelay - elapsed);
646
+ bootRetryTimer = setTimeout(() => {
647
+ if (!isCurrentEffect()) return;
648
+ setRefreshTick((tick) => (tick + 1) % 1_000_000);
649
+ }, remaining);
650
+ bootRetryTimer.unref?.();
610
651
  return;
611
652
  }
612
653
  if (!canAttemptBootFullRender(bootFullNextAttemptAtRef.current)) {
654
+ // Backoff window not elapsed yet — arm a follow-up timeout for exactly
655
+ // the remaining backoff so the retry fires on time instead of waiting
656
+ // for the next refreshTick interval.
657
+ const remaining = Math.max(150, bootFullNextAttemptAtRef.current - Date.now());
658
+ bootRetryTimer = setTimeout(() => {
659
+ if (!isCurrentEffect()) return;
660
+ setRefreshTick((tick) => (tick + 1) % 1_000_000);
661
+ }, remaining);
662
+ bootRetryTimer.unref?.();
613
663
  return;
614
664
  }
615
665
  }
@@ -644,8 +694,9 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
644
694
  return () => {
645
695
  alive = false;
646
696
  clearTimeout(timer);
697
+ clearTimeout(bootRetryTimer);
647
698
  };
648
- }, [sessionId, clientHostPid, provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow, compactBoundaryTokens, autoCompactTokenLimit, rawContextWindow, resizeEpoch, agentRevision, agentWorkers, agentJobs, activeToolsSignature, refreshTick, themeEpoch]);
699
+ }, [sessionId, clientHostPid, provider, model, effort, fast, cwd, statsSignature, contextWindow, displayContextWindow, compactBoundaryTokens, autoCompactTokenLimit, rawContextWindow, resizeEpoch, agentRevision, agentWorkersSignature, agentJobsSignature, activeToolsSignature, refreshTick, themeEpoch]);
649
700
 
650
701
  const lines = line ? line.split('\n').slice(0, 2) : [' ', ' '];
651
702
  const workflowLabel = workflowModeLabel(workflow, remoteEnabled);
@@ -159,6 +159,7 @@ function isAnyModifiedEnterSequence(input) {
159
159
  export function TextEntryPanel({
160
160
  title,
161
161
  hint = '',
162
+ detail = '',
162
163
  initialValue = '',
163
164
  mask = false,
164
165
  columns = 80,
@@ -465,6 +466,10 @@ export function TextEntryPanel({
465
466
  // The hint is collapsed to one line and width-truncated so a long manual
466
467
  // OAuth URL can never wrap and push the bordered title off the top.
467
468
  const hintText = truncateText(singleLine(hint), Math.max(0, columns - 4));
469
+ // Optional wrapped detail block (e.g. the manual OAuth URL). Lives INSIDE the
470
+ // live panel — never pushed to the transcript — so it disappears completely
471
+ // when the panel closes instead of lingering in terminal scrollback.
472
+ const detailText = String(detail || '').trim();
468
473
 
469
474
  return (
470
475
  <Box flexDirection="column" flexShrink={0} width="100%">
@@ -475,6 +480,12 @@ export function TextEntryPanel({
475
480
  </Box>
476
481
  <Text> </Text>
477
482
  <Text color={theme.subtle}>{hintText || ' '}</Text>
483
+ {detailText ? (
484
+ <Box flexDirection="column" width="100%">
485
+ <Text> </Text>
486
+ <Text color={theme.subtle} wrap="wrap">{detailText}</Text>
487
+ </Box>
488
+ ) : null}
478
489
  <Text> </Text>
479
490
  <Box ref={boxRef} flexDirection="row" width="100%" backgroundColor={surfaceBackground()}>
480
491
  <Text color={theme.inactive}>{promptLabel}</Text>