@zergai/ztc 0.4.0-beta.1 → 0.5.0-beta.1

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 (718) hide show
  1. package/README.md +398 -45
  2. package/RUST_THIRD_PARTY_LICENSES.txt +1080 -822
  3. package/THIRD_PARTY_NOTICES.md +273 -12
  4. package/bin/zmux-darwin-arm64 +0 -0
  5. package/bin/ztc-audio-darwin-arm64 +0 -0
  6. package/bin/ztc-capture-darwin-arm64 +0 -0
  7. package/bin/ztc-computer-darwin-arm64 +0 -0
  8. package/bin/ztc-llm-darwin-arm64 +0 -0
  9. package/bin/ztc-tts-darwin-arm64 +0 -0
  10. package/bin/ztc-tui-darwin-arm64 +0 -0
  11. package/bin/ztc-zlicky-overlay-darwin-arm64 +0 -0
  12. package/bin/zterm-darwin-arm64 +0 -0
  13. package/dist/App.js +1206 -209
  14. package/dist/agent/agent.d.ts +74 -8
  15. package/dist/agent/agent.js +1073 -184
  16. package/dist/agent/backends/account_rotation.d.ts +4 -0
  17. package/dist/agent/backends/account_rotation.js +8 -2
  18. package/dist/agent/backends/anthropic.d.ts +13 -0
  19. package/dist/agent/backends/anthropic.js +169 -28
  20. package/dist/agent/backends/claude_cli.js +18 -8
  21. package/dist/agent/backends/codex_app_server.d.ts +1 -0
  22. package/dist/agent/backends/codex_app_server.js +55 -3
  23. package/dist/agent/backends/codex_auth.d.ts +1 -0
  24. package/dist/agent/backends/codex_auth.js +67 -13
  25. package/dist/agent/backends/codex_buffered_recovery.d.ts +27 -0
  26. package/dist/agent/backends/codex_buffered_recovery.js +293 -0
  27. package/dist/agent/backends/gemini.js +3 -1
  28. package/dist/agent/backends/http_error.d.ts +26 -0
  29. package/dist/agent/backends/http_error.js +221 -0
  30. package/dist/agent/backends/index.d.ts +9 -2
  31. package/dist/agent/backends/index.js +22 -2
  32. package/dist/agent/backends/openai.d.ts +0 -6
  33. package/dist/agent/backends/openai.js +5 -2
  34. package/dist/agent/backends/openai_compatible.d.ts +5 -4
  35. package/dist/agent/backends/openai_compatible.js +26 -19
  36. package/dist/agent/backends/openai_responses.d.ts +8 -3
  37. package/dist/agent/backends/openai_responses.js +77 -33
  38. package/dist/agent/backends/provider_ids.d.ts +1 -0
  39. package/dist/agent/backends/provider_ids.js +4 -0
  40. package/dist/agent/backends/request_metrics.js +12 -2
  41. package/dist/agent/backends/types.d.ts +109 -6
  42. package/dist/agent/backends/types.js +5 -1
  43. package/dist/agent/backends/zrouter_stream.d.ts +6 -1
  44. package/dist/agent/backends/zrouter_stream.js +160 -42
  45. package/dist/agent/background_registry.d.ts +2 -0
  46. package/dist/agent/background_registry.js +1 -0
  47. package/dist/agent/claim_observation.d.ts +6 -0
  48. package/dist/agent/claim_observation.js +54 -0
  49. package/dist/agent/commands/accounts.js +46 -177
  50. package/dist/agent/commands/attention.d.ts +2 -0
  51. package/dist/agent/commands/attention.js +67 -0
  52. package/dist/agent/commands/auth.js +18 -2
  53. package/dist/agent/commands/board.d.ts +2 -0
  54. package/dist/agent/commands/board.js +169 -0
  55. package/dist/agent/commands/clipboard.js +50 -1
  56. package/dist/agent/commands/compact.js +21 -13
  57. package/dist/agent/commands/config.js +4 -1
  58. package/dist/agent/commands/context.js +6 -1
  59. package/dist/agent/commands/desktop.d.ts +2 -0
  60. package/dist/agent/commands/desktop.js +29 -0
  61. package/dist/agent/commands/effort.js +14 -6
  62. package/dist/agent/commands/environment.d.ts +3 -0
  63. package/dist/agent/commands/environment.js +109 -0
  64. package/dist/agent/commands/evidence.d.ts +2 -0
  65. package/dist/agent/commands/evidence.js +9 -0
  66. package/dist/agent/commands/fork.js +4 -3
  67. package/dist/agent/commands/goal.d.ts +33 -1
  68. package/dist/agent/commands/goal.js +174 -14
  69. package/dist/agent/commands/goal_exit.d.ts +36 -0
  70. package/dist/agent/commands/goal_exit.js +120 -0
  71. package/dist/agent/commands/help.js +3 -1
  72. package/dist/agent/commands/history.js +3 -1
  73. package/dist/agent/commands/inbox.d.ts +2 -0
  74. package/dist/agent/commands/inbox.js +148 -0
  75. package/dist/agent/commands/index.js +16 -1
  76. package/dist/agent/commands/input_mode.js +8 -6
  77. package/dist/agent/commands/job.d.ts +2 -0
  78. package/dist/agent/commands/job.js +93 -0
  79. package/dist/agent/commands/keybindings.js +4 -1
  80. package/dist/agent/commands/label.d.ts +2 -0
  81. package/dist/agent/commands/label.js +77 -0
  82. package/dist/agent/commands/layout.d.ts +2 -0
  83. package/dist/agent/commands/layout.js +20 -0
  84. package/dist/agent/commands/mirror.js +16 -1
  85. package/dist/agent/commands/model.js +12 -2
  86. package/dist/agent/commands/permissions.d.ts +3 -1
  87. package/dist/agent/commands/permissions.js +79 -89
  88. package/dist/agent/commands/plan.js +5 -15
  89. package/dist/agent/commands/profile.js +21 -2
  90. package/dist/agent/commands/provider_selection.js +20 -1
  91. package/dist/agent/commands/rename.js +1 -1
  92. package/dist/agent/commands/resources.d.ts +2 -0
  93. package/dist/agent/commands/resources.js +67 -0
  94. package/dist/agent/commands/rewind.d.ts +3 -0
  95. package/dist/agent/commands/rewind.js +16 -0
  96. package/dist/agent/commands/runbooks.d.ts +2 -0
  97. package/dist/agent/commands/runbooks.js +73 -0
  98. package/dist/agent/commands/runbooks_lazy.d.ts +2 -0
  99. package/dist/agent/commands/runbooks_lazy.js +8 -0
  100. package/dist/agent/commands/sessions.d.ts +1 -1
  101. package/dist/agent/commands/sessions.js +33 -5
  102. package/dist/agent/commands/settings.js +185 -39
  103. package/dist/agent/commands/setup.d.ts +2 -0
  104. package/dist/agent/commands/setup.js +78 -0
  105. package/dist/agent/commands/share.d.ts +2 -0
  106. package/dist/agent/commands/share.js +64 -0
  107. package/dist/agent/commands/status.js +14 -2
  108. package/dist/agent/commands/types.d.ts +99 -6
  109. package/dist/agent/commands/update.js +8 -0
  110. package/dist/agent/commands/usage.d.ts +2 -0
  111. package/dist/agent/commands/usage.js +8 -0
  112. package/dist/agent/factory.d.ts +11 -0
  113. package/dist/agent/factory.js +113 -88
  114. package/dist/agent/loop_policy.d.ts +6 -3
  115. package/dist/agent/loop_policy.js +12 -3
  116. package/dist/agent/measurement_guidance.d.ts +2 -0
  117. package/dist/agent/measurement_guidance.js +5 -0
  118. package/dist/agent/request_encoder.d.ts +8 -1
  119. package/dist/agent/request_encoder.js +95 -23
  120. package/dist/agent/runtime/benchmark_comparison.d.ts +30 -0
  121. package/dist/agent/runtime/benchmark_comparison.js +94 -0
  122. package/dist/agent/runtime/benchmark_contract.d.ts +48 -0
  123. package/dist/agent/runtime/benchmark_contract.js +90 -0
  124. package/dist/agent/runtime/checked_commands.d.ts +32 -0
  125. package/dist/agent/runtime/checked_commands.js +119 -0
  126. package/dist/agent/runtime/command_execution_policy.d.ts +26 -0
  127. package/dist/agent/runtime/command_execution_policy.js +91 -0
  128. package/dist/agent/runtime/command_jobs.d.ts +36 -18
  129. package/dist/agent/runtime/command_jobs.js +151 -13
  130. package/dist/agent/runtime/command_panes.d.ts +59 -0
  131. package/dist/agent/runtime/command_panes.js +398 -0
  132. package/dist/agent/runtime/command_wait.d.ts +19 -0
  133. package/dist/agent/runtime/command_wait.js +167 -0
  134. package/dist/agent/runtime/cpu_benchmark.d.ts +30 -0
  135. package/dist/agent/runtime/cpu_benchmark.js +126 -0
  136. package/dist/agent/runtime/delegated_evidence.d.ts +26 -0
  137. package/dist/agent/runtime/delegated_evidence.js +80 -0
  138. package/dist/agent/runtime/evidence.d.ts +127 -0
  139. package/dist/agent/runtime/evidence.js +468 -0
  140. package/dist/agent/runtime/execution_context.d.ts +7 -0
  141. package/dist/agent/runtime/execution_context.js +39 -3
  142. package/dist/agent/runtime/json_verifier_recipe.d.ts +8 -0
  143. package/dist/agent/runtime/json_verifier_recipe.js +66 -0
  144. package/dist/agent/runtime/measurements.d.ts +110 -0
  145. package/dist/agent/runtime/measurements.js +317 -0
  146. package/dist/agent/runtime/policy.d.ts +4 -0
  147. package/dist/agent/runtime/policy.js +31 -20
  148. package/dist/agent/runtime/provider_native_evidence.d.ts +20 -0
  149. package/dist/agent/runtime/provider_native_evidence.js +86 -0
  150. package/dist/agent/runtime/recovery.d.ts +2 -0
  151. package/dist/agent/runtime/recovery.js +45 -2
  152. package/dist/agent/runtime/remote_broker_transport.d.ts +10 -0
  153. package/dist/agent/runtime/remote_broker_transport.js +84 -0
  154. package/dist/agent/runtime/remote_deletion_receipts.d.ts +3 -0
  155. package/dist/agent/runtime/remote_deletion_receipts.js +43 -0
  156. package/dist/agent/runtime/remote_journal_replay.d.ts +13 -0
  157. package/dist/agent/runtime/remote_journal_replay.js +233 -0
  158. package/dist/agent/runtime/remote_mutation_journal.d.ts +25 -0
  159. package/dist/agent/runtime/remote_mutation_journal.js +208 -0
  160. package/dist/agent/runtime/remote_resource_endpoint.d.ts +7 -0
  161. package/dist/agent/runtime/remote_resource_endpoint.js +6 -0
  162. package/dist/agent/runtime/remote_resource_errors.d.ts +5 -0
  163. package/dist/agent/runtime/remote_resource_errors.js +9 -0
  164. package/dist/agent/runtime/remote_resource_script.d.ts +2 -0
  165. package/dist/agent/runtime/remote_resource_script.js +1009 -0
  166. package/dist/agent/runtime/remote_resources.d.ts +29 -0
  167. package/dist/agent/runtime/remote_resources.js +160 -0
  168. package/dist/agent/runtime/source_verification.d.ts +12 -0
  169. package/dist/agent/runtime/source_verification.js +210 -0
  170. package/dist/agent/runtime/source_verification_contract.d.ts +75 -0
  171. package/dist/agent/runtime/source_verification_contract.js +203 -0
  172. package/dist/agent/runtime/tool_checkpoints.d.ts +11 -0
  173. package/dist/agent/runtime/tool_checkpoints.js +26 -0
  174. package/dist/agent/runtime/tool_effects.d.ts +5 -0
  175. package/dist/agent/runtime/tool_progress.d.ts +12 -0
  176. package/dist/agent/runtime/tool_progress.js +76 -0
  177. package/dist/agent/runtime/tool_runtime.d.ts +16 -1
  178. package/dist/agent/runtime/tool_runtime.js +204 -10
  179. package/dist/agent/runtime/tracing.d.ts +1 -1
  180. package/dist/agent/runtime/verifier_capture.d.ts +9 -0
  181. package/dist/agent/runtime/verifier_capture.js +100 -0
  182. package/dist/agent/runtime/verifier_contract.d.ts +24 -0
  183. package/dist/agent/runtime/verifier_contract.js +41 -0
  184. package/dist/agent/subagent.d.ts +20 -10
  185. package/dist/agent/subagent.js +266 -29
  186. package/dist/agent/tool_response_directory.d.ts +24 -0
  187. package/dist/agent/tool_response_directory.js +61 -0
  188. package/dist/agent/tool_response_source.d.ts +8 -0
  189. package/dist/agent/tool_response_source.js +49 -0
  190. package/dist/agent/tool_response_store.d.ts +75 -4
  191. package/dist/agent/tool_response_store.js +583 -64
  192. package/dist/agent/tools/ask_user.d.ts +3 -1
  193. package/dist/agent/tools/ask_user.js +30 -11
  194. package/dist/agent/tools/background.js +1 -0
  195. package/dist/agent/tools/benchmark_cpu.d.ts +2 -0
  196. package/dist/agent/tools/benchmark_cpu.js +102 -0
  197. package/dist/agent/tools/compare_benchmarks.d.ts +2 -0
  198. package/dist/agent/tools/compare_benchmarks.js +53 -0
  199. package/dist/agent/tools/deferred.js +1 -0
  200. package/dist/agent/tools/delegate.js +18 -2
  201. package/dist/agent/tools/helper_requirements.d.ts +7 -0
  202. package/dist/agent/tools/helper_requirements.js +19 -0
  203. package/dist/agent/tools/index.d.ts +1 -0
  204. package/dist/agent/tools/index.js +3 -0
  205. package/dist/agent/tools/measurement_report.d.ts +22 -0
  206. package/dist/agent/tools/measurement_report.js +190 -0
  207. package/dist/agent/tools/pane.d.ts +0 -7
  208. package/dist/agent/tools/pane.js +5 -4
  209. package/dist/agent/tools/project_context.d.ts +3 -0
  210. package/dist/agent/tools/project_context.js +45 -0
  211. package/dist/agent/tools/recall_tool_response.d.ts +3 -5
  212. package/dist/agent/tools/recall_tool_response.js +19 -29
  213. package/dist/agent/tools/registry.d.ts +1 -0
  214. package/dist/agent/tools/registry.js +23 -0
  215. package/dist/agent/tools/related_sessions.d.ts +2 -0
  216. package/dist/agent/tools/related_sessions.js +156 -0
  217. package/dist/agent/tools/routine.js +13 -3
  218. package/dist/agent/tools/session_evidence.d.ts +3 -0
  219. package/dist/agent/tools/session_evidence.js +72 -0
  220. package/dist/agent/tools/shell.d.ts +4 -3
  221. package/dist/agent/tools/shell.js +174 -47
  222. package/dist/agent/tools/tool_search.js +3 -2
  223. package/dist/agent/tools/types.d.ts +28 -0
  224. package/dist/agent/tools/verify_artifact.d.ts +2 -0
  225. package/dist/agent/tools/verify_artifact.js +60 -0
  226. package/dist/agent/tools/workflow.js +1 -0
  227. package/dist/app/activity.d.ts +6 -1
  228. package/dist/app/activity.js +81 -12
  229. package/dist/app/agent_runner.d.ts +6 -1
  230. package/dist/app/agent_runner.js +34 -19
  231. package/dist/app/command_context.js +106 -10
  232. package/dist/app/controller.d.ts +15 -4
  233. package/dist/app/controller.js +29 -3
  234. package/dist/app/escape_stop.d.ts +11 -0
  235. package/dist/app/escape_stop.js +15 -0
  236. package/dist/app/liveness.d.ts +6 -4
  237. package/dist/app/liveness.js +13 -3
  238. package/dist/app/model_catalog_cache.d.ts +5 -0
  239. package/dist/app/model_catalog_cache.js +138 -0
  240. package/dist/app/ordered_transcript.js +43 -4
  241. package/dist/app/prompt_turn.d.ts +14 -0
  242. package/dist/app/prompt_turn.js +53 -0
  243. package/dist/app/recap.d.ts +3 -0
  244. package/dist/app/recap.js +9 -3
  245. package/dist/app/restart.d.ts +3 -2
  246. package/dist/app/restart.js +22 -7
  247. package/dist/app/rewind.d.ts +3 -3
  248. package/dist/app/rewind.js +3 -3
  249. package/dist/app/selection_completion.d.ts +17 -2
  250. package/dist/app/selection_completion.js +84 -9
  251. package/dist/app/session_manager.d.ts +38 -6
  252. package/dist/app/session_manager.js +263 -60
  253. package/dist/app/session_messages.d.ts +3 -0
  254. package/dist/app/session_messages.js +60 -0
  255. package/dist/app/task_scope_router.d.ts +20 -0
  256. package/dist/app/task_scope_router.js +173 -0
  257. package/dist/app/transcript_window.d.ts +2 -0
  258. package/dist/app/transcript_window.js +4 -0
  259. package/dist/app/workspace_admission_details.d.ts +3 -0
  260. package/dist/app/workspace_admission_details.js +26 -0
  261. package/dist/app/workspace_layout_controller.d.ts +11 -0
  262. package/dist/app/workspace_layout_controller.js +88 -0
  263. package/dist/app/workspace_monitor.d.ts +1 -0
  264. package/dist/app/workspace_monitor.js +216 -0
  265. package/dist/app/workspace_presets.d.ts +29 -0
  266. package/dist/app/workspace_presets.js +192 -0
  267. package/dist/attention/queue.d.ts +37 -0
  268. package/dist/attention/queue.js +183 -0
  269. package/dist/bench/rf_shared.d.ts +2 -0
  270. package/dist/board/binding.d.ts +42 -0
  271. package/dist/board/binding.js +38 -0
  272. package/dist/board/client.d.ts +42 -0
  273. package/dist/board/client.js +60 -0
  274. package/dist/cli.js +1 -1
  275. package/dist/cli_args.js +19 -17
  276. package/dist/cli_main.js +110 -13
  277. package/dist/cli_ratatui.d.ts +183 -16
  278. package/dist/cli_ratatui.js +3989 -1255
  279. package/dist/components/ActivityLine.d.ts +3 -2
  280. package/dist/components/ActivityLine.js +2 -2
  281. package/dist/components/InputArea.d.ts +19 -1
  282. package/dist/components/InputArea.js +264 -190
  283. package/dist/components/MessageList.d.ts +2 -0
  284. package/dist/components/MessageList.js +3 -1
  285. package/dist/components/SingleMessage.d.ts +2 -0
  286. package/dist/components/SingleMessage.js +4 -2
  287. package/dist/components/StatusBar.d.ts +4 -2
  288. package/dist/components/StatusBar.js +3 -1
  289. package/dist/conductor_branch.js +3 -0
  290. package/dist/config/api_retry.d.ts +6 -1
  291. package/dist/config/api_retry.js +19 -1
  292. package/dist/config/defaults.d.ts +2 -1
  293. package/dist/config/defaults.js +9 -2
  294. package/dist/config/message_display.d.ts +1 -0
  295. package/dist/config/message_display.js +17 -2
  296. package/dist/config/permissions.d.ts +21 -0
  297. package/dist/config/permissions.js +89 -0
  298. package/dist/config/resolved.d.ts +2 -2
  299. package/dist/config/types.d.ts +32 -5
  300. package/dist/config.d.ts +38 -4
  301. package/dist/config.js +290 -42
  302. package/dist/context/project_intelligence.d.ts +44 -0
  303. package/dist/context/project_intelligence.js +246 -0
  304. package/dist/desktop/cli_launch.d.ts +8 -0
  305. package/dist/desktop/cli_launch.js +6 -0
  306. package/dist/desktop/cli_shim.d.ts +8 -0
  307. package/dist/desktop/cli_shim.js +120 -0
  308. package/dist/desktop/runtime.d.ts +16 -0
  309. package/dist/desktop/runtime.js +161 -0
  310. package/dist/desktop/worker_launch.d.ts +7 -0
  311. package/dist/desktop/worker_launch.js +7 -0
  312. package/dist/diagnostics/cargo_mutants_selection.d.ts +27 -0
  313. package/dist/diagnostics/cargo_mutants_selection.js +48 -0
  314. package/dist/diagnostics/native_terminal_benchmark.d.ts +58 -0
  315. package/dist/diagnostics/native_terminal_benchmark.js +1247 -0
  316. package/dist/diagnostics/session_evidence_package.d.ts +40 -0
  317. package/dist/diagnostics/session_evidence_package.js +234 -0
  318. package/dist/diagnostics/session_export.d.ts +93 -0
  319. package/dist/diagnostics/session_export.js +634 -0
  320. package/dist/emulation/tool_adapters.js +48 -18
  321. package/dist/emulation/ztc_addendum.d.ts +1 -0
  322. package/dist/emulation/ztc_addendum.js +13 -12
  323. package/dist/environment/service_runner.d.ts +1 -0
  324. package/dist/environment/service_runner.js +68 -0
  325. package/dist/environment/store.d.ts +120 -0
  326. package/dist/environment/store.js +387 -0
  327. package/dist/events.d.ts +40 -1
  328. package/dist/events.js +10 -17
  329. package/dist/goal_contract.d.ts +25 -0
  330. package/dist/goal_contract.js +44 -0
  331. package/dist/headless.d.ts +31 -0
  332. package/dist/headless.js +578 -70
  333. package/dist/headless_cli.js +8 -0
  334. package/dist/headless_cli_args.d.ts +1 -0
  335. package/dist/headless_cli_args.js +1 -0
  336. package/dist/intake/cli.d.ts +1 -0
  337. package/dist/intake/cli.js +33 -0
  338. package/dist/intake/format.d.ts +2 -0
  339. package/dist/intake/format.js +14 -0
  340. package/dist/intake/notifications.d.ts +16 -0
  341. package/dist/intake/notifications.js +132 -0
  342. package/dist/intake/store.d.ts +56 -0
  343. package/dist/intake/store.js +238 -0
  344. package/dist/library.d.ts +8 -0
  345. package/dist/library.js +19 -10
  346. package/dist/mcp/client.d.ts +1 -0
  347. package/dist/mcp/client.js +20 -0
  348. package/dist/modes/builtin/plan.yaml +2 -2
  349. package/dist/modes/handlers.js +1 -1
  350. package/dist/onboarding/first_run.d.ts +3 -0
  351. package/dist/onboarding/first_run.js +33 -25
  352. package/dist/onboarding/setup.d.ts +32 -0
  353. package/dist/onboarding/setup.js +133 -0
  354. package/dist/onboarding/setup_agent_health.d.ts +17 -0
  355. package/dist/onboarding/setup_agent_health.js +79 -0
  356. package/dist/onboarding/setup_environment.d.ts +66 -0
  357. package/dist/onboarding/setup_environment.js +219 -0
  358. package/dist/onboarding/setup_workspace.d.ts +17 -0
  359. package/dist/onboarding/setup_workspace.js +129 -0
  360. package/dist/optimize/adapters/index.d.ts +4 -1
  361. package/dist/optimize/adapters/index.js +7 -0
  362. package/dist/optimize/adapters/kernelbench.js +14 -5
  363. package/dist/optimize/adapters/popcorn.js +3 -2
  364. package/dist/optimize/adapters/profile_benchmark.js +13 -7
  365. package/dist/optimize/adapters/rfbench.js +25 -14
  366. package/dist/optimize/adapters/types.d.ts +3 -0
  367. package/dist/optimize/adapters/zergpod.js +5 -2
  368. package/dist/optimize/baseline.js +10 -21
  369. package/dist/optimize/branch.js +9 -12
  370. package/dist/optimize/combine.js +2 -3
  371. package/dist/optimize/executor.js +34 -48
  372. package/dist/optimize/measurements.d.ts +55 -0
  373. package/dist/optimize/measurements.js +173 -0
  374. package/dist/optimize/results.d.ts +1 -0
  375. package/dist/optimize/results.js +11 -33
  376. package/dist/optimize/tge.js +2 -1
  377. package/dist/panes/initial_input.d.ts +19 -0
  378. package/dist/panes/initial_input.js +206 -0
  379. package/dist/panes/labels.d.ts +16 -0
  380. package/dist/panes/labels.js +42 -0
  381. package/dist/panes/model_catalog.d.ts +3 -3
  382. package/dist/panes/model_catalog.js +28 -4
  383. package/dist/panes/pane_registry.js +6 -2
  384. package/dist/panes/types.d.ts +3 -0
  385. package/dist/panes/ztc_launch.js +19 -0
  386. package/dist/plugin/optimization.d.ts +2 -0
  387. package/dist/plugin/optimization.js +1 -0
  388. package/dist/plugin/runtime.d.ts +2 -0
  389. package/dist/plugin/simulation.d.ts +6 -1
  390. package/dist/plugin/simulation_contract.d.ts +836 -1
  391. package/dist/plugin/simulation_documents.d.ts +22 -0
  392. package/dist/plugin/simulation_documents.js +1 -0
  393. package/dist/plugin/simulation_interventions.d.ts +103 -0
  394. package/dist/plugin/simulation_interventions.js +1 -0
  395. package/dist/plugin/simulation_living.d.ts +1 -0
  396. package/dist/plugin/simulation_living.js +3 -0
  397. package/dist/plugin/simulation_meetings.d.ts +121 -0
  398. package/dist/plugin/simulation_meetings.js +1 -0
  399. package/dist/plugin/simulation_story_review.d.ts +111 -0
  400. package/dist/plugin/simulation_story_review.js +1 -0
  401. package/dist/plugin/simulation_storyteller.d.ts +91 -0
  402. package/dist/plugin/simulation_storyteller.js +1 -0
  403. package/dist/plugin/simulation_structured.d.ts +2 -0
  404. package/dist/plugin/simulation_structured.js +2 -0
  405. package/dist/plugin/simulation_toolkit.d.ts +21 -0
  406. package/dist/plugin/simulation_toolkit.js +24 -0
  407. package/dist/prompts/tool_delegate_task.j2 +1 -1
  408. package/dist/remote-broker/Dockerfile +10 -0
  409. package/dist/remote-broker/archive.py +1227 -0
  410. package/dist/remote-broker/cutover.py +761 -0
  411. package/dist/remote-broker/requirements.txt +5 -0
  412. package/dist/remote-broker/resources.json +6 -0
  413. package/dist/remote-broker/resources.write-staging.example.json +18 -0
  414. package/dist/remote-broker/retained.py +686 -0
  415. package/dist/remote-broker/retained_cleanup.py +550 -0
  416. package/dist/remote-broker/server.py +648 -0
  417. package/dist/remote-broker/worker.py +1007 -0
  418. package/dist/router_auth.d.ts +31 -0
  419. package/dist/router_auth.js +155 -9
  420. package/dist/runbooks/catalog.d.ts +3 -0
  421. package/dist/runbooks/catalog.js +26 -0
  422. package/dist/runbooks/library.d.ts +11 -0
  423. package/dist/runbooks/library.js +95 -0
  424. package/dist/runbooks/package.d.ts +43 -0
  425. package/dist/runbooks/package.js +102 -0
  426. package/dist/session.d.ts +65 -13
  427. package/dist/session.js +548 -43
  428. package/dist/session_handoff.d.ts +25 -0
  429. package/dist/session_handoff.js +322 -0
  430. package/dist/session_handoff_contract.d.ts +89 -0
  431. package/dist/session_handoff_contract.js +179 -0
  432. package/dist/session_media.d.ts +41 -0
  433. package/dist/session_media.js +357 -0
  434. package/dist/session_ownership.d.ts +16 -0
  435. package/dist/session_ownership.js +221 -0
  436. package/dist/session_public.d.ts +10 -2
  437. package/dist/session_public.js +6 -1
  438. package/dist/simulation/dynamic_history.d.ts +42 -0
  439. package/dist/simulation/dynamic_history.js +173 -0
  440. package/dist/simulation/engines/swarm/pane_manager.js +7 -6
  441. package/dist/simulation/inference.d.ts +3 -1
  442. package/dist/simulation/inference.js +19 -1
  443. package/dist/simulation/intervention_generation.d.ts +27 -0
  444. package/dist/simulation/intervention_generation.js +246 -0
  445. package/dist/simulation/intervention_material.d.ts +4 -0
  446. package/dist/simulation/intervention_material.js +81 -0
  447. package/dist/simulation/living/context.d.ts +3130 -0
  448. package/dist/simulation/living/context.js +944 -0
  449. package/dist/simulation/living/definer.d.ts +12 -0
  450. package/dist/simulation/living/definer.js +223 -0
  451. package/dist/simulation/living/definition.d.ts +257 -0
  452. package/dist/simulation/living/definition.js +341 -0
  453. package/dist/simulation/living/frame.d.ts +259 -0
  454. package/dist/simulation/living/frame.js +223 -0
  455. package/dist/simulation/living/identifiers.d.ts +2 -0
  456. package/dist/simulation/living/identifiers.js +12 -0
  457. package/dist/simulation/living/index.d.ts +15 -0
  458. package/dist/simulation/living/index.js +15 -0
  459. package/dist/simulation/living/inference.d.ts +219 -0
  460. package/dist/simulation/living/inference.js +576 -0
  461. package/dist/simulation/living/mechanics.d.ts +54 -0
  462. package/dist/simulation/living/mechanics.js +588 -0
  463. package/dist/simulation/living/model-input.d.ts +25 -0
  464. package/dist/simulation/living/model-input.js +215 -0
  465. package/dist/simulation/living/open-world.d.ts +257 -0
  466. package/dist/simulation/living/open-world.js +102 -0
  467. package/dist/simulation/living/projection.d.ts +7 -0
  468. package/dist/simulation/living/projection.js +31 -0
  469. package/dist/simulation/living/recall.d.ts +17 -0
  470. package/dist/simulation/living/recall.js +25 -0
  471. package/dist/simulation/living/reduce.d.ts +15 -0
  472. package/dist/simulation/living/reduce.js +1853 -0
  473. package/dist/simulation/living/replay.d.ts +6 -0
  474. package/dist/simulation/living/replay.js +22 -0
  475. package/dist/simulation/living/runtime.d.ts +140 -0
  476. package/dist/simulation/living/runtime.js +3020 -0
  477. package/dist/simulation/living/shared-kinds.d.ts +24 -0
  478. package/dist/simulation/living/shared-kinds.js +96 -0
  479. package/dist/simulation/living/types.d.ts +2785 -0
  480. package/dist/simulation/living/types.js +669 -0
  481. package/dist/simulation/llm.d.ts +3 -1
  482. package/dist/simulation/llm.js +90 -30
  483. package/dist/simulation/memory/embedding.d.ts +1 -1
  484. package/dist/simulation/memory/embedding.js +9 -3
  485. package/dist/simulation/memory/filesystem.d.ts +2 -0
  486. package/dist/simulation/memory/filesystem.js +5 -1
  487. package/dist/simulation/memory/filesystem_file_tree.d.ts +4 -1
  488. package/dist/simulation/memory/filesystem_file_tree.js +41 -10
  489. package/dist/simulation/memory/filesystem_invariants.js +1 -2
  490. package/dist/simulation/memory/filesystem_types.d.ts +1 -0
  491. package/dist/simulation/memory/memory.d.ts +2 -0
  492. package/dist/simulation/memory/memory.js +5 -1
  493. package/dist/simulation/orchestration/intent.js +0 -2
  494. package/dist/simulation/orchestration/lifecycle.js +0 -2
  495. package/dist/simulation/plugin_host.d.ts +25 -1
  496. package/dist/simulation/plugin_host.js +334 -17
  497. package/dist/simulation/session_documents.d.ts +20 -0
  498. package/dist/simulation/session_documents.js +62 -0
  499. package/dist/simulation/session_input_validation.d.ts +43 -0
  500. package/dist/simulation/session_input_validation.js +364 -0
  501. package/dist/simulation/session_inputs.d.ts +32 -0
  502. package/dist/simulation/session_inputs.js +154 -0
  503. package/dist/simulation/telemetry.d.ts +11 -14
  504. package/dist/simulation/telemetry.js +177 -54
  505. package/dist/simulation/toolkit/bounded_turns.d.ts +13 -0
  506. package/dist/simulation/toolkit/bounded_turns.js +80 -0
  507. package/dist/simulation/toolkit/dynamic_agents.d.ts +25 -0
  508. package/dist/simulation/toolkit/dynamic_agents.js +218 -0
  509. package/dist/simulation/toolkit/dynamic_digest.d.ts +1 -0
  510. package/dist/simulation/toolkit/dynamic_digest.js +8 -0
  511. package/dist/simulation/toolkit/dynamic_documents.d.ts +7 -0
  512. package/dist/simulation/toolkit/dynamic_documents.js +71 -0
  513. package/dist/simulation/toolkit/dynamic_driver.d.ts +8 -0
  514. package/dist/simulation/toolkit/dynamic_driver.js +269 -0
  515. package/dist/simulation/toolkit/dynamic_game.d.ts +53 -0
  516. package/dist/simulation/toolkit/dynamic_game.js +805 -0
  517. package/dist/simulation/toolkit/dynamic_knowledge.d.ts +15 -0
  518. package/dist/simulation/toolkit/dynamic_knowledge.js +91 -0
  519. package/dist/simulation/toolkit/dynamic_mechanics.d.ts +22 -0
  520. package/dist/simulation/toolkit/dynamic_mechanics.js +910 -0
  521. package/dist/simulation/toolkit/dynamic_meetings.d.ts +239 -0
  522. package/dist/simulation/toolkit/dynamic_meetings.js +350 -0
  523. package/dist/simulation/toolkit/dynamic_progress.d.ts +131 -0
  524. package/dist/simulation/toolkit/dynamic_progress.js +214 -0
  525. package/dist/simulation/toolkit/dynamic_projection.d.ts +9 -0
  526. package/dist/simulation/toolkit/dynamic_projection.js +219 -0
  527. package/dist/simulation/toolkit/dynamic_runtime.d.ts +412 -0
  528. package/dist/simulation/toolkit/dynamic_runtime.js +6536 -0
  529. package/dist/simulation/toolkit/dynamic_state.d.ts +111 -0
  530. package/dist/simulation/toolkit/dynamic_state.js +1967 -0
  531. package/dist/simulation/toolkit/dynamic_story_review.d.ts +508 -0
  532. package/dist/simulation/toolkit/dynamic_story_review.js +333 -0
  533. package/dist/simulation/toolkit/dynamic_storyteller.d.ts +77 -0
  534. package/dist/simulation/toolkit/dynamic_storyteller.js +114 -0
  535. package/dist/simulation/toolkit/dynamic_storyteller_runtime.d.ts +26 -0
  536. package/dist/simulation/toolkit/dynamic_storyteller_runtime.js +126 -0
  537. package/dist/simulation/toolkit/dynamic_world.d.ts +22 -0
  538. package/dist/simulation/toolkit/dynamic_world.js +377 -0
  539. package/dist/simulation/toolkit/structured_output.d.ts +5 -0
  540. package/dist/simulation/toolkit/structured_output.js +321 -0
  541. package/dist/simulation/types/world.d.ts +2 -0
  542. package/dist/simulation/world/artifact_store.js +1 -1
  543. package/dist/simulation/world/memory_manager.d.ts +5 -0
  544. package/dist/simulation/world/memory_manager.js +17 -2
  545. package/dist/simulation/world/state.d.ts +3 -2
  546. package/dist/simulation/world/state.js +15 -5
  547. package/dist/skills/bundled/measurement-validation/SKILL.md +144 -0
  548. package/dist/terminal_attach.d.ts +1 -0
  549. package/dist/terminal_attach.js +113 -0
  550. package/dist/tool_progress.d.ts +3 -0
  551. package/dist/tool_progress.js +27 -0
  552. package/dist/types.d.ts +145 -10
  553. package/dist/types.js +3 -0
  554. package/dist/ui/core/input.d.ts +3 -0
  555. package/dist/ui/core/input_segments.d.ts +18 -1
  556. package/dist/ui/core/input_segments.js +131 -47
  557. package/dist/ui/core/input_state.d.ts +12 -0
  558. package/dist/ui/core/layout_yoga.d.ts +1 -1
  559. package/dist/ui/core/turn_metadata.d.ts +5 -0
  560. package/dist/ui/core/turn_metadata.js +22 -0
  561. package/dist/ui/ink/render.d.ts +2 -1
  562. package/dist/ui/ink/render.js +6 -5
  563. package/dist/ui/ratatui/headless_pane_manager.d.ts +14 -1
  564. package/dist/ui/ratatui/headless_pane_manager.js +293 -104
  565. package/dist/ui/ratatui/index.d.ts +1 -0
  566. package/dist/ui/ratatui/input.d.ts +4 -0
  567. package/dist/ui/ratatui/input.js +15 -0
  568. package/dist/ui/ratatui/ipc_path.d.ts +2 -0
  569. package/dist/ui/ratatui/ipc_path.js +29 -1
  570. package/dist/ui/ratatui/mux_persistence.d.ts +3 -1
  571. package/dist/ui/ratatui/mux_persistence.js +139 -1
  572. package/dist/ui/ratatui/pane_relay.d.ts +1 -1
  573. package/dist/ui/ratatui/pane_relay.js +55 -27
  574. package/dist/ui/ratatui/process.d.ts +17 -1
  575. package/dist/ui/ratatui/process.js +80 -7
  576. package/dist/ui/ratatui/protocol.d.ts +236 -1
  577. package/dist/ui/ratatui/protocol.js +90 -0
  578. package/dist/ui/scrollback.d.ts +2 -1
  579. package/dist/ui/scrollback.js +9 -4
  580. package/dist/ui/stream_typewriter.d.ts +8 -3
  581. package/dist/ui/stream_typewriter.js +30 -1
  582. package/dist/ui/streaming_scrollback.d.ts +1 -0
  583. package/dist/ui/streaming_scrollback.js +1 -1
  584. package/dist/ui/transcript_render_cache.js +10 -0
  585. package/dist/ui/views/activity_line.d.ts +5 -3
  586. package/dist/ui/views/activity_line.js +65 -7
  587. package/dist/ui/views/app.d.ts +6 -3
  588. package/dist/ui/views/app.js +17 -6
  589. package/dist/ui/views/completion_menu.js +10 -5
  590. package/dist/ui/views/input_area.d.ts +7 -3
  591. package/dist/ui/views/input_area.js +15 -9
  592. package/dist/ui/views/message_list.d.ts +4 -1
  593. package/dist/ui/views/message_list.js +89 -19
  594. package/dist/ui/views/queue_panel.d.ts +3 -1
  595. package/dist/ui/views/queue_panel.js +9 -2
  596. package/dist/ui/views/status_bar.d.ts +4 -2
  597. package/dist/ui/views/status_bar.js +28 -13
  598. package/dist/ui/views/theme.d.ts +2 -0
  599. package/dist/ui/views/theme.js +2 -0
  600. package/dist/ui/widgets/color_panel.js +18 -0
  601. package/dist/ui/widgets/form.js +8 -0
  602. package/dist/ui/widgets/log_view.js +7 -0
  603. package/dist/ui/widgets/navigator_panel.js +8 -0
  604. package/dist/ui/widgets/prompt_viewer.js +2 -0
  605. package/dist/ui/widgets/questionnaire.d.ts +2 -0
  606. package/dist/ui/widgets/questionnaire.js +79 -16
  607. package/dist/ui/widgets/resume_panel.d.ts +2 -0
  608. package/dist/ui/widgets/resume_panel.js +26 -15
  609. package/dist/ui/widgets/rewind_panel.d.ts +2 -2
  610. package/dist/ui/widgets/rewind_panel.js +4 -2
  611. package/dist/ui/widgets/select.js +16 -27
  612. package/dist/ui/widgets/settings_panel.js +16 -0
  613. package/dist/ui/widgets/status_line_panel.d.ts +1 -0
  614. package/dist/ui/widgets/status_line_panel.js +30 -4
  615. package/dist/ui/widgets/text_input.d.ts +2 -0
  616. package/dist/ui/widgets/text_input.js +15 -1
  617. package/dist/ui/widgets/types.d.ts +2 -0
  618. package/dist/usage_projection.d.ts +24 -0
  619. package/dist/usage_projection.js +130 -0
  620. package/dist/usage_snapshot.d.ts +36 -0
  621. package/dist/usage_snapshot.js +131 -0
  622. package/dist/utils/assistant_message.d.ts +2 -0
  623. package/dist/utils/assistant_message.js +5 -0
  624. package/dist/utils/atomic_write.d.ts +6 -2
  625. package/dist/utils/atomic_write.js +29 -4
  626. package/dist/utils/checked_shell.d.ts +25 -0
  627. package/dist/utils/checked_shell.js +107 -0
  628. package/dist/utils/compaction.d.ts +4 -1
  629. package/dist/utils/compaction.js +31 -7
  630. package/dist/utils/context.d.ts +4 -4
  631. package/dist/utils/context.js +14 -12
  632. package/dist/utils/deepseek.d.ts +2 -2
  633. package/dist/utils/deepseek.js +13 -5
  634. package/dist/utils/ephemeral.js +4 -0
  635. package/dist/utils/ephemeral_tools.d.ts +18 -0
  636. package/dist/utils/ephemeral_tools.js +97 -0
  637. package/dist/utils/jev_client.d.ts +4 -0
  638. package/dist/utils/jev_client.js +15 -0
  639. package/dist/utils/model_cache.d.ts +15 -0
  640. package/dist/utils/model_cache.js +143 -0
  641. package/dist/utils/models.d.ts +4 -2
  642. package/dist/utils/models.js +12 -16
  643. package/dist/utils/reasoning.d.ts +8 -1
  644. package/dist/utils/reasoning.js +31 -11
  645. package/dist/utils/router_catalog.d.ts +49 -0
  646. package/dist/utils/router_catalog.js +271 -0
  647. package/dist/utils/sandbox.d.ts +9 -0
  648. package/dist/utils/sandbox.js +118 -15
  649. package/dist/utils/shell.d.ts +7 -0
  650. package/dist/utils/shell.js +98 -34
  651. package/dist/utils/subprocess_env.d.ts +3 -0
  652. package/dist/utils/subprocess_env.js +27 -1
  653. package/dist/web/accounts_mirror.d.ts +3 -1
  654. package/dist/web/accounts_mirror.js +13 -9
  655. package/dist/web/child_publication.d.ts +17 -0
  656. package/dist/web/child_publication.js +45 -0
  657. package/dist/web/mirror_authenticator.d.ts +31 -11
  658. package/dist/web/mirror_authenticator.js +67 -17
  659. package/dist/web/mirror_client.d.ts +31 -4
  660. package/dist/web/mirror_client.js +189 -23
  661. package/dist/web/mirror_stream.d.ts +84 -22
  662. package/dist/web/mirror_stream.js +676 -95
  663. package/dist/web/observer_query_redaction.d.ts +6 -0
  664. package/dist/web/observer_query_redaction.js +58 -0
  665. package/dist/web/pane_registry.d.ts +2 -0
  666. package/dist/web/pane_registry.js +86 -22
  667. package/dist/web/tool_image.d.ts +6 -0
  668. package/dist/web/tool_image.js +17 -0
  669. package/dist/web/view_plugin.d.ts +14 -0
  670. package/dist/web/view_plugin.js +23 -1
  671. package/dist/worker/executor.js +31 -12
  672. package/dist/worker/index.d.ts +1 -0
  673. package/dist/worker/toolsets.js +3 -1
  674. package/dist/worker/types.d.ts +12 -1
  675. package/dist/workflows/registry.js +1 -0
  676. package/dist/workflows/registry_handle.d.ts +2 -0
  677. package/dist/workflows/runtime.d.ts +2 -0
  678. package/dist/workflows/runtime.js +1 -0
  679. package/dist/workspace_agent.d.ts +7 -0
  680. package/dist/workspace_agent.js +36 -0
  681. package/dist/workspace_collision.d.ts +76 -0
  682. package/dist/workspace_collision.js +688 -0
  683. package/dist/workspace_layout.d.ts +18 -0
  684. package/dist/workspace_layout.js +121 -0
  685. package/dist/workspace_runtime.d.ts +44 -0
  686. package/dist/workspace_runtime.js +82 -0
  687. package/dist/workspace_session_context.d.ts +62 -0
  688. package/dist/workspace_session_context.js +160 -0
  689. package/dist/zlm-embed.mjs +32518 -9615
  690. package/dist/zlm.js +1 -0
  691. package/node_modules/@zerg/model-contracts/LICENSE.md +375 -0
  692. package/node_modules/@zerg/model-contracts/README.md +90 -0
  693. package/node_modules/@zerg/model-contracts/index.d.ts +20 -0
  694. package/node_modules/@zerg/model-contracts/index.js +223 -0
  695. package/node_modules/@zerg/model-contracts/package.json +8 -0
  696. package/node_modules/glob/node_modules/brace-expansion/index.js +78 -22
  697. package/node_modules/glob/node_modules/brace-expansion/package.json +1 -1
  698. package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
  699. package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
  700. package/package.json +45 -24
  701. package/packages/model-contracts/LICENSE.md +375 -0
  702. package/packages/model-contracts/README.md +90 -0
  703. package/packages/model-contracts/index.d.ts +20 -0
  704. package/packages/model-contracts/index.js +223 -0
  705. package/packages/model-contracts/package.json +8 -0
  706. package/remote-broker/Dockerfile +10 -0
  707. package/remote-broker/README.md +308 -0
  708. package/remote-broker/archive.py +1227 -0
  709. package/remote-broker/cutover.py +761 -0
  710. package/remote-broker/requirements.txt +5 -0
  711. package/remote-broker/resources.example.json +6 -0
  712. package/remote-broker/resources.write-staging.example.json +18 -0
  713. package/remote-broker/retained.py +686 -0
  714. package/remote-broker/retained_cleanup.py +550 -0
  715. package/remote-broker/server.py +648 -0
  716. package/scripts/build-remote-broker.mjs +13 -0
  717. package/scripts/ensure-local-packages.mjs +6 -0
  718. package/scripts/node-pty-helpers.mjs +103 -0
package/dist/App.js CHANGED
@@ -1,14 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PromptTurn } from './app/prompt_turn.js';
2
3
  import React, { useState, useCallback, useMemo, useRef } from 'react';
3
4
  import { Box, Text, useApp, useInput, Static } from 'ink';
4
5
  import { Header, MessageList, SingleMessage, InputArea, StatusBar, FullScreen, ActivityLine, useScreenSize, FormWidget, QueuePanel } from './components/index.js';
5
6
  import { AgentError } from './agent/index.js';
6
7
  import { commands } from './agent/commands/index.js';
8
+ import { goalManager } from './agent/commands/goal.js';
9
+ import { listCommandJobs, subscribeCommandJobs } from './agent/runtime/command_jobs.js';
10
+ import { normalizeInputMode } from './types.js';
7
11
  import { DEFAULT_TOAST_MS, MAX_QUEUE } from './app/controller.js';
8
12
  import { getQueuePreview } from './ui/views/queue_panel.js';
9
13
  import { estimateInputLines } from './ui/views/input_area.js';
10
14
  import { configStore } from './config.js';
15
+ import { applyAccountOp } from './web/accounts_mirror.js';
16
+ import { createUsageSnapshot, mergeUsageSnapshots, recordUsageReceipt } from './usage_snapshot.js';
11
17
  import { resolveMessageDisplaySettings } from './config/message_display.js';
18
+ import { initialStreamTypewriterState, planStreamTypewriterSample, STREAM_TYPEWRITER_DEFAULTS } from './ui/stream_typewriter.js';
12
19
  import { createInputBus, isControlKey } from './ui/core/input.js';
13
20
  import { loadEmulationProfiles, getEmulationProfile } from './emulation/catalog.js';
14
21
  import { getTraceStyle } from './emulation/trace_style.js';
@@ -21,6 +28,20 @@ import { formatToolStart, formatToolError, buildToolOutputMessage } from './util
21
28
  import { autoActivateSkills, buildSkillPrompt } from './skills/index.js';
22
29
  import { getSkills } from './skills/registry.js';
23
30
  import { createAgentFromConfig } from './agent/factory.js';
31
+ import { hasWorkspaceCollisionRisk, LocalWorkspaceCoordinator, inspectWorkspaceSessions, prepareWorkspaceIsolation, WorkspaceIsolationError } from './workspace_collision.js';
32
+ import { generateSessionId, saveSession } from './session.js';
33
+ import { prepareSessionMessages } from './app/session_messages.js';
34
+ import { captureSessionDiagnosticSnapshot } from './diagnostics/session_export.js';
35
+ import { runtimePluginRegistry } from './plugin/runtime_registry.js';
36
+ import { classifyBusyInput } from './app/task_scope_router.js';
37
+ import { formatWorkspaceAdmissionDetails } from './app/workspace_admission_details.js';
38
+ import { reconcileOrderedTranscript } from './app/ordered_transcript.js';
39
+ import { advanceEscapeStop, isEscapeStopArmed, ESCAPE_STOP_WINDOW_MS } from './app/escape_stop.js';
40
+ import { SelectWidget } from './ui/widgets/select.js';
41
+ import { QuestionnaireWidget } from './ui/widgets/questionnaire.js';
42
+ import { setAskUserCallback, clearAskUserCallback, resetAskUserTurnFlag } from './agent/tools/ask_user.js';
43
+ import { formatAskUserResponse } from './app/ask_user_response.js';
44
+ import { InkNode } from './ui/ink/render.js';
24
45
  import { getModeConfig, getNextMode, } from './app/modes.js';
25
46
  import { getModeRegistry } from './modes/index.js';
26
47
  import { isMeaningfulUserInput } from './app/input_validation.js';
@@ -35,10 +56,11 @@ import { isNativeDictationAvailable, onDictationEvent } from './utils/dictation_
35
56
  import { getContextStatus, contextStatusFromUsed } from './utils/context.js';
36
57
  import { compactMessages, localSummarizer } from './utils/compaction.js';
37
58
  import { speak, optionsFromConfig, isProviderAvailable } from './utils/tts.js';
38
- import { applyAgentEventToState, buildActivityDisplayState, getAgentActivityPlaceholder } from './app/activity.js';
59
+ import { applyAgentEventToState, buildActivityDisplayState, formatApiRetryNotice, getAgentActivityPlaceholder } from './app/activity.js';
39
60
  import { buildCredentialSetupMessage, buildNoCredentialsWelcome, buildReadyWelcome, createFirstRunLaunchMessage, isFirstRunWelcomeMessage, } from './onboarding/first_run.js';
40
61
  // --- Utilities ---
41
62
  const generateId = () => Math.random().toString(36).slice(2, 11);
63
+ const ESCAPE_CONFIRMATION_HINT = 'Esc again to stop';
42
64
  /**
43
65
  * Render a VU meter from dB level
44
66
  * @param db Audio level in dB (-60 to 0)
@@ -116,7 +138,7 @@ function getWelcomeMessage(runtime) {
116
138
  };
117
139
  }
118
140
  // --- Create agent if possible ---
119
- function createAgent(mode) {
141
+ function createAgent(mode, skillPrompt, workspace) {
120
142
  // Can create agent if we have an API key OR are authenticated
121
143
  const hasKey = configStore.hasApiKey();
122
144
  const isAuthenticated = configStore.isAuthenticated();
@@ -132,7 +154,9 @@ function createAgent(mode) {
132
154
  openaiCompatibleBaseUrl: configStore.getOpenAICompatibleBaseUrl(),
133
155
  emulationId: configStore.getEmulationId(),
134
156
  mode,
157
+ skillPrompt,
135
158
  allowSelfPlanTransition: false,
159
+ ...workspace,
136
160
  });
137
161
  }
138
162
  // --- Main App ---
@@ -146,30 +170,111 @@ export const App = () => {
146
170
  const messageDisplay = resolveMessageDisplaySettings(configStore.get());
147
171
  const shellCwdRef = React.useRef(process.cwd());
148
172
  // State
149
- const [messages, setMessages] = useState([getWelcomeMessage()]);
173
+ const [messages, updateMessages] = useState([getWelcomeMessage()]);
150
174
  const messagesRef = useRef(messages);
151
- const [agentState, setAgentState] = useState({ status: 'idle' });
175
+ const readingStatesRef = useRef(new Map());
176
+ const bypassedReadingIdsRef = useRef(new Set());
177
+ const [readingTick, refreshReadingDisplay] = useState(0);
178
+ const [, refreshActivityClock] = useState(0);
179
+ const flushReadingDisplay = useCallback(() => {
180
+ for (const id of readingStatesRef.current.keys())
181
+ bypassedReadingIdsRef.current.add(id);
182
+ readingStatesRef.current.clear();
183
+ refreshReadingDisplay(tick => tick + 1);
184
+ }, []);
185
+ const setMessages = useCallback((next) => {
186
+ const value = typeof next === 'function' ? next(messagesRef.current) : next;
187
+ messagesRef.current = value;
188
+ updateMessages(value);
189
+ }, []);
190
+ const [agentState, updateAgentState] = useState({ status: 'idle' });
191
+ const setAgentState = useCallback((next) => {
192
+ updateAgentState(previous => {
193
+ const value = typeof next === 'function' ? next(previous) : next;
194
+ return { ...value, evidence: value.evidence ?? previous.evidence,
195
+ workspaceCollision: value.workspaceCollision ?? previous.workspaceCollision };
196
+ });
197
+ }, []);
152
198
  // Latest agentState for closures that must not capture a stale snapshot (the
153
199
  // command context getters below read the real per-call context count live).
154
200
  const agentStateRef = useRef(agentState);
155
201
  agentStateRef.current = agentState;
156
- const [sessionId] = useState(generateId());
202
+ const [sessionId, setSessionId] = useState(generateSessionId);
203
+ const sessionIdRef = useRef(sessionId);
204
+ sessionIdRef.current = sessionId;
205
+ const readingSessionRef = useRef(sessionId);
206
+ const [sessionName, setSessionName] = useState();
157
207
  const [agentMode, setAgentMode] = useState('normal');
158
- const [agent, setAgent] = useState(createAgent);
208
+ const [agent, setAgent] = useState(null);
209
+ const diagnosticAgentRef = useRef(null);
210
+ diagnosticAgentRef.current = agent;
211
+ const retainedUsageRef = useRef({ sessionId, snapshot: createUsageSnapshot() });
212
+ const [routingUsageRevision, setRoutingUsageRevision] = useState(0);
213
+ const getUsageSnapshot = useCallback(() => {
214
+ if (retainedUsageRef.current.sessionId !== sessionId)
215
+ retainedUsageRef.current = { sessionId, snapshot: createUsageSnapshot() };
216
+ const current = diagnosticAgentRef.current;
217
+ if (current?.getUsageSessionId() === sessionId)
218
+ retainedUsageRef.current.snapshot = mergeUsageSnapshots(retainedUsageRef.current.snapshot, current.getUsageSnapshot());
219
+ return structuredClone(retainedUsageRef.current.snapshot);
220
+ }, [sessionId]);
221
+ const usageSaveQueueRef = useRef(Promise.resolve());
222
+ const lastUsageSaveRef = useRef('');
223
+ const retiredAgentsRef = useRef(new WeakSet());
159
224
  const [expandToolOutputs, setExpandToolOutputs] = useState(false);
160
225
  const [skills, setSkills] = useState([]);
161
- const [inputMode, setInputMode] = useState('queue');
226
+ const [inputMode, setInputMode] = useState('steer');
162
227
  const [queue, setQueue] = useState([]);
163
228
  const queueRef = useRef(queue);
164
- React.useEffect(() => { queueRef.current = queue; }, [queue]);
229
+ const queuePausedRef = useRef(false);
165
230
  const flushingRef = useRef(false);
231
+ const [flushing, setFlushing] = useState(false);
166
232
  const prevStatusRef = useRef('idle');
167
233
  const activeRunIdRef = useRef(null);
168
234
  const runCounterRef = useRef(0);
169
235
  const abortControllerRef = useRef(null);
236
+ const runtimeMutationRef = useRef(false);
237
+ const accountReloadPendingRef = useRef(false);
238
+ const [escapeStopArm, setEscapeStopArm] = useState(null);
239
+ const escapeStopArmRef = useRef(null);
240
+ const escapeStopTimerRef = useRef(null);
170
241
  const lastRunDurationRef = useRef(null);
171
242
  const pendingPromptRef = useRef(null);
172
243
  const formCallbacksRef = useRef(new Map());
244
+ const questionnaireWidgetRef = useRef(null);
245
+ const questionnaireInputReadyRef = useRef(false);
246
+ const questionnaireEventOwnedRef = useRef(false);
247
+ const agentQuestionRef = useRef(null);
248
+ const [questionnaireVersion, renderQuestionnaireWidget] = useState(0);
249
+ const scopeWidgetRef = useRef(null);
250
+ const [, renderScopeWidget] = useState(0);
251
+ const routingEpochRef = useRef(0);
252
+ const routingTailRef = useRef(Promise.resolve());
253
+ const routingControllersRef = useRef(new Map());
254
+ const routingDraftsRef = useRef(new Map());
255
+ const routingChoiceCancelRef = useRef(null);
256
+ const workspaceWidgetRef = useRef(null);
257
+ const [, renderWorkspaceWidget] = useState(0);
258
+ const workspaceChoiceCancelRef = useRef(null);
259
+ const workspaceAdmissionTailRef = useRef(Promise.resolve());
260
+ const workspacePausedRef = useRef(false);
261
+ const pausedWorkspaceInputRef = useRef(null);
262
+ const pausedWorkspaceOperationRef = useRef(null);
263
+ const isolatedWorkspaceResumeRef = useRef(null);
264
+ const workspaceIsolationRunningRef = useRef(false);
265
+ const [workspaceResumeVersion, setWorkspaceResumeVersion] = useState(0);
266
+ const workspaceSubmitRef = useRef(async () => { throw new Error('Workspace input is not initialized.'); });
267
+ const workspaceClosedRef = useRef(false);
268
+ const workspaceCoordinator = useMemo(() => new LocalWorkspaceCoordinator({
269
+ sessionId, getCwd: () => shellCwdRef.current, kind: 'interactive',
270
+ getWork: () => ({ sessionId: sessionIdRef.current, messages: messagesRef.current,
271
+ goal: goalManager.get(), jobs: listCommandJobs(sessionIdRef.current) }),
272
+ onSnapshot: snapshot => {
273
+ if (!workspaceClosedRef.current && snapshot.current.sessionId === sessionIdRef.current)
274
+ setAgentState(previous => ({ ...previous, workspaceCollision: snapshot }));
275
+ },
276
+ }), [sessionId, setAgentState]);
277
+ const workspaceReadyRef = useRef(null);
173
278
  React.useEffect(() => {
174
279
  let active = true;
175
280
  let runtimeTimer;
@@ -184,7 +289,9 @@ export const App = () => {
184
289
  return prev;
185
290
  });
186
291
  };
187
- configStore.load(true).then(() => {
292
+ // Mount reuses settings already loaded by the host. A forced disk reload
293
+ // can overwrite a newer interactive selection while its save is pending.
294
+ configStore.load(false).then(() => {
188
295
  if (!active)
189
296
  return;
190
297
  refreshUntouchedWelcome();
@@ -211,10 +318,27 @@ export const App = () => {
211
318
  history: [],
212
319
  historyIdx: -1
213
320
  });
321
+ const [clearDraftVersion, setClearDraftVersion] = useState(0);
214
322
  const lastRequestRef = useRef(null);
323
+ const promptTurnRef = useRef(null);
324
+ const [, setPromptClock] = useState(0);
215
325
  const [retryAvailable, setRetryAvailable] = useState(false);
216
326
  const [toast, setToast] = useState(null);
217
327
  const toastTimerRef = useRef(null);
328
+ const resetEscapeStop = useCallback(() => {
329
+ if (escapeStopTimerRef.current)
330
+ clearTimeout(escapeStopTimerRef.current);
331
+ escapeStopTimerRef.current = null;
332
+ escapeStopArmRef.current = null;
333
+ setEscapeStopArm(null);
334
+ setToast(current => current === ESCAPE_CONFIRMATION_HINT ? null : current);
335
+ }, []);
336
+ React.useEffect(() => () => {
337
+ if (escapeStopTimerRef.current)
338
+ clearTimeout(escapeStopTimerRef.current);
339
+ if (toastTimerRef.current)
340
+ clearTimeout(toastTimerRef.current);
341
+ }, []);
218
342
  const [isRecording, setIsRecording] = useState(false);
219
343
  const [recordingDevice, setRecordingDevice] = useState(null);
220
344
  const [isTranscribing, setIsTranscribing] = useState(false);
@@ -237,8 +361,8 @@ export const App = () => {
237
361
  // the provider-reported window (Codex) as the denominator. Only before the
238
362
  // first API response (no real count yet) do we fall back to the char/4 estimate.
239
363
  const contextStatus = useMemo(() => (agentState.contextTokens && agentState.contextTokens > 0)
240
- ? contextStatusFromUsed(agentState.contextTokens, currentModel, agentState.contextWindow)
241
- : getContextStatus(messages, currentModel), [agentState.contextTokens, agentState.contextWindow, messages, currentModel]);
364
+ ? contextStatusFromUsed(agentState.contextTokens, currentModel, agentState.contextWindow ?? configStore.getModelContextWindow(currentModel))
365
+ : getContextStatus(messages, currentModel, configStore.getModelContextWindow(currentModel)), [agentState.contextTokens, agentState.contextWindow, messages, currentModel]);
242
366
  const spinnerVerbs = configStore.get().spinnerVerbs || DEFAULT_SPINNER_VERBS;
243
367
  const spinnerVerbsKey = spinnerVerbs.join('|');
244
368
  const queuePreview = useMemo(() => getQueuePreview(queue), [queue]);
@@ -246,10 +370,8 @@ export const App = () => {
246
370
  renderCount.current += 1;
247
371
  debugLog(`App render #${renderCount.current} (messages=${messages.length}, state=${agentState.status})`);
248
372
  });
249
- React.useEffect(() => {
250
- messagesRef.current = messages;
251
- }, [messages]);
252
373
  const [debug, setDebug] = useState(false);
374
+ const [transcriptGeneration, setTranscriptGeneration] = useState(0);
253
375
  const scrollback = process.env.ZTC_SCROLLBACK === '1' || process.env.ZTC_ALT_SCREEN !== '1';
254
376
  // Spinner label effect - picks a random verb when agent starts working
255
377
  React.useEffect(() => {
@@ -274,7 +396,7 @@ export const App = () => {
274
396
  return;
275
397
  }
276
398
  setSpinnerLabel(label);
277
- }, [agentState.status, agentState.currentTool, spinnerVerbsKey]);
399
+ }, [agentState.status, agentState.currentTool, agentState.requestRetry, agentState.waiting, spinnerVerbsKey]);
278
400
  // Spinner frame animation effect
279
401
  React.useEffect(() => {
280
402
  const isActive = agentState.status === 'thinking' || agentState.status === 'streaming' || agentState.status === 'tool_use';
@@ -299,6 +421,14 @@ export const App = () => {
299
421
  clearInterval(interval);
300
422
  };
301
423
  }, [agentState.status, scrollback]);
424
+ // Scrollback keeps a static spinner, but elapsed time must advance even when
425
+ // the provider or tool emits nothing. Idle/unmounted apps own no clock timer.
426
+ React.useEffect(() => {
427
+ if (!scrollback || !['thinking', 'streaming', 'tool_use'].includes(agentState.status))
428
+ return;
429
+ const timer = setInterval(() => refreshActivityClock(tick => tick + 1), 1_000);
430
+ return () => clearInterval(timer);
431
+ }, [agentState.status, scrollback]);
302
432
  const headerHeight = scrollback ? 0 : 4; // border (2) + content (1) + margin (1)
303
433
  const statusHeight = 1;
304
434
  const suggestionLines = 4;
@@ -323,10 +453,6 @@ export const App = () => {
323
453
  }, [columns, estimateBadgePreviewLines, inputSnapshot, rows]);
324
454
  // Calculate content height (total - header - input - status)
325
455
  const contentHeight = useMemo(() => scrollback ? undefined : Math.max(rows - (headerHeight + inputHeight + statusHeight), 5), [rows, scrollback, inputHeight]);
326
- // Reload agent when config changes
327
- const reloadAgent = useCallback(() => {
328
- setAgent(createAgent());
329
- }, []);
330
456
  React.useEffect(() => {
331
457
  let active = true;
332
458
  getSkills()
@@ -339,27 +465,66 @@ export const App = () => {
339
465
  return () => { active = false; };
340
466
  }, []);
341
467
  // Keyboard shortcuts
468
+ const ownsLocalForm = formCallbacksRef.current.size > 0;
342
469
  useInput((input, key) => {
343
- if (isControlKey(input, key, 'c')) {
344
- if (!cancelGeneration())
345
- exit();
470
+ if (!key.escape)
471
+ resetEscapeStop();
472
+ if (workspaceWidgetRef.current?.isActive()) {
473
+ if (key.escape)
474
+ resetEscapeStop();
475
+ workspaceWidgetRef.current.handleInput(input, key);
476
+ return;
477
+ }
478
+ if (questionnaireWidgetRef.current?.isActive()) {
479
+ // Every subscriber to this same key must respect its widget owner,
480
+ // even when answering closes the widget before the composer sees it.
481
+ questionnaireEventOwnedRef.current = true;
482
+ queueMicrotask(() => { questionnaireEventOwnedRef.current = false; });
483
+ resetEscapeStop();
484
+ if (questionnaireInputReadyRef.current)
485
+ questionnaireWidgetRef.current.handleInput(input, key);
486
+ return;
487
+ }
488
+ if (scopeWidgetRef.current?.isActive()) {
489
+ if (key.escape)
490
+ resetEscapeStop();
491
+ scopeWidgetRef.current.handleInput(input, key);
492
+ return;
346
493
  }
347
494
  if (key.escape) {
348
- // Esc precedence: active run → cancel; else idle + empty composer +
349
- // non-empty queue → drop newest; else cancel (no-op).
350
- const hasActiveRun = abortControllerRef.current !== null;
351
- const composerEmpty = inputSnapshot.segments.length === 0;
352
- if (hasActiveRun) {
353
- cancelGeneration();
495
+ if (ownsLocalForm) {
496
+ resetEscapeStop();
497
+ // The focused FormWidget consumes this same key locally.
498
+ return;
354
499
  }
355
- else if (composerEmpty && queueRef.current.length > 0) {
356
- const newest = queueRef.current[queueRef.current.length - 1];
357
- removeQueueEntry(newest.id);
358
- showToastRef.current?.('Dropped queued message');
500
+ if (pendingPromptRef.current) {
501
+ resetEscapeStop();
502
+ retireRouting();
503
+ pendingPromptRef.current = null;
504
+ setClearDraftVersion(version => version + 1);
505
+ addMessage({ role: 'system', content: 'Cancelled.' });
506
+ return;
359
507
  }
360
- else {
508
+ const next = advanceEscapeStop(escapeStopArmRef.current, abortControllerRef.current, Date.now());
509
+ resetEscapeStop();
510
+ if (next.confirmed) {
511
+ queuePausedRef.current = true;
361
512
  cancelGeneration();
513
+ return;
514
+ }
515
+ if (next.arm) {
516
+ const arm = next.arm;
517
+ escapeStopArmRef.current = arm;
518
+ setEscapeStopArm(arm);
519
+ // The policy includes the exact 2000ms boundary.
520
+ escapeStopTimerRef.current = setTimeout(() => {
521
+ if (escapeStopArmRef.current !== arm)
522
+ return;
523
+ resetEscapeStop();
524
+ }, ESCAPE_STOP_WINDOW_MS + 1);
525
+ showToastRef.current?.(ESCAPE_CONFIRMATION_HINT, ESCAPE_STOP_WINDOW_MS);
362
526
  }
527
+ return;
363
528
  }
364
529
  if (isControlKey(input, key, 'l')) {
365
530
  setMessages([{
@@ -377,50 +542,542 @@ export const App = () => {
377
542
  const addMessage = useCallback((msg) => {
378
543
  setMessages(prev => [...prev, {
379
544
  ...msg,
545
+ ...(msg.role === 'assistant' && !msg.turn && promptTurnRef.current ? { turn: promptTurnRef.current.snapshot() } : {}),
380
546
  id: generateId(),
381
547
  timestamp: new Date()
382
548
  }]);
383
549
  }, []);
384
550
  const showToastRef = useRef(null);
551
+ const questionnaireWidget = useMemo(() => new QuestionnaireWidget({
552
+ render: () => renderQuestionnaireWidget(version => version + 1),
553
+ addSystemMessage: content => addMessage({ role: 'system', content }),
554
+ setToast: (content, durationMs) => {
555
+ if (content)
556
+ showToastRef.current?.(content, durationMs);
557
+ else
558
+ setToast(null);
559
+ },
560
+ writeClipboard,
561
+ }), [addMessage]);
562
+ questionnaireWidgetRef.current = questionnaireWidget;
563
+ React.useEffect(() => () => {
564
+ questionnaireInputReadyRef.current = false;
565
+ questionnaireWidget.cancel();
566
+ }, [questionnaireWidget]);
567
+ const retireAgentQuestion = useCallback(() => {
568
+ if (!agentQuestionRef.current)
569
+ return;
570
+ agentQuestionRef.current = null;
571
+ questionnaireInputReadyRef.current = false;
572
+ questionnaireWidget.cancel();
573
+ renderQuestionnaireWidget(version => version + 1);
574
+ }, [questionnaireWidget]);
575
+ React.useEffect(() => {
576
+ setAskUserCallback(questions => {
577
+ const runId = activeRunIdRef.current;
578
+ const owner = lastRequestRef.current?.agent;
579
+ if (!runId || !owner || workspaceClosedRef.current || !abortControllerRef.current
580
+ || abortControllerRef.current.signal.aborted || retiredAgentsRef.current.has(owner))
581
+ return 'Question input has no active owning agent turn.';
582
+ if (pendingPromptRef.current || formCallbacksRef.current.size > 0 || questionnaireWidget.isActive())
583
+ return 'Another interactive input owns this session.';
584
+ const pending = agentQuestionRef.current;
585
+ if (pending && (pending.runId !== runId || pending.agent !== owner))
586
+ return 'Another agent turn owns the pending question.';
587
+ if (pending)
588
+ pending.questions.push(...structuredClone(questions));
589
+ else
590
+ agentQuestionRef.current = { runId, sessionId: sessionIdRef.current, agent: owner,
591
+ questions: structuredClone(questions), shown: false };
592
+ return null;
593
+ });
594
+ return () => {
595
+ clearAskUserCallback();
596
+ agentQuestionRef.current = null;
597
+ questionnaireInputReadyRef.current = false;
598
+ questionnaireWidget.cancel();
599
+ };
600
+ }, [questionnaireWidget]);
601
+ React.useEffect(() => { retireAgentQuestion(); }, [agent, sessionId, retireAgentQuestion]);
602
+ const presentAgentQuestion = useCallback((runId) => {
603
+ const owner = agentQuestionRef.current;
604
+ if (!owner || owner.runId !== runId || owner.shown)
605
+ return;
606
+ if (workspaceClosedRef.current || owner.sessionId !== sessionIdRef.current
607
+ || owner.agent !== diagnosticAgentRef.current || retiredAgentsRef.current.has(owner.agent)) {
608
+ retireAgentQuestion();
609
+ return;
610
+ }
611
+ owner.shown = true;
612
+ questionnaireInputReadyRef.current = false;
613
+ questionnaireWidget.show({
614
+ title: owner.questions.length === 1 ? 'Question' : 'Questions',
615
+ questions: owner.questions.map((question, index) => ({ name: `q${index}`,
616
+ header: owner.questions.length > 1 ? `Q${index + 1}` : undefined,
617
+ question: question.question, type: 'select', required: true,
618
+ options: question.options.map(option => ({ value: option.label, ...option })),
619
+ })),
620
+ allowOther: owner.questions[0]?.allow_other ?? true,
621
+ allowOptionNotes: true,
622
+ callback: async (answers) => {
623
+ if (agentQuestionRef.current !== owner || workspaceClosedRef.current
624
+ || owner.sessionId !== sessionIdRef.current || owner.agent !== diagnosticAgentRef.current
625
+ || retiredAgentsRef.current.has(owner.agent))
626
+ return;
627
+ agentQuestionRef.current = null;
628
+ questionnaireInputReadyRef.current = false;
629
+ await workspaceSubmitRef.current(answers === null ? '__SKIP__' : formatAskUserResponse(owner.questions, answers), undefined, undefined, true);
630
+ },
631
+ });
632
+ queueMicrotask(() => { if (agentQuestionRef.current === owner)
633
+ questionnaireInputReadyRef.current = true; });
634
+ }, [questionnaireWidget, retireAgentQuestion]);
635
+ const scopeWidget = useMemo(() => new SelectWidget({
636
+ render: () => renderScopeWidget(version => version + 1),
637
+ addSystemMessage: content => addMessage({ role: 'system', content }),
638
+ setToast: content => { if (content)
639
+ showToastRef.current?.(content); },
640
+ writeClipboard,
641
+ }), [addMessage]);
642
+ scopeWidgetRef.current = scopeWidget;
643
+ const retireRouting = useCallback((restore = true) => {
644
+ resetEscapeStop();
645
+ routingEpochRef.current += 1;
646
+ for (const controller of routingControllersRef.current.values())
647
+ controller.abort();
648
+ routingControllersRef.current.clear();
649
+ if (restore)
650
+ for (const draft of routingDraftsRef.current.values())
651
+ draft?.restore();
652
+ routingDraftsRef.current.clear();
653
+ routingChoiceCancelRef.current?.();
654
+ routingChoiceCancelRef.current = null;
655
+ scopeWidget.cancel();
656
+ routingTailRef.current = Promise.resolve();
657
+ renderScopeWidget(version => version + 1);
658
+ }, [resetEscapeStop, scopeWidget]);
659
+ React.useEffect(() => () => { retireRouting(false); }, [retireRouting]);
385
660
  /** Returns the post-enqueue size, or null if the queue was full. */
386
- const enqueueMessage = useCallback((text, delivery = 'end-of-turn') => {
661
+ const enqueueMessage = useCallback((text, delivery = 'end-of-turn', submitted) => {
387
662
  const current = queueRef.current;
388
- if (current.length >= MAX_QUEUE) {
663
+ const reserved = routingDraftsRef.current.size
664
+ - (submitted && routingDraftsRef.current.has(submitted.id) ? 1 : 0);
665
+ if (current.length + reserved >= MAX_QUEUE) {
389
666
  showToastRef.current?.(`Queue full (${MAX_QUEUE}). Cancel or wait.`);
390
667
  return null;
391
668
  }
392
- const entry = {
669
+ const entry = submitted ?? {
393
670
  id: generateId(),
394
671
  text,
395
672
  queuedAt: new Date(),
396
673
  delivery,
397
674
  };
398
675
  const nextSize = current.length + 1;
399
- setQueue(prev => (prev.length >= MAX_QUEUE ? prev : [...prev, entry]));
676
+ queueRef.current = [...current, entry];
677
+ setQueue(queueRef.current);
400
678
  return nextSize;
401
679
  }, []);
402
680
  const dequeueMessage = useCallback(() => {
403
681
  const current = queueRef.current;
404
682
  if (current.length === 0)
405
683
  return undefined;
406
- const head = current[0];
407
- setQueue(prev => prev.slice(1));
408
- return head;
409
- }, []);
410
- const removeQueueEntry = useCallback((id) => {
411
- setQueue(prev => prev.filter(e => e.id !== id));
684
+ // A steering correction that missed its tool boundary gets the next
685
+ // request before Tab's explicit after-turn work.
686
+ const index = Math.max(0, current.findIndex(entry => entry.delivery === 'subturn'));
687
+ const entry = current[index];
688
+ queueRef.current = current.filter(candidate => candidate.id !== entry.id);
689
+ setQueue(queueRef.current);
690
+ return entry;
412
691
  }, []);
413
692
  const cancelGeneration = useCallback(() => {
693
+ retireAgentQuestion();
694
+ resetEscapeStop();
695
+ flushReadingDisplay();
414
696
  if (abortControllerRef.current) {
697
+ retireRouting();
698
+ if (lastRequestRef.current)
699
+ retiredAgentsRef.current.add(lastRequestRef.current.agent);
415
700
  activeRunIdRef.current = null;
416
701
  abortControllerRef.current.abort();
417
702
  abortControllerRef.current = null;
703
+ streamingMessageId.current = null;
704
+ streamedResponse.current = false;
705
+ setMessages(previous => previous.map(message => message.isStreaming
706
+ ? { ...message, isStreaming: false } : message));
418
707
  setAgentState({ status: 'idle' });
419
- addMessage({ role: 'system', content: 'Generation cancelled' });
708
+ showToastRef.current?.('Stopped by you');
420
709
  return true;
421
710
  }
422
711
  return false;
423
- }, [addMessage]);
712
+ }, [resetEscapeStop, retireRouting, flushReadingDisplay, retireAgentQuestion]);
713
+ const workspaceWidget = useMemo(() => new SelectWidget({
714
+ render: () => renderWorkspaceWidget(version => version + 1),
715
+ addSystemMessage: content => addMessage({ role: 'system', content }),
716
+ setToast: content => { if (content)
717
+ showToastRef.current?.(content); },
718
+ writeClipboard,
719
+ }), [addMessage]);
720
+ workspaceWidgetRef.current = workspaceWidget;
721
+ React.useEffect(() => {
722
+ workspaceClosedRef.current = false;
723
+ workspaceReadyRef.current = workspaceCoordinator.start();
724
+ const unsubscribeJobs = subscribeCommandJobs(sessionId, () => {
725
+ if (!workspaceClosedRef.current && sessionIdRef.current === sessionId)
726
+ void workspaceCoordinator.refresh().catch(error => debugLog(`[workspace.jobs] ${String(error)}`));
727
+ });
728
+ void workspaceReadyRef.current.catch(error => {
729
+ if (!workspaceClosedRef.current)
730
+ addMessage({ role: 'system', content: `Workspace discovery unavailable: ${error instanceof Error ? error.message : String(error)}` });
731
+ });
732
+ return () => {
733
+ workspaceClosedRef.current = true;
734
+ unsubscribeJobs();
735
+ workspaceChoiceCancelRef.current?.();
736
+ pausedWorkspaceOperationRef.current?.cancel();
737
+ pausedWorkspaceOperationRef.current = null;
738
+ workspaceWidget.cancel();
739
+ void workspaceCoordinator.close().catch(error => debugLog(`[workspace.close] ${String(error)}`));
740
+ };
741
+ }, [sessionId, workspaceCoordinator, workspaceWidget, addMessage]);
742
+ const refreshWorkspace = useCallback(async () => {
743
+ if (workspaceReadyRef.current)
744
+ await workspaceReadyRef.current;
745
+ return workspaceCoordinator.refresh();
746
+ }, [workspaceCoordinator]);
747
+ const readWorkspace = useCallback(async (cwd) => {
748
+ const snapshot = await refreshWorkspace();
749
+ return cwd === undefined ? snapshot : inspectWorkspaceSessions(cwd, sessionId);
750
+ }, [refreshWorkspace, sessionId]);
751
+ const admitWorkspace = useCallback((signal, cwd) => {
752
+ const admission = workspaceAdmissionTailRef.current.then(async () => {
753
+ let snapshot = await readWorkspace(cwd);
754
+ if (workspacePausedRef.current || workspaceClosedRef.current || signal?.aborted)
755
+ return { allowed: false, snapshot };
756
+ while (hasWorkspaceCollisionRisk(snapshot) && !workspaceCoordinator.isAcknowledged(snapshot)) {
757
+ resetEscapeStop();
758
+ const choice = await new Promise(resolve => {
759
+ let settled = false;
760
+ const finish = (value) => {
761
+ if (settled)
762
+ return;
763
+ settled = true;
764
+ signal?.removeEventListener('abort', abort);
765
+ workspaceChoiceCancelRef.current = null;
766
+ workspaceWidget.cancel();
767
+ renderWorkspaceWidget(version => version + 1);
768
+ resolve(value);
769
+ };
770
+ const abort = () => finish(null);
771
+ workspaceChoiceCancelRef.current = () => finish(null);
772
+ if (signal?.aborted || workspaceClosedRef.current) {
773
+ finish(null);
774
+ return;
775
+ }
776
+ signal?.addEventListener('abort', abort, { once: true });
777
+ workspaceWidget.show({ title: 'Workspace overlap',
778
+ question: formatWorkspaceAdmissionDetails(snapshot),
779
+ type: 'select', allowOther: false,
780
+ options: [{ value: 'continue', label: 'Continue here' },
781
+ { value: 'isolate', label: 'Isolate this session' }, { value: 'pause', label: 'Pause this session' }],
782
+ callback: value => finish(typeof value === 'string' ? value : null),
783
+ });
784
+ });
785
+ if (signal?.aborted || workspaceClosedRef.current)
786
+ return { allowed: false, snapshot };
787
+ if (choice === 'continue') {
788
+ const checked = await readWorkspace(cwd);
789
+ if (checked.fingerprint !== snapshot.fingerprint) {
790
+ snapshot = checked;
791
+ continue;
792
+ }
793
+ workspaceCoordinator.acknowledge(checked);
794
+ return { allowed: true, snapshot: checked };
795
+ }
796
+ workspacePausedRef.current = true;
797
+ queuePausedRef.current = true;
798
+ const paused = await workspaceCoordinator.setActivity('paused');
799
+ if (cwd === undefined)
800
+ snapshot = paused;
801
+ return { allowed: false, snapshot, isolate: choice === 'isolate' };
802
+ }
803
+ return { allowed: true, snapshot };
804
+ });
805
+ workspaceAdmissionTailRef.current = admission.then(() => { }, () => { });
806
+ return admission;
807
+ }, [readWorkspace, workspaceCoordinator, workspaceWidget, resetEscapeStop, addMessage]);
808
+ const workspaceFactoryOptions = useCallback(() => ({
809
+ cwd: shellCwdRef.current, sessionId,
810
+ toolEffectObserver: async (event) => {
811
+ if (event.effect === 'read')
812
+ return;
813
+ const admission = await admitWorkspace(event.signal, event.cwd);
814
+ if (!admission.allowed) {
815
+ if (workspaceClosedRef.current || event.signal?.aborted)
816
+ throw new AgentError('Workspace execution cancelled before this managed effect.', 'CANCELLED');
817
+ if (pausedWorkspaceOperationRef.current)
818
+ throw new AgentError('Workspace paused with an earlier operation retained.', 'CANCELLED');
819
+ const owner = abortControllerRef.current?.signal === event.signal ? abortControllerRef.current : null;
820
+ await new Promise((resolve, reject) => {
821
+ let settled = false;
822
+ let resuming = false;
823
+ const finish = (error) => {
824
+ if (settled)
825
+ return;
826
+ settled = true;
827
+ event.signal?.removeEventListener('abort', abort);
828
+ if (pausedWorkspaceOperationRef.current === operation)
829
+ pausedWorkspaceOperationRef.current = null;
830
+ if (error)
831
+ reject(error);
832
+ else
833
+ resolve();
834
+ };
835
+ const abort = () => finish(new AgentError('Workspace execution cancelled before this managed effect.', 'CANCELLED'));
836
+ const operation = {
837
+ kind: 'tool', cwd: event.cwd ?? shellCwdRef.current,
838
+ fingerprint: admission.snapshot.fingerprint,
839
+ resume: async () => {
840
+ if (settled || resuming)
841
+ return;
842
+ resuming = true;
843
+ try {
844
+ const checked = await admitWorkspace(event.signal, event.cwd);
845
+ if (workspaceClosedRef.current || event.signal?.aborted) {
846
+ abort();
847
+ return;
848
+ }
849
+ if (!checked.allowed) {
850
+ operation.fingerprint = checked.snapshot.fingerprint;
851
+ return;
852
+ }
853
+ await workspaceCoordinator.setActivity('running');
854
+ if (workspaceClosedRef.current || event.signal?.aborted) {
855
+ abort();
856
+ return;
857
+ }
858
+ finish();
859
+ }
860
+ catch (error) {
861
+ if (!settled) {
862
+ workspacePausedRef.current = true;
863
+ queuePausedRef.current = true;
864
+ }
865
+ throw error;
866
+ }
867
+ finally {
868
+ resuming = false;
869
+ }
870
+ },
871
+ cancel: () => { owner?.abort(); abort(); },
872
+ };
873
+ pausedWorkspaceOperationRef.current = operation;
874
+ event.signal?.addEventListener('abort', abort, { once: true });
875
+ if (event.signal?.aborted || workspaceClosedRef.current) {
876
+ abort();
877
+ return;
878
+ }
879
+ addMessage({ role: 'system', content: 'Workspace session paused. The managed operation is retained; use /sessions continue.' });
880
+ });
881
+ }
882
+ },
883
+ onLocalFileChange: async (paths, cwd) => { await workspaceCoordinator.recordObservedPaths(paths, cwd); },
884
+ }), [admitWorkspace, addMessage, sessionId, workspaceCoordinator]);
885
+ const createBoundAgent = useCallback((mode, skillPrompt) => {
886
+ const next = createAgent(mode, skillPrompt, workspaceFactoryOptions());
887
+ const previous = diagnosticAgentRef.current;
888
+ if (next) {
889
+ next.restoreUsageSnapshot(getUsageSnapshot());
890
+ if (previous?.getDiagnosticSessionId() === next.getDiagnosticSessionId()) {
891
+ next.restoreDiagnosticSnapshot(captureSessionDiagnosticSnapshot({
892
+ messages: messagesRef.current, diagnostics: previous.getDiagnosticSnapshot(),
893
+ }));
894
+ }
895
+ next.recordDiagnosticEvent('runtime', { renderer: 'ink', rendererVersion: null, nativeCapabilities: null });
896
+ next.recordDiagnosticEvent('mode', { mode: mode ?? 'normal', source: 'agent_construction' });
897
+ diagnosticAgentRef.current = next;
898
+ }
899
+ return next;
900
+ }, [workspaceFactoryOptions, getUsageSnapshot]);
901
+ // Every reconstruction retains workspace admission and successful-file observation.
902
+ const reloadAgent = useCallback(() => {
903
+ retireAgentQuestion();
904
+ const previous = diagnosticAgentRef.current;
905
+ const next = createBoundAgent();
906
+ diagnosticAgentRef.current = next;
907
+ if (previous && previous !== next) {
908
+ retiredAgentsRef.current.add(previous);
909
+ previous.dispose();
910
+ }
911
+ if (lastRequestRef.current && next)
912
+ lastRequestRef.current = { ...lastRequestRef.current, agent: next };
913
+ setAgent(next);
914
+ }, [createBoundAgent, retireAgentQuestion]);
915
+ const applyAccountOperation = useCallback(async (body) => {
916
+ if (runtimeMutationRef.current || abortControllerRef.current
917
+ || (agentStateRef.current.status !== 'idle' && agentStateRef.current.status !== 'error'))
918
+ return { ok: false, blocked: true, error: 'An agent turn or tool is active; wait before switching accounts.' };
919
+ runtimeMutationRef.current = true;
920
+ let operationOpen = true;
921
+ try {
922
+ return await applyAccountOp(body, configStore, () => {
923
+ if (!operationOpen && (abortControllerRef.current || runtimeMutationRef.current)) {
924
+ accountReloadPendingRef.current = true;
925
+ addMessage({ role: 'system', content: 'Account sign-in completed; reload will apply after this turn finishes.' });
926
+ return;
927
+ }
928
+ reloadAgent();
929
+ });
930
+ }
931
+ finally {
932
+ operationOpen = false;
933
+ runtimeMutationRef.current = false;
934
+ }
935
+ }, [reloadAgent, addMessage]);
936
+ React.useEffect(() => { setAgent(createBoundAgent()); }, [createBoundAgent]);
937
+ React.useEffect(() => {
938
+ if ((agentState.status !== 'idle' && agentState.status !== 'error') || abortControllerRef.current)
939
+ return;
940
+ if (accountReloadPendingRef.current && !runtimeMutationRef.current) {
941
+ accountReloadPendingRef.current = false;
942
+ reloadAgent();
943
+ }
944
+ const usage = getUsageSnapshot();
945
+ if (!usage.receipts.length && !usage.omittedReceipts)
946
+ return;
947
+ const signature = JSON.stringify([sessionId, usage]);
948
+ if (signature === lastUsageSaveRef.current)
949
+ return;
950
+ const messages = structuredClone(messagesRef.current);
951
+ const runtime = { mode: agentMode, usage, evidence: agent?.getEvidenceSnapshot(),
952
+ diagnostics: captureSessionDiagnosticSnapshot({ messages, diagnostics: agent?.getDiagnosticSnapshot() }) };
953
+ const cwd = shellCwdRef.current;
954
+ usageSaveQueueRef.current = usageSaveQueueRef.current.then(async () => {
955
+ await saveSession(sessionId, cwd, prepareSessionMessages(messages), configStore.getModel(), configStore.getProvider(), undefined, undefined, runtime);
956
+ lastUsageSaveRef.current = signature;
957
+ }).catch(error => { addMessage({ role: 'system', content: `Usage checkpoint could not be saved: ${String(error)}` }); });
958
+ }, [agentState.status, agent, agentMode, sessionId, getUsageSnapshot, reloadAgent, addMessage, routingUsageRevision]);
959
+ const isolateWorkspace = useCallback(async () => {
960
+ if (workspaceIsolationRunningRef.current || workspaceClosedRef.current)
961
+ return false;
962
+ if (pausedWorkspaceOperationRef.current) {
963
+ addMessage({ role: 'system', content: 'The paused operation belongs to the current execution context. Use /sessions continue to finish it, then isolate a fresh task; the original operation is retained.' });
964
+ return false;
965
+ }
966
+ if (configStore.get().remoteResources !== undefined || process.env.ZTC_HOST_RUNTIME !== undefined
967
+ || runtimePluginRegistry.resolveHostRuntime({ mode: agentMode })) {
968
+ addMessage({ role: 'system', content: 'This host or guarded remote session owns its execution context. Local isolation cannot transfer its conversation or remote lease. Your input is retained; use /sessions continue here or open a separate local session.' });
969
+ return false;
970
+ }
971
+ workspaceIsolationRunningRef.current = true;
972
+ workspacePausedRef.current = true;
973
+ queuePausedRef.current = true;
974
+ const oldSessionId = sessionId;
975
+ let retainedTarget = null;
976
+ try {
977
+ cancelGeneration();
978
+ retireRouting();
979
+ const snapshot = await workspaceCoordinator.setActivity('paused');
980
+ await saveSession(oldSessionId, shellCwdRef.current, prepareSessionMessages(structuredClone(messagesRef.current)), configStore.getModel(), configStore.getProvider(), undefined, undefined, { mode: agentMode, usage: getUsageSnapshot(), evidence: agent?.getEvidenceSnapshot(), diagnostics: captureSessionDiagnosticSnapshot({ messages: messagesRef.current, diagnostics: agent?.getDiagnosticSnapshot() }) });
981
+ const isolated = await prepareWorkspaceIsolation(snapshot);
982
+ retainedTarget = isolated.cwd;
983
+ const nextSessionId = generateSessionId();
984
+ await saveSession(nextSessionId, isolated.cwd, [], configStore.getModel(), configStore.getProvider(), undefined, undefined, { mode: agentMode });
985
+ if (workspaceClosedRef.current || sessionIdRef.current !== oldSessionId)
986
+ return false;
987
+ const pending = pausedWorkspaceInputRef.current;
988
+ isolatedWorkspaceResumeRef.current = { sessionId: nextSessionId,
989
+ ...(pending ? { text: pending.text, queuedEntry: pending.queuedEntry, askUserAnswer: pending.askUserAnswer } : {}) };
990
+ if (agent) {
991
+ retiredAgentsRef.current.add(agent);
992
+ agent.dispose();
993
+ }
994
+ lastRequestRef.current = null;
995
+ activeSkillIds.current = [];
996
+ toolStartTimes.current.clear();
997
+ streamingMessageId.current = null;
998
+ streamedResponse.current = false;
999
+ shellCwdRef.current = isolated.cwd;
1000
+ sessionIdRef.current = nextSessionId;
1001
+ setAgent(null);
1002
+ setMessages([]);
1003
+ updateAgentState({ status: 'idle' });
1004
+ setClearDraftVersion(version => version + 1);
1005
+ setSessionId(nextSessionId);
1006
+ addMessage({ role: 'system', content: `Workspace isolated: ${isolated.cwd}\nSession: ${nextSessionId}\nPrevious session saved: ${oldSessionId}. Source edits and index preserved.` });
1007
+ return true;
1008
+ }
1009
+ catch (error) {
1010
+ if (error instanceof WorkspaceIsolationError)
1011
+ retainedTarget = error.retainedTarget;
1012
+ if (!workspaceClosedRef.current)
1013
+ addMessage({ role: 'system', content: `Workspace isolation failed: ${error instanceof Error ? error.message : String(error)}${retainedTarget ? `\nRetained target: ${retainedTarget}` : ''}\nYour current session, input and queued work are retained; use /sessions continue.` });
1014
+ return false;
1015
+ }
1016
+ finally {
1017
+ workspaceIsolationRunningRef.current = false;
1018
+ }
1019
+ }, [addMessage, agent, agentMode, cancelGeneration, retireRouting, sessionId, setMessages, workspaceCoordinator]);
1020
+ React.useEffect(() => {
1021
+ const pending = isolatedWorkspaceResumeRef.current;
1022
+ if (!pending || pending.sessionId !== sessionId)
1023
+ return;
1024
+ // This effect runs after the fresh coordinator and Agent bindings are installed.
1025
+ // Keep the queue paused until the new session owns the submitted input.
1026
+ void refreshWorkspace().then(async () => {
1027
+ if (workspaceClosedRef.current || sessionIdRef.current !== pending.sessionId)
1028
+ return;
1029
+ isolatedWorkspaceResumeRef.current = null;
1030
+ pausedWorkspaceInputRef.current = null;
1031
+ workspacePausedRef.current = false;
1032
+ queuePausedRef.current = false;
1033
+ if (pending.text !== undefined) {
1034
+ flushingRef.current = true;
1035
+ setFlushing(true);
1036
+ try {
1037
+ await workspaceSubmitRef.current(pending.text, undefined, pending.queuedEntry, pending.askUserAnswer);
1038
+ }
1039
+ finally {
1040
+ flushingRef.current = false;
1041
+ setFlushing(false);
1042
+ }
1043
+ }
1044
+ setWorkspaceResumeVersion(version => version + 1);
1045
+ }).catch(error => {
1046
+ if (!workspaceClosedRef.current)
1047
+ addMessage({ role: 'system', content: `The isolated session could not resume: ${error instanceof Error ? error.message : String(error)}. Use /sessions continue.` });
1048
+ });
1049
+ }, [sessionId, refreshWorkspace, addMessage]);
1050
+ const continueWorkspace = useCallback(async () => {
1051
+ const pending = pausedWorkspaceInputRef.current;
1052
+ const operation = pausedWorkspaceOperationRef.current;
1053
+ const snapshot = await readWorkspace(operation?.cwd);
1054
+ if ((operation?.fingerprint ?? pending?.fingerprint) === snapshot.fingerprint)
1055
+ workspaceCoordinator.acknowledge(snapshot);
1056
+ workspacePausedRef.current = false;
1057
+ queuePausedRef.current = false;
1058
+ await workspaceCoordinator.setActivity('idle');
1059
+ if (pending || operation) {
1060
+ pausedWorkspaceInputRef.current = null;
1061
+ // A suspended tool stays reachable for cancellation until its owner resolves.
1062
+ if (operation?.kind !== 'tool')
1063
+ pausedWorkspaceOperationRef.current = null;
1064
+ flushingRef.current = true;
1065
+ setFlushing(true);
1066
+ try {
1067
+ if (operation?.kind === 'tool')
1068
+ await operation.resume();
1069
+ if (pending)
1070
+ await workspaceSubmitRef.current(pending.text, undefined, pending.queuedEntry, pending.askUserAnswer);
1071
+ if (operation && operation.kind !== 'tool')
1072
+ await operation.resume();
1073
+ }
1074
+ finally {
1075
+ flushingRef.current = false;
1076
+ setFlushing(false);
1077
+ }
1078
+ }
1079
+ setWorkspaceResumeVersion(version => version + 1);
1080
+ }, [readWorkspace, workspaceCoordinator]);
424
1081
  React.useEffect(() => {
425
1082
  if (process.env.ZTC_DISABLE_UPDATE === '1')
426
1083
  return;
@@ -495,8 +1152,13 @@ export const App = () => {
495
1152
  });
496
1153
  }, [contextStatus, agentState.status, messages, currentModel, addMessage]);
497
1154
  const clearMessages = useCallback(() => {
1155
+ retireAgentQuestion();
1156
+ resetEscapeStop();
1157
+ retireRouting();
1158
+ flushReadingDisplay();
498
1159
  setMessages([]);
499
- }, []);
1160
+ setTranscriptGeneration(generation => generation + 1);
1161
+ }, [resetEscapeStop, retireRouting, setMessages, flushReadingDisplay, retireAgentQuestion]);
500
1162
  const getMessages = useCallback(() => messages, [messages]);
501
1163
  const shellController = useMemo(() => ({
502
1164
  getCwd: () => shellCwdRef.current,
@@ -507,156 +1169,245 @@ export const App = () => {
507
1169
  if (agent) {
508
1170
  agent.setCwd(next);
509
1171
  }
1172
+ await refreshWorkspace();
510
1173
  return next;
511
1174
  },
512
- run: async (command) => runShellCommand(command, shellCwdRef.current)
513
- }), [agent]);
1175
+ run: (command) => {
1176
+ const cwd = shellCwdRef.current;
1177
+ return new Promise((resolve, reject) => {
1178
+ const execute = async () => {
1179
+ const admission = await admitWorkspace(undefined, cwd);
1180
+ if (workspaceClosedRef.current)
1181
+ throw new Error('Workspace command cancelled when the session closed.');
1182
+ if (!admission.allowed) {
1183
+ if (pausedWorkspaceOperationRef.current)
1184
+ throw new Error('Workspace paused with an earlier operation retained. Continue that operation, then resubmit this command.');
1185
+ pausedWorkspaceOperationRef.current = { kind: 'shell', cwd,
1186
+ fingerprint: admission.snapshot.fingerprint, resume: execute,
1187
+ cancel: () => reject(new Error('Workspace command cancelled when the session closed.')) };
1188
+ addMessage({ role: 'system', content: 'Workspace session paused. The shell command is retained; use /sessions continue.' });
1189
+ return;
1190
+ }
1191
+ resolve(await runShellCommand(command, cwd));
1192
+ };
1193
+ void execute().catch(reject);
1194
+ });
1195
+ }
1196
+ }), [agent, refreshWorkspace, admitWorkspace, addMessage]);
514
1197
  const isRetryableError = useCallback((message) => {
515
1198
  const lower = message.toLowerCase();
516
1199
  return lower.includes('overloaded') || lower.includes('529') || lower.includes('429') || lower.includes('rate limit');
517
1200
  }, []);
518
- const runWithRetry = useCallback(async (requestMessages, runAgent, isManual = false, isActive, signal) => {
1201
+ const runWithRetry = useCallback(async (requestMessages, runAgent, isManual = false, isActive, signal, takeSteering, promptTurn = promptTurnRef.current) => {
519
1202
  const maxRetries = 3;
520
1203
  let attempt = 0;
521
1204
  setRetryAvailable(false);
522
- while (attempt < maxRetries) {
523
- attempt += 1;
524
- if (isActive && !isActive())
525
- return;
526
- if (signal?.aborted)
527
- return;
528
- try {
529
- streamedResponse.current = false;
530
- const priorIds = new Set(requestMessages.map(message => message.id));
531
- const stopCheckpoints = runAgent.on(event => {
532
- if (event.type !== 'history_checkpoint')
533
- return;
534
- const ordered = event.messages;
535
- setMessages(prev => [...prev.filter(message => priorIds.has(message.id)), ...ordered,
536
- ...prev.filter(message => !priorIds.has(message.id) && (message.role === 'system' || message.role === 'tool'))]);
537
- });
538
- let result;
539
- try {
540
- result = await runAgent.run(requestMessages, signal);
541
- }
542
- finally {
543
- stopCheckpoints();
544
- }
1205
+ promptTurn?.start();
1206
+ try {
1207
+ while (attempt < maxRetries) {
1208
+ attempt += 1;
545
1209
  if (isActive && !isActive())
546
1210
  return;
547
- if (!streamedResponse.current) {
548
- // Only add assistant message if there's actual text content.
549
- // Tool calls are already displayed via tool_start/tool_end events.
550
- // When the model returns empty content after tool execution, don't show empty message.
551
- const hasContent = result.content && result.content.trim().length > 0;
552
- if (hasContent) {
553
- addMessage({
554
- role: 'assistant',
555
- content: result.content,
556
- toolCalls: result.turnMessages?.length ? undefined : result.toolCalls.length > 0 ? result.toolCalls : undefined,
557
- // Keep the provider's reasoning trace alongside usage: reasoning
558
- // models expect the assistant message replayed with its
559
- // reasoning_content, and dropping it here strips every later turn.
560
- metadata: (result.usage || result.reasoning)
561
- ? {
562
- ...(result.usage ? { usage: result.usage } : {}),
563
- ...(result.reasoning ? { reasoningContent: result.reasoning } : {}),
564
- }
565
- : undefined
1211
+ if (signal?.aborted)
1212
+ return;
1213
+ try {
1214
+ streamedResponse.current = false;
1215
+ const iterations = [];
1216
+ const toolIterations = new Map();
1217
+ const stopCheckpoints = runAgent.on(event => {
1218
+ if (isActive && !isActive())
1219
+ return;
1220
+ if (event.type === 'thinking_start')
1221
+ iterations.push({ start: messagesRef.current.length });
1222
+ if (event.type === 'tool_start') {
1223
+ const iteration = iterations.length - 1;
1224
+ if (iteration >= 0) {
1225
+ // The live listener has just painted this tool's start trace.
1226
+ iterations[iteration].toolStart ??= messagesRef.current.length - 1;
1227
+ toolIterations.set(event.toolCallId, iteration);
1228
+ }
1229
+ }
1230
+ if (event.type !== 'history_checkpoint')
1231
+ return;
1232
+ setMessages(live => reconcileOrderedTranscript({ live, ordered: event.messages, iterations, toolIterations }));
1233
+ });
1234
+ let result;
1235
+ try {
1236
+ result = await runAgent.run(requestMessages, signal, takeSteering, undefined, undefined, promptTurn?.observe);
1237
+ }
1238
+ finally {
1239
+ stopCheckpoints();
1240
+ }
1241
+ if (isActive && !isActive())
1242
+ return;
1243
+ if (!streamedResponse.current) {
1244
+ // Only add assistant message if there's actual text content.
1245
+ // Tool calls are already displayed via tool_start/tool_end events.
1246
+ // When the model returns empty content after tool execution, don't show empty message.
1247
+ const hasContent = result.content && result.content.trim().length > 0;
1248
+ if (hasContent) {
1249
+ addMessage({
1250
+ role: 'assistant',
1251
+ content: result.content,
1252
+ ...(result.nativeContent ? { nativeContent: result.nativeContent, nativeModel: result.nativeModel, nativeContextId: result.nativeContextId } : {}),
1253
+ toolCalls: result.turnMessages?.length ? undefined : result.toolCalls.length > 0 ? result.toolCalls : undefined,
1254
+ // Keep the provider's reasoning trace alongside usage: reasoning
1255
+ // models expect the assistant message replayed with its
1256
+ // reasoning_content, and dropping it here strips every later turn.
1257
+ metadata: (result.usage || result.reasoning)
1258
+ ? {
1259
+ ...(result.usage ? { usage: result.usage } : {}),
1260
+ ...(result.reasoning ? { reasoningContent: result.reasoning } : {}),
1261
+ }
1262
+ : undefined
1263
+ });
1264
+ }
1265
+ }
1266
+ else if (result.reasoning || result.nativeContent) {
1267
+ // A streamed turn already put its assistant message in state, so the
1268
+ // reasoning trace has to be attached after the fact. Without this,
1269
+ // only buffered replies keep it and streamed turns — the common case —
1270
+ // replay a stripped history.
1271
+ setMessages(prev => {
1272
+ const lastIdx = prev.reduce((last, m, i) => (isPersistableAssistantMessage(m) ? i : last), -1);
1273
+ if (lastIdx < 0)
1274
+ return prev;
1275
+ return prev.map((m, i) => i === lastIdx
1276
+ ? { ...m, ...(result.nativeContent ? { nativeContent: result.nativeContent, nativeModel: result.nativeModel, nativeContextId: result.nativeContextId } : {}),
1277
+ metadata: { ...m.metadata, ...(result.reasoning ? { reasoningContent: result.reasoning } : {}) } }
1278
+ : m);
566
1279
  });
567
1280
  }
1281
+ // Idle publication can synchronously flush queued work. Retire the
1282
+ // completed owner's confirmation before publishing that transition.
1283
+ if (abortControllerRef.current?.signal === signal) {
1284
+ abortControllerRef.current = null;
1285
+ resetEscapeStop();
1286
+ }
1287
+ setAgentState(s => ({
1288
+ status: 'idle',
1289
+ tokensUsed: result.usage?.totalTokens || 0,
1290
+ // Carry the PER-CALL context the last token_usage event set; do NOT use
1291
+ // result.usage.inputTokens — it is the cumulative cross-iteration sum
1292
+ // (mergeUsage) and would inflate the bar. (currentTool/error/startedAt
1293
+ // are intentionally dropped on idle, as before.)
1294
+ contextTokens: s.contextTokens,
1295
+ contextWindow: s.contextWindow,
1296
+ cacheInputTokens: s.cacheInputTokens,
1297
+ cachedInputTokens: s.cachedInputTokens,
1298
+ tokensEstimated: s.tokensEstimated,
1299
+ authError: false
1300
+ }));
1301
+ return;
568
1302
  }
569
- else if (result.reasoning) {
570
- // A streamed turn already put its assistant message in state, so the
571
- // reasoning trace has to be attached after the fact. Without this,
572
- // only buffered replies keep it and streamed turns — the common case —
573
- // replay a stripped history.
574
- setMessages(prev => {
575
- const lastIdx = prev.reduce((last, m, i) => (isPersistableAssistantMessage(m) ? i : last), -1);
576
- if (lastIdx < 0)
577
- return prev;
578
- return prev.map((m, i) => i === lastIdx
579
- ? { ...m, metadata: { ...m.metadata, reasoningContent: result.reasoning } }
580
- : m);
1303
+ catch (err) {
1304
+ const errorMsg = err.message || 'Agent error';
1305
+ if ((err instanceof AgentError && err.code === 'CANCELLED') || signal?.aborted)
1306
+ return;
1307
+ if (isActive && !isActive())
1308
+ return;
1309
+ if (!isRetryableError(errorMsg) || attempt >= maxRetries) {
1310
+ throw err;
1311
+ }
1312
+ const delayMs = 500 * Math.pow(2, attempt - 1);
1313
+ addMessage({
1314
+ role: 'system',
1315
+ content: `Overloaded. Retrying in ${Math.round(delayMs / 100) / 10}s... (attempt ${attempt + 1}/${maxRetries})`
581
1316
  });
1317
+ await new Promise(resolve => setTimeout(resolve, delayMs));
582
1318
  }
583
- setAgentState(s => ({
584
- status: 'idle',
585
- tokensUsed: result.usage?.totalTokens || 0,
586
- // Carry the PER-CALL context the last token_usage event set; do NOT use
587
- // result.usage.inputTokens — it is the cumulative cross-iteration sum
588
- // (mergeUsage) and would inflate the bar. (currentTool/error/startedAt
589
- // are intentionally dropped on idle, as before.)
590
- contextTokens: s.contextTokens,
591
- contextWindow: s.contextWindow,
592
- cacheInputTokens: s.cacheInputTokens,
593
- cachedInputTokens: s.cachedInputTokens,
594
- tokensEstimated: s.tokensEstimated,
595
- authError: false
596
- }));
597
- return;
598
- }
599
- catch (err) {
600
- const errorMsg = err.message || 'Agent error';
601
- if ((err instanceof AgentError && err.code === 'CANCELLED') || signal?.aborted)
602
- return;
603
- if (isActive && !isActive())
604
- return;
605
- if (!isRetryableError(errorMsg) || attempt >= maxRetries) {
606
- throw err;
607
- }
608
- const delayMs = 500 * Math.pow(2, attempt - 1);
609
- addMessage({
610
- role: 'system',
611
- content: `Overloaded. Retrying in ${Math.round(delayMs / 100) / 10}s... (attempt ${attempt + 1}/${maxRetries})`
612
- });
613
- await new Promise(resolve => setTimeout(resolve, delayMs));
614
1319
  }
615
1320
  }
616
- }, [addMessage, isRetryableError, setAgentState]);
1321
+ finally {
1322
+ promptTurn?.finish();
1323
+ }
1324
+ }, [addMessage, isRetryableError, resetEscapeStop, setAgentState]);
617
1325
  const retryLast = useCallback(() => {
618
1326
  const last = lastRequestRef.current;
619
1327
  if (!last) {
620
1328
  addMessage({ role: 'system', content: 'No previous request to retry.' });
621
1329
  return;
622
1330
  }
623
- const runId = `${Date.now()}_${runCounterRef.current++}`;
624
- activeRunIdRef.current = runId;
625
- const isActive = () => activeRunIdRef.current === runId;
626
- const retryController = new AbortController();
627
- abortControllerRef.current = retryController;
628
- void runWithRetry(last.messages, last.agent, true, isActive, retryController.signal).catch((err) => {
629
- const message = err.message || 'Agent error';
630
- if (!isActive())
1331
+ const replay = async () => {
1332
+ const cwd = last.agent.getCwd();
1333
+ const admission = await admitWorkspace(undefined, cwd);
1334
+ if (workspaceClosedRef.current)
1335
+ return;
1336
+ if (!admission.allowed) {
1337
+ if (!pausedWorkspaceOperationRef.current)
1338
+ pausedWorkspaceOperationRef.current = {
1339
+ kind: 'retry', cwd, fingerprint: admission.snapshot.fingerprint, resume: replay, cancel: () => { },
1340
+ };
1341
+ addMessage({ role: 'system', content: 'Workspace session paused. The original retry request is retained; use /sessions continue.' });
631
1342
  return;
632
- addMessage({ role: 'system', content: `Error: ${message}` });
633
- if (isRetryableError(message)) {
634
- addMessage({ role: 'system', content: 'Retries exhausted. Use /retry to try again.' });
635
- setRetryAvailable(true);
636
1343
  }
637
- setAgentState({ status: 'error', error: message });
638
- }).finally(() => {
639
- if (abortControllerRef.current === retryController)
640
- abortControllerRef.current = null;
1344
+ const runId = `${Date.now()}_${runCounterRef.current++}`;
1345
+ flushReadingDisplay();
1346
+ activeRunIdRef.current = runId;
1347
+ resetAskUserTurnFlag();
1348
+ const isActive = () => activeRunIdRef.current === runId;
1349
+ const retryController = new AbortController();
1350
+ resetEscapeStop();
1351
+ queuePausedRef.current = false;
1352
+ abortControllerRef.current = retryController;
1353
+ await workspaceCoordinator.setActivity('running');
1354
+ try {
1355
+ await runWithRetry(last.messages, last.agent, true, isActive, retryController.signal);
1356
+ }
1357
+ catch (err) {
1358
+ const message = err.message || 'Agent error';
1359
+ if (!isActive())
1360
+ return;
1361
+ addMessage({ role: 'system', content: `Error: ${message}` });
1362
+ if (isRetryableError(message)) {
1363
+ addMessage({ role: 'system', content: 'Retries exhausted. Use /retry to try again.' });
1364
+ setRetryAvailable(true);
1365
+ }
1366
+ setAgentState({ status: 'error', error: message });
1367
+ }
1368
+ finally {
1369
+ if (abortControllerRef.current === retryController) {
1370
+ abortControllerRef.current = null;
1371
+ resetEscapeStop();
1372
+ }
1373
+ if (isActive() && !workspaceClosedRef.current && !workspacePausedRef.current)
1374
+ await workspaceCoordinator.setActivity('idle');
1375
+ if (isActive()) {
1376
+ activeRunIdRef.current = null;
1377
+ presentAgentQuestion(runId);
1378
+ }
1379
+ }
1380
+ };
1381
+ void replay().catch(error => {
1382
+ if (!workspaceClosedRef.current)
1383
+ addMessage({ role: 'system', content: `Unable to retry: ${error instanceof Error ? error.message : String(error)}` });
641
1384
  });
642
- }, [addMessage, isRetryableError, runWithRetry, setAgentState]);
1385
+ }, [addMessage, isRetryableError, resetEscapeStop, runWithRetry, setAgentState, admitWorkspace, workspaceCoordinator, flushReadingDisplay, presentAgentQuestion]);
643
1386
  // Prompt controller for interactive commands
644
1387
  const promptController = useMemo(() => ({
645
- prompt: (message, callback) => {
646
- pendingPromptRef.current = { message, callback, isPassword: false };
1388
+ prompt: (message, callback, options) => {
1389
+ retireRouting();
1390
+ pendingPromptRef.current = { message, callback, isPassword: false, allowEmptySubmit: options?.allowEmptySubmit };
647
1391
  addMessage({ role: 'system', content: message });
648
1392
  },
649
1393
  promptPassword: (message, callback) => {
1394
+ retireRouting();
650
1395
  pendingPromptRef.current = { message, callback, isPassword: true };
651
1396
  addMessage({ role: 'system', content: message });
652
1397
  },
653
1398
  hasPending: () => pendingPromptRef.current !== null,
654
1399
  getPending: () => pendingPromptRef.current,
655
- clearPending: () => { pendingPromptRef.current = null; },
656
- }), [addMessage]);
1400
+ clearPending: () => {
1401
+ const passwordDismissed = pendingPromptRef.current?.isPassword === true;
1402
+ pendingPromptRef.current = null;
1403
+ if (passwordDismissed)
1404
+ setClearDraftVersion(version => version + 1);
1405
+ },
1406
+ }), [addMessage, retireRouting]);
657
1407
  // Form controller for inline form widgets
658
1408
  const formController = useMemo(() => ({
659
1409
  showForm: (options, callback) => {
1410
+ retireRouting();
660
1411
  const formId = generateId();
661
1412
  formCallbacksRef.current.set(formId, callback);
662
1413
  setMessages(prev => [...prev, {
@@ -676,7 +1427,7 @@ export const App = () => {
676
1427
  cancelForm: (formId) => {
677
1428
  formCallbacksRef.current.delete(formId);
678
1429
  }
679
- }), []);
1430
+ }), [retireRouting, setMessages]);
680
1431
  // Handle form submission from FormWidget
681
1432
  const handleFormSubmit = useCallback(async (formId, values) => {
682
1433
  const callback = formCallbacksRef.current.get(formId);
@@ -700,19 +1451,44 @@ export const App = () => {
700
1451
  const appContext = useMemo(() => ({
701
1452
  addMessage,
702
1453
  clearMessages,
1454
+ getEvidenceSnapshot: () => agent?.getEvidenceSnapshot(),
1455
+ getDiagnostics: () => {
1456
+ const snapshot = captureSessionDiagnosticSnapshot({ messages: messagesRef.current, diagnostics: agent?.getDiagnosticSnapshot() });
1457
+ agent?.restoreDiagnosticSnapshot(snapshot);
1458
+ return snapshot;
1459
+ },
1460
+ clearTransientState: () => {
1461
+ agent?.clearPinnedTask();
1462
+ if (agent) {
1463
+ agent.toolResponseStore.evidence.clear();
1464
+ const snapshot = agent.getEvidenceSnapshot();
1465
+ setAgentState(previous => applyAgentEventToState(previous, { type: 'evidence', sessionId: previous.evidence?.sessionId ?? sessionId, snapshot }));
1466
+ }
1467
+ },
703
1468
  getMessages,
704
1469
  setMessages,
705
1470
  setAgentState,
706
1471
  reloadAgent,
1472
+ applyAccountOperation,
1473
+ getUsageSnapshot,
707
1474
  setDebug,
708
1475
  retry: retryLast,
709
1476
  exit,
1477
+ getSessionId: () => sessionId,
1478
+ getSessionName: () => sessionName,
1479
+ setSessionName,
1480
+ setPaneLabels: async (labels) => { setSessionName(labels.manual_title); },
710
1481
  config: configStore,
711
1482
  emulation: {
712
1483
  list: loadEmulationProfiles,
713
1484
  get: getEmulationProfile
714
1485
  },
715
1486
  shell: shellController,
1487
+ workspace: {
1488
+ getSnapshot: refreshWorkspace,
1489
+ continue: continueWorkspace,
1490
+ isolate: async () => { await isolateWorkspace(); },
1491
+ },
716
1492
  clipboard: {
717
1493
  writeText: writeClipboard
718
1494
  },
@@ -745,11 +1521,21 @@ export const App = () => {
745
1521
  cancelSelect: () => { },
746
1522
  },
747
1523
  questionnaire: {
748
- showQuestionnaire: () => { },
749
- cancelQuestionnaire: () => { },
1524
+ showQuestionnaire: (options, callback) => {
1525
+ retireRouting();
1526
+ // Ink subscribers share the current key dispatch; the opening Enter belongs to the composer.
1527
+ questionnaireInputReadyRef.current = false;
1528
+ questionnaireWidget.show({ ...options, callback });
1529
+ queueMicrotask(() => { questionnaireInputReadyRef.current = true; });
1530
+ },
1531
+ cancelQuestionnaire: () => {
1532
+ questionnaireInputReadyRef.current = false;
1533
+ questionnaireWidget.cancel();
1534
+ renderQuestionnaireWidget(version => version + 1);
1535
+ },
750
1536
  },
751
1537
  getInputMode: () => inputMode,
752
- setInputMode: (mode) => setInputMode(mode),
1538
+ setInputMode: (mode) => setInputMode(normalizeInputMode(mode)),
753
1539
  triggerAgentTurn: () => { },
754
1540
  // Authoritative context getters for /context + /compact: the real per-call
755
1541
  // provider count + window (read live via the ref so commands never see a
@@ -763,10 +1549,15 @@ export const App = () => {
763
1549
  cacheInputTokens: undefined, cachedInputTokens: undefined,
764
1550
  })),
765
1551
  model: configStore.get().model
766
- }), [addMessage, clearMessages, getMessages, setMessages, reloadAgent, exit, shellController, retryLast, inputMode, promptController, formController]);
1552
+ }), [addMessage, clearMessages, getMessages, setMessages, reloadAgent, exit, shellController, retryLast, inputMode, promptController, formController, questionnaireWidget, retireRouting, agent, sessionId, sessionName, setAgentState, refreshWorkspace, continueWorkspace, isolateWorkspace]);
767
1553
  // Handle commands
768
1554
  const handleCommand = useCallback((cmd, args) => {
769
1555
  const normalizedCommand = cmd.toLowerCase();
1556
+ if (normalizedCommand === 'model' && (runtimeMutationRef.current || abortControllerRef.current
1557
+ || (agentStateRef.current.status !== 'idle' && agentStateRef.current.status !== 'error'))) {
1558
+ addMessage({ role: 'system', content: 'Cannot switch model while an active agent turn or account change is running.' });
1559
+ return;
1560
+ }
770
1561
  let resolvedPlanMode = false;
771
1562
  if (normalizedCommand === 'mode' && args[0]) {
772
1563
  try {
@@ -804,13 +1595,93 @@ export const App = () => {
804
1595
  });
805
1596
  }
806
1597
  }, [appContext, addMessage]);
1598
+ const routeBusyInput = useCallback((text, draft) => {
1599
+ if (routingDraftsRef.current.size + queueRef.current.length >= MAX_QUEUE) {
1600
+ showToastRef.current?.(`Queue full (${MAX_QUEUE}). Cancel or wait.`);
1601
+ // InputArea clears after this callback, so restoration waits for that
1602
+ // submission's empty-composer revision.
1603
+ queueMicrotask(() => draft?.restore());
1604
+ return Promise.resolve();
1605
+ }
1606
+ const submitted = { id: generateId(), text, queuedAt: new Date(), delivery: 'subturn' };
1607
+ const epoch = routingEpochRef.current;
1608
+ const controller = new AbortController();
1609
+ const config = structuredClone(configStore.get());
1610
+ const backendOptions = { config, provider: configStore.getProvider(), model: configStore.getModel(),
1611
+ openaiCompatibleBaseUrl: configStore.getOpenAICompatibleBaseUrl(), emulationId: configStore.getEmulationId(),
1612
+ mode: agentMode, cwd: shellCwdRef.current, sessionId, allowSelfPlanTransition: false };
1613
+ const currentTask = lastRequestRef.current?.messages.filter(message => message.role === 'user'
1614
+ && !message.metadata?.toolResult).at(-1)?.content ?? text;
1615
+ getUsageSnapshot();
1616
+ const routingUsage = retainedUsageRef.current;
1617
+ routingControllersRef.current.set(submitted.id, controller);
1618
+ routingDraftsRef.current.set(submitted.id, draft);
1619
+ const owns = () => routingEpochRef.current === epoch && !controller.signal.aborted;
1620
+ // Serial decisions preserve submission FIFO even when a parent completes
1621
+ // or the first decision needs an explicit choice.
1622
+ const routed = routingTailRef.current.then(async () => {
1623
+ if (!owns())
1624
+ return;
1625
+ const decision = await classifyBusyInput({ backendOptions, currentTask, inputText: text,
1626
+ hosted: false, signal: controller.signal, onUsage: receipt => {
1627
+ routingUsage.snapshot = recordUsageReceipt(routingUsage.snapshot, receipt);
1628
+ if (sessionIdRef.current === receipt.sessionId)
1629
+ setRoutingUsageRevision(revision => revision + 1);
1630
+ } });
1631
+ if (!owns())
1632
+ return;
1633
+ if (pendingPromptRef.current || formCallbacksRef.current.size > 0 || questionnaireWidget.isActive()) {
1634
+ draft?.restore();
1635
+ return;
1636
+ }
1637
+ if (decision.route === 'divergent') {
1638
+ const choice = await new Promise(resolve => {
1639
+ routingChoiceCancelRef.current = () => resolve(null);
1640
+ scopeWidget.show({ title: 'Separate task', question: 'This input appears to start different work.', type: 'select',
1641
+ options: [{ value: 'pane', label: 'New pane' }, { value: 'session', label: 'New session' },
1642
+ { value: 'steer', label: 'Steer anyway' }, { value: 'cancel', label: 'Cancel' }],
1643
+ callback: value => { routingChoiceCancelRef.current = null; resolve(typeof value === 'string' ? value : null); } });
1644
+ });
1645
+ if (!owns())
1646
+ return;
1647
+ if (choice !== 'steer') {
1648
+ if (choice === 'pane' || choice === 'session')
1649
+ addMessage({ role: 'system', content: `The Ink renderer cannot open a ${choice === 'pane' ? 'new pane' : 'new session'}. Use the Ratatui renderer (run ./scripts/build-rust.sh), or start a separate ZTC terminal. Your unsent draft is retained.` });
1650
+ const restored = draft?.restore();
1651
+ if (draft && !restored)
1652
+ addMessage({ role: 'system', content: 'Your newer composer draft was kept. The earlier separate-task input was not sent.' });
1653
+ return;
1654
+ }
1655
+ }
1656
+ if (enqueueMessage(text, 'subturn', submitted) === null)
1657
+ draft?.restore();
1658
+ else
1659
+ showToastRef.current?.('Steering current task · Sending after the current tool finishes');
1660
+ }).catch(error => {
1661
+ if (!owns())
1662
+ return;
1663
+ draft?.restore();
1664
+ addMessage({ role: 'system', content: `Unable to route this input: ${error instanceof Error ? error.message : String(error)}` });
1665
+ }).finally(() => {
1666
+ routingControllersRef.current.delete(submitted.id);
1667
+ routingDraftsRef.current.delete(submitted.id);
1668
+ });
1669
+ routingTailRef.current = routed;
1670
+ return routed;
1671
+ }, [addMessage, agentMode, enqueueMessage, scopeWidget, questionnaireWidget, getUsageSnapshot, sessionId]);
807
1672
  // Handle message submission
808
- const handleSubmit = useCallback(async (text) => {
1673
+ const handleSubmit = useCallback(async (text, draft, queuedEntry, askUserAnswer = false) => {
809
1674
  // Reject empty / whitespace-only prompts before any downstream work.
810
1675
  // Interactive pending prompts (password, etc.) may legitimately want
811
1676
  // blank input, so bypass this check when one is pending.
812
1677
  if (!pendingPromptRef.current && !isMeaningfulUserInput(text))
813
1678
  return;
1679
+ if (runtimeMutationRef.current && !pendingPromptRef.current) {
1680
+ const position = enqueueMessage(text, 'end-of-turn');
1681
+ addMessage({ role: 'system', content: position === null ? 'Account change in progress; the queue is full. Retry when it finishes.'
1682
+ : 'Account change in progress; input retained in the queue.' });
1683
+ return;
1684
+ }
814
1685
  // Check for pending prompts first (interactive command flow)
815
1686
  const pending = pendingPromptRef.current;
816
1687
  if (pending) {
@@ -831,7 +1702,7 @@ export const App = () => {
831
1702
  }
832
1703
  return;
833
1704
  }
834
- if (text.startsWith('!')) {
1705
+ if (!askUserAnswer && text.startsWith('!')) {
835
1706
  const commandText = text.slice(1).trim();
836
1707
  if (commandText) {
837
1708
  // Intercept 'cd' commands to use the proper /cd handler that persists
@@ -855,22 +1726,48 @@ export const App = () => {
855
1726
  });
856
1727
  return;
857
1728
  }
858
- const busy = agentState.status !== 'idle' && agentState.status !== 'error';
1729
+ const admission = await admitWorkspace();
1730
+ if (!admission.allowed) {
1731
+ if (!workspaceClosedRef.current) {
1732
+ if (!pausedWorkspaceInputRef.current)
1733
+ pausedWorkspaceInputRef.current = {
1734
+ text, draft, queuedEntry, askUserAnswer, fingerprint: admission.snapshot.fingerprint,
1735
+ };
1736
+ if (admission.isolate && await isolateWorkspace())
1737
+ return;
1738
+ draft?.restore();
1739
+ addMessage({ role: 'system', content: 'Workspace session paused. Input and queued work retained; use /sessions continue.' });
1740
+ }
1741
+ return;
1742
+ }
1743
+ const busy = abortControllerRef.current !== null
1744
+ || (agentStateRef.current.status !== 'idle' && agentStateRef.current.status !== 'error');
1745
+ flushReadingDisplay();
1746
+ if ((busy || routingDraftsRef.current.size > 0) && inputMode === 'steer' && !queuedEntry) {
1747
+ await routeBusyInput(text, draft);
1748
+ return;
1749
+ }
859
1750
  if (busy) {
860
- if (inputMode === 'queue') {
861
- const nextSize = enqueueMessage(text);
1751
+ if (inputMode === 'steer') {
1752
+ const nextSize = enqueueMessage(text, 'subturn');
862
1753
  if (nextSize !== null) {
863
- // Durable audit alongside the live queue panel.
864
- addMessage({ role: 'system', content: `Queued (${nextSize})` });
1754
+ showToastRef.current?.('Steering current task · Sending after the current tool finishes');
865
1755
  }
866
1756
  return;
867
1757
  }
1758
+ // Retire the prior event owner before aborting it, so late output and
1759
+ // checkpoints cannot overwrite the replacement turn.
1760
+ cancelGeneration();
868
1761
  addMessage({
869
1762
  role: 'system',
870
1763
  content: 'Interrupting current response...'
871
1764
  });
872
1765
  }
873
- let currentAgent = agent;
1766
+ // Agent events do not carry a run owner. An aborted HTTP body can finish
1767
+ // after its run promise settles, so a replacement must not subscribe to
1768
+ // that retired Agent's emitter.
1769
+ const liveAgent = diagnosticAgentRef.current;
1770
+ let currentAgent = liveAgent && !retiredAgentsRef.current.has(liveAgent) ? liveAgent : null;
874
1771
  if (skills.length > 0) {
875
1772
  const activated = autoActivateSkills(text, skills);
876
1773
  const ids = activated.map(skill => skill.id).sort();
@@ -880,15 +1777,7 @@ export const App = () => {
880
1777
  activeSkillIds.current = ids;
881
1778
  const prompt = buildSkillPrompt(activated);
882
1779
  // Let the resolver apply config-key precedence (no env-first getApiKey()).
883
- const nextAgent = createAgentFromConfig({
884
- config: configStore.get(),
885
- provider: configStore.getProvider(),
886
- model: configStore.getModel(),
887
- openaiCompatibleBaseUrl: configStore.getOpenAICompatibleBaseUrl(),
888
- emulationId: configStore.getEmulationId(),
889
- skillPrompt: prompt,
890
- allowSelfPlanTransition: false,
891
- });
1780
+ const nextAgent = createBoundAgent(agentMode, prompt);
892
1781
  if (nextAgent) {
893
1782
  setAgent(nextAgent);
894
1783
  currentAgent = nextAgent;
@@ -902,7 +1791,7 @@ export const App = () => {
902
1791
  }
903
1792
  }
904
1793
  if (!currentAgent) {
905
- currentAgent = createAgent();
1794
+ currentAgent = createBoundAgent(agentMode, buildSkillPrompt(skills.filter(skill => activeSkillIds.current.includes(skill.id))));
906
1795
  setAgent(currentAgent);
907
1796
  }
908
1797
  if (!currentAgent) {
@@ -913,18 +1802,28 @@ export const App = () => {
913
1802
  return;
914
1803
  }
915
1804
  const userMsg = {
916
- id: generateId(),
1805
+ id: queuedEntry?.id ?? generateId(),
917
1806
  role: 'user',
918
1807
  content: text,
919
- timestamp: new Date()
1808
+ timestamp: queuedEntry?.queuedAt ?? new Date(),
1809
+ ...(askUserAnswer ? { metadata: { isAskUserAnswer: true } } : {}),
920
1810
  };
921
- setMessages(prev => [...prev, userMsg]);
922
1811
  const requestMessages = [...messagesRef.current, userMsg];
923
- lastRequestRef.current = { messages: requestMessages, agent: currentAgent };
1812
+ setMessages(prev => [...prev, userMsg]);
1813
+ const promptTurn = new PromptTurn(userMsg.id, () => {
1814
+ const snapshot = promptTurn.snapshot();
1815
+ setMessages(prev => prev.map(message => message.turn?.id === snapshot.id ? { ...message, turn: snapshot } : message));
1816
+ });
1817
+ promptTurnRef.current = promptTurn;
1818
+ lastRequestRef.current = { messages: requestMessages, agent: currentAgent, turn: promptTurn };
924
1819
  const runId = `${Date.now()}_${runCounterRef.current++}`;
925
1820
  activeRunIdRef.current = runId;
1821
+ resetAskUserTurnFlag();
926
1822
  const controller = new AbortController();
1823
+ resetEscapeStop();
1824
+ queuePausedRef.current = false;
927
1825
  abortControllerRef.current = controller;
1826
+ await workspaceCoordinator.setActivity('running');
928
1827
  const isActive = () => activeRunIdRef.current === runId;
929
1828
  const runStartedAt = Date.now();
930
1829
  streamingMessageId.current = null;
@@ -933,6 +1832,12 @@ export const App = () => {
933
1832
  if (!isActive())
934
1833
  return;
935
1834
  switch (event.type) {
1835
+ case 'evidence':
1836
+ setAgentState(previous => applyAgentEventToState(previous, event));
1837
+ break;
1838
+ case 'command_wait':
1839
+ case 'command_wait_end':
1840
+ case 'tool_progress':
936
1841
  case 'thinking_start':
937
1842
  setAgentState(s => applyAgentEventToState(s, event));
938
1843
  break;
@@ -954,13 +1859,17 @@ export const App = () => {
954
1859
  setAgentState(s => applyAgentEventToState(s, event));
955
1860
  if (!streamingMessageId.current) {
956
1861
  const id = generateId();
1862
+ // Register the owned stream before React can batch its completion
1863
+ // into the same paint. Historical settled messages are never typed.
1864
+ readingStatesRef.current.set(id, { ...initialStreamTypewriterState(), messageId: id });
957
1865
  streamingMessageId.current = id;
958
1866
  setMessages(prev => [...prev, {
959
1867
  id,
960
1868
  role: 'assistant',
961
1869
  content: '',
962
1870
  timestamp: new Date(),
963
- isStreaming: true
1871
+ isStreaming: true,
1872
+ turn: promptTurn.snapshot(),
964
1873
  }]);
965
1874
  }
966
1875
  break;
@@ -983,6 +1892,7 @@ export const App = () => {
983
1892
  });
984
1893
  break;
985
1894
  case 'stream_end':
1895
+ setAgentState(s => applyAgentEventToState(s, event));
986
1896
  if (streamingMessageId.current) {
987
1897
  // Get the streaming message to speak it
988
1898
  const streamingMsgId = streamingMessageId.current;
@@ -1007,6 +1917,7 @@ export const App = () => {
1007
1917
  streamingMessageId.current = null;
1008
1918
  break;
1009
1919
  case 'tool_end': {
1920
+ setAgentState(s => applyAgentEventToState(s, event));
1010
1921
  const key = event.tool;
1011
1922
  const bucket = toolStartTimes.current.get(key) || [];
1012
1923
  const started = bucket.shift();
@@ -1024,6 +1935,7 @@ export const App = () => {
1024
1935
  break;
1025
1936
  }
1026
1937
  case 'tool_error': {
1938
+ setAgentState(s => applyAgentEventToState(s, event));
1027
1939
  addMessage({
1028
1940
  role: 'tool',
1029
1941
  content: formatToolError(event.tool, event.error, configStore.getEmulationId())
@@ -1047,9 +1959,10 @@ export const App = () => {
1047
1959
  }));
1048
1960
  break;
1049
1961
  case 'api_retry':
1962
+ setAgentState(s => applyAgentEventToState(s, event));
1050
1963
  addMessage({
1051
1964
  role: 'system',
1052
- content: `⚠️ ${event.reason}. Retrying in ${event.delay / 1000}s... (attempt ${event.attempt}/${event.maxAttempts})`
1965
+ content: formatApiRetryNotice(event)
1053
1966
  });
1054
1967
  break;
1055
1968
  case 'error':
@@ -1058,7 +1971,17 @@ export const App = () => {
1058
1971
  }
1059
1972
  });
1060
1973
  try {
1061
- await runWithRetry(requestMessages, currentAgent, false, isActive, controller.signal);
1974
+ await runWithRetry(requestMessages, currentAgent, false, isActive, controller.signal, () => {
1975
+ if (!isActive())
1976
+ return [];
1977
+ const pending = queueRef.current.filter(entry => entry.delivery === 'subturn');
1978
+ queueRef.current = queueRef.current.filter(entry => entry.delivery !== 'subturn');
1979
+ setQueue(queueRef.current);
1980
+ // These messages become visible through the real history checkpoint
1981
+ // when the Agent admits them after the completed tool result.
1982
+ return pending.map(entry => ({ id: entry.id, role: 'user', content: entry.text,
1983
+ timestamp: entry.queuedAt, attachments: entry.attachments, metadata: { steering: true } }));
1984
+ }, promptTurn);
1062
1985
  }
1063
1986
  catch (err) {
1064
1987
  if ((err instanceof AgentError && err.code === 'CANCELLED') || controller.signal.aborted)
@@ -1098,6 +2021,7 @@ export const App = () => {
1098
2021
  activeRunIdRef.current = null;
1099
2022
  abortControllerRef.current = null;
1100
2023
  lastRunDurationRef.current = Date.now() - runStartedAt;
2024
+ resetEscapeStop();
1101
2025
  if (getTraceStyle(configStore.getEmulationId()) === 'codex') {
1102
2026
  const seconds = Math.max(0, Math.round((lastRunDurationRef.current || 0) / 1000));
1103
2027
  addMessage({
@@ -1106,6 +2030,9 @@ export const App = () => {
1106
2030
  });
1107
2031
  }
1108
2032
  cleanup();
2033
+ if (!workspaceClosedRef.current && !workspacePausedRef.current)
2034
+ await workspaceCoordinator.setActivity('idle');
2035
+ presentAgentQuestion(runId);
1109
2036
  // Queue flushing is handled by the flush-on-idle effect below; shifting
1110
2037
  // here would strand the queue on cancel/error paths.
1111
2038
  }
@@ -1113,7 +2040,8 @@ export const App = () => {
1113
2040
  cleanup();
1114
2041
  }
1115
2042
  }
1116
- }, [agent, agentState.status, addMessage, handleCommand, enqueueMessage, inputMode, isRetryableError, runWithRetry]);
2043
+ }, [agent, agentMode, skills, agentState.status, addMessage, handleCommand, enqueueMessage, inputMode, isRetryableError, runWithRetry, cancelGeneration, resetEscapeStop, routeBusyInput, admitWorkspace, workspaceFactoryOptions, createBoundAgent, workspaceCoordinator, isolateWorkspace, flushReadingDisplay, presentAgentQuestion]);
2044
+ workspaceSubmitRef.current = handleSubmit;
1117
2045
  // Flush-on-idle: one dequeue per idle-edge, uniform across cancel/error.
1118
2046
  // Latch is set BEFORE dequeue because setQueue's notify is synchronous —
1119
2047
  // otherwise this effect would re-enter itself and dequeue twice.
@@ -1125,35 +2053,41 @@ export const App = () => {
1125
2053
  // must still be sent when the run is configured to interrupt on Enter.
1126
2054
  if (agentState.status !== 'idle')
1127
2055
  return;
2056
+ if (abortControllerRef.current !== null)
2057
+ return;
2058
+ if (queuePausedRef.current || workspacePausedRef.current || agentQuestionRef.current || pendingPromptRef.current || formCallbacksRef.current.size > 0 || questionnaireWidget.isActive() || scopeWidget.isActive() || workspaceWidget.isActive())
2059
+ return;
1128
2060
  if (prevStatus === 'idle' && queue.length === 0)
1129
2061
  return;
1130
2062
  if (queue.length === 0)
1131
2063
  return;
1132
2064
  if (flushingRef.current)
1133
2065
  return;
1134
- // End-of-turn entries only; a `subturn` entry still waiting here is started
1135
- // by its own fallback path, so taking it here would send it twice.
1136
- const ready = queueRef.current.find(entry => entry.delivery !== 'subturn');
1137
- if (!ready)
1138
- return;
1139
2066
  flushingRef.current = true;
2067
+ setFlushing(true);
1140
2068
  const next = dequeueMessage();
1141
2069
  if (!next) {
1142
2070
  flushingRef.current = false;
2071
+ setFlushing(false);
1143
2072
  return;
1144
2073
  }
1145
- void handleSubmit(next.text).finally(() => {
2074
+ void handleSubmit(next.text, undefined, next).finally(() => {
1146
2075
  flushingRef.current = false;
2076
+ setFlushing(false);
1147
2077
  });
1148
- }, [agentState.status, queue.length, dequeueMessage, handleSubmit]);
1149
- const showToast = useCallback((message) => {
2078
+ }, [agentState.status, queue.length, flushing, dequeueMessage, handleSubmit, scopeWidget, workspaceWidget, questionnaireWidget, questionnaireVersion, workspaceResumeVersion]);
2079
+ const showToast = useCallback((message, durationMs = DEFAULT_TOAST_MS) => {
1150
2080
  setToast(message);
1151
2081
  if (toastTimerRef.current) {
1152
2082
  clearTimeout(toastTimerRef.current);
1153
2083
  }
1154
- toastTimerRef.current = setTimeout(() => {
2084
+ const timer = setTimeout(() => {
2085
+ if (toastTimerRef.current !== timer)
2086
+ return;
2087
+ toastTimerRef.current = null;
1155
2088
  setToast(null);
1156
- }, DEFAULT_TOAST_MS);
2089
+ }, durationMs);
2090
+ toastTimerRef.current = timer;
1157
2091
  }, []);
1158
2092
  // Wire showToast into the ref so enqueue (which is declared before showToast)
1159
2093
  // can surface "Queue full" notices without creating a dependency cycle.
@@ -1170,7 +2104,7 @@ export const App = () => {
1170
2104
  if (skippedPlan)
1171
2105
  next = getNextMode(next);
1172
2106
  setAgentMode(next);
1173
- setAgent(createAgent(next));
2107
+ setAgent(createBoundAgent(next));
1174
2108
  const label = getModeConfig(next).label;
1175
2109
  // The status line's mode field already reads out the active mode, so this
1176
2110
  // fallback only speaks up when it had to skip Plan.
@@ -1181,7 +2115,7 @@ export const App = () => {
1181
2115
  catch (error) {
1182
2116
  showToast(error instanceof Error ? error.message : String(error));
1183
2117
  }
1184
- }, [agentMode, showToast]);
2118
+ }, [agentMode, showToast, createBoundAgent]);
1185
2119
  const handleDictationStateChange = useCallback((state) => {
1186
2120
  setIsRecording(state === 'recording');
1187
2121
  setIsTranscribing(state === 'transcribing');
@@ -1225,29 +2159,92 @@ export const App = () => {
1225
2159
  });
1226
2160
  return cleanup;
1227
2161
  }, [useNativeDictation, showToast]);
2162
+ // Reveal is a display projection; canonical messages and provider/session
2163
+ // history always retain the full received body and its actual completion.
2164
+ const { displayMessages, readingPending } = useMemo(() => {
2165
+ if (readingSessionRef.current !== sessionId) {
2166
+ readingStatesRef.current.clear();
2167
+ bypassedReadingIdsRef.current.clear();
2168
+ readingSessionRef.current = sessionId;
2169
+ }
2170
+ const ids = new Set(messages.map(message => message.id));
2171
+ for (const id of readingStatesRef.current.keys())
2172
+ if (!ids.has(id))
2173
+ readingStatesRef.current.delete(id);
2174
+ for (const id of bypassedReadingIdsRef.current)
2175
+ if (!ids.has(id))
2176
+ bypassedReadingIdsRef.current.delete(id);
2177
+ let pending = false;
2178
+ const shown = messages.map(message => {
2179
+ const eligible = message.role === 'assistant' && message.metadata?.reasoning !== true
2180
+ && message.metadata?.toolResult !== true && !message.metadata?.toolOutput
2181
+ && !message.toolCalls?.length && !message.widget;
2182
+ const previous = readingStatesRef.current.get(message.id);
2183
+ if (!eligible || bypassedReadingIdsRef.current.has(message.id)
2184
+ || (!message.isStreaming && !previous))
2185
+ return message;
2186
+ const plan = planStreamTypewriterSample(previous ?? initialStreamTypewriterState(), message, {
2187
+ enabled: messageDisplay.smoothStreaming || messageDisplay.proseReadingRateWordsPerSecond !== null,
2188
+ now: Date.now(), proseReadingRateWordsPerSecond: messageDisplay.proseReadingRateWordsPerSecond,
2189
+ });
2190
+ readingStatesRef.current.set(message.id, plan.nextState);
2191
+ pending ||= plan.pending;
2192
+ return plan.content === message.content && !plan.pending ? message
2193
+ : { ...message, content: plan.content, isStreaming: message.isStreaming || plan.pending };
2194
+ });
2195
+ return { displayMessages: shown, readingPending: pending };
2196
+ }, [messages, sessionId, messageDisplay.smoothStreaming, messageDisplay.proseReadingRateWordsPerSecond, readingTick]);
2197
+ React.useEffect(() => {
2198
+ if (!readingPending)
2199
+ return;
2200
+ const timer = setTimeout(() => refreshReadingDisplay(tick => tick + 1), STREAM_TYPEWRITER_DEFAULTS.tickMs);
2201
+ return () => clearTimeout(timer);
2202
+ }, [readingPending, readingTick, messages, messageDisplay.smoothStreaming, messageDisplay.proseReadingRateWordsPerSecond]);
1228
2203
  // Compute messages for Static (completed) vs live (streaming/interactive)
1229
- // Static component handles deduplication by key - it only renders new items
1230
- const { staticMessages, streamingMessage, formMessages } = useMemo(() => {
2204
+ // Static renders only items past its retained index. Explicit clear starts a
2205
+ // new component generation so the shorter transcript is rendered from zero.
2206
+ const { staticMessages, liveTurnMessages, formMessages } = useMemo(() => {
1231
2207
  // Form widget messages are always computed (need interactivity)
1232
2208
  const forms = messages.filter(m => m.widget?.widgetType === 'form');
1233
2209
  if (!scrollback) {
1234
- return { staticMessages: [], streamingMessage: null, formMessages: forms };
2210
+ return { staticMessages: [], liveTurnMessages: [], formMessages: forms };
1235
2211
  }
1236
2212
  // Completed messages without widgets go to Static (rendered once, kept in scrollback)
1237
- const completed = messages.filter(m => !m.isStreaming
2213
+ const completed = displayMessages.filter(m => !m.isStreaming
2214
+ && !(messageDisplay.showTurnMetadata && m.turn && m.turn.finishedAt === undefined)
1238
2215
  && !m.widget
1239
2216
  && (messageDisplay.showThinking || m.metadata?.reasoning !== true));
1240
2217
  // Streaming message stays in live section (can update)
1241
- const streaming = messages.find(m => m.isStreaming
1242
- && (messageDisplay.showThinking || m.metadata?.reasoning !== true)) || null;
2218
+ const streaming = displayMessages.filter(m => (m.isStreaming || (messageDisplay.showTurnMetadata && m.turn && m.turn.finishedAt === undefined))
2219
+ && (messageDisplay.showThinking || m.metadata?.reasoning !== true));
1243
2220
  return {
1244
2221
  staticMessages: completed,
1245
- streamingMessage: streaming,
2222
+ liveTurnMessages: streaming,
1246
2223
  formMessages: forms
1247
2224
  };
1248
- }, [messages, scrollback, messageDisplay.showThinking]);
1249
- return (_jsxs(FullScreen, { debug: debug, scrollback: scrollback, children: [!scrollback && _jsx(Header, { version: getVersion(), debug: debug, renderer: "ink" }), scrollback ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticMessages, children: (msg) => (_jsx(Box, { flexDirection: "column", children: _jsx(SingleMessage, { message: msg, expandToolOutputs: expandToolOutputs }) }, msg.id)) }), streamingMessage && (_jsx(SingleMessage, { message: streamingMessage, expandToolOutputs: expandToolOutputs }))] })) : (_jsx(MessageList, { messages: messages, height: contentHeight, debug: debug, expandToolOutputs: expandToolOutputs, scrollback: scrollback, showThinking: messageDisplay.showThinking })), _jsx(ActivityLine, { state: agentState, spinnerLabel: spinnerLabel, spinnerFrame: spinnerFrame, inputMode: inputMode, scrollback: scrollback }), _jsx(QueuePanel, { entries: queuePreview, total: queue.length, cols: columns }), isRecording && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "red", children: "\u25CF Recording" }), recordingDevice && (_jsxs(Text, { color: "gray", children: [" [", recordingDevice, "]"] })), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: renderVUMeter(audioLevel) }), _jsx(Text, { color: "gray", children: " \u2014 Ctrl+R to stop" })] }), partialTranscript && (_jsx(Box, { children: _jsxs(Text, { color: "gray", italic: true, children: ["\"", partialTranscript, "\""] }) }))] })), isTranscribing && !useNativeDictation && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "yellow", children: "\u280B Transcribing..." }) })), formMessages.map((msg) => (msg.widget && (_jsx(FormWidget, { title: msg.widget.title, fields: msg.widget.fields, formId: msg.widget.formId, submitLabel: msg.widget.submitLabel, onSubmit: handleFormSubmit, onCancel: handleFormCancel, isActive: true }, msg.id)))), _jsx(InputArea, { onSubmit: handleSubmit, onCommand: handleCommand, commands: commands, onStateChange: setInputSnapshot, onToast: showToast, onDictationStateChange: handleDictationStateChange, cols: columns, inputBus: inputBus, disabled: false, debug: debug, cwd: shellCwdRef.current, placeholder: (!configStore.hasApiKey() && !configStore.isAuthenticated())
2225
+ }, [messages, displayMessages, scrollback, messageDisplay.showThinking, messageDisplay.showTurnMetadata]);
2226
+ React.useEffect(() => {
2227
+ if (!messageDisplay.showTurnMetadata || !messages.some(message => message.turn && message.turn.finishedAt === undefined))
2228
+ return;
2229
+ const timer = setInterval(() => setPromptClock(clock => clock + 1), 1000);
2230
+ return () => clearInterval(timer);
2231
+ }, [messages, messageDisplay.showTurnMetadata]);
2232
+ return (_jsxs(FullScreen, { debug: debug, scrollback: scrollback, children: [!scrollback && _jsx(Header, { version: getVersion(), debug: debug, renderer: "ink" }), scrollback ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticMessages, children: (msg) => (_jsx(Box, { flexDirection: "column", children: _jsx(SingleMessage, { message: msg, expandToolOutputs: expandToolOutputs, cols: columns, showTurnMetadata: messageDisplay.showTurnMetadata }) }, msg.id)) }, transcriptGeneration), liveTurnMessages.map(message => _jsx(SingleMessage, { message: message, expandToolOutputs: expandToolOutputs, cols: columns, showTurnMetadata: messageDisplay.showTurnMetadata }, message.id))] })) : (_jsx(MessageList, { messages: displayMessages, height: contentHeight, debug: debug, expandToolOutputs: expandToolOutputs, scrollback: scrollback, showThinking: messageDisplay.showThinking, showTurnMetadata: messageDisplay.showTurnMetadata, cols: columns })), _jsx(ActivityLine, { state: agentState, spinnerLabel: spinnerLabel, spinnerFrame: spinnerFrame, inputMode: inputMode, scrollback: scrollback, cols: columns }), _jsx(QueuePanel, { entries: queuePreview, total: queue.length, cols: columns }), isRecording && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { children: [_jsx(Text, { color: "red", children: "\u25CF Recording" }), recordingDevice && (_jsxs(Text, { color: "gray", children: [" [", recordingDevice, "]"] })), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: renderVUMeter(audioLevel) }), _jsx(Text, { color: "gray", children: " \u2014 Ctrl+R to stop" })] }), partialTranscript && (_jsx(Box, { children: _jsxs(Text, { color: "gray", italic: true, children: ["\"", partialTranscript, "\""] }) }))] })), isTranscribing && !useNativeDictation && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: "yellow", children: "\u280B Transcribing..." }) })), scopeWidget.isActive() && _jsx(Box, { paddingX: 1, children: _jsx(Text, { children: scopeWidget.buildContent(columns) }) }), workspaceWidget.isActive() && _jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { children: workspaceWidget.buildContent(columns) }) }), questionnaireWidget.isActive() && _jsx(InkNode, { node: questionnaireWidget.buildSurface(columns, rows).view }), formMessages.map((msg) => (msg.widget && (_jsx(FormWidget, { title: msg.widget.title, fields: msg.widget.fields, formId: msg.widget.formId, submitLabel: msg.widget.submitLabel, onSubmit: handleFormSubmit, onCancel: handleFormCancel, isActive: formCallbacksRef.current.has(msg.widget.formId) }, msg.id)))), _jsx(InputArea, { onCancelRun: () => {
2233
+ const routing = routingControllersRef.current.size > 0;
2234
+ retireRouting();
2235
+ return cancelGeneration() || routing;
2236
+ }, isRunning: agentState.status !== 'idle' && agentState.status !== 'error', mode: agentMode, onSubmit: handleSubmit, onQueue: (text) => {
2237
+ const size = enqueueMessage(text, 'end-of-turn');
2238
+ if (size === null)
2239
+ return false;
2240
+ showToast(`Queued (${size})`);
2241
+ return true;
2242
+ }, onCommand: handleCommand, commands: commands, onStateChange: setInputSnapshot, onToast: showToast, onDictationStateChange: handleDictationStateChange, cols: columns, inputBus: inputBus, inputBlocked: () => questionnaireWidget.isActive() || questionnaireEventOwnedRef.current, clearDraftVersion: clearDraftVersion, disabled: scopeWidget.isActive() || workspaceWidget.isActive() || questionnaireWidget.isActive() || formCallbacksRef.current.size > 0, debug: debug, cwd: shellCwdRef.current, placeholder: (!configStore.hasApiKey() && !configStore.isAuthenticated())
1250
2243
  ? 'Use /config key or /auth login to get started'
1251
- : getAgentActivityPlaceholder(agentState) ?? 'Type a message or /help for commands...', spinnerLabel: scrollback ? spinnerLabel : null, spinnerFrame: scrollback ? spinnerFrame : null, passwordMode: pendingPromptRef.current?.isPassword ?? false, onCycleMode: onCycleMode }), _jsx(StatusBar, { state: agentState, sessionId: sessionId, version: getVersion(), connectionStatus: (configStore.hasApiKey() || configStore.isAuthenticated()) ? 'connected' : 'disconnected', contextLength: contextLength, contextEstimated: contextEstimated, contextStatus: contextStatus, provider: provider, model: model, emulationId: emulationId, effort: configStore.getCodexEffort(), inputMode: inputMode, toast: toast, debug: debug, mode: agentMode, cols: columns, statusLine: configStore.get().statusLine })] }));
2244
+ : getAgentActivityPlaceholder(agentState) ?? 'Type a message or /help for commands...', spinnerLabel: scrollback ? spinnerLabel : null, spinnerFrame: scrollback ? spinnerFrame : null, passwordMode: pendingPromptRef.current?.isPassword ?? false, promptOwner: pendingPromptRef.current || undefined, allowEmptySubmit: pendingPromptRef.current?.allowEmptySubmit ?? false, onCycleMode: onCycleMode }), _jsx(StatusBar, { state: agentState, sessionId: sessionId, sessionName: sessionName, version: getVersion(), connectionStatus: (configStore.hasApiKey() || configStore.isAuthenticated()) ? 'connected' : 'disconnected', contextLength: contextLength, contextEstimated: contextEstimated, contextStatus: contextStatus, provider: provider, model: model, emulationId: emulationId, effort: configStore.getCodexEffort(), inputMode: inputMode, toast: toast, escapeHint: scopeWidget.isActive() || workspaceWidget.isActive() || questionnaireWidget.isActive() || formCallbacksRef.current.size > 0 || pendingPromptRef.current
2245
+ ? 'Esc close'
2246
+ : abortControllerRef.current
2247
+ ? isEscapeStopArmed(escapeStopArm, abortControllerRef.current, Date.now()) ? ESCAPE_CONFIRMATION_HINT : 'Esc twice to stop'
2248
+ : queue.length > 0 ? 'Esc keeps queue · /rewind' : 'Esc keeps draft · /rewind', debug: debug, mode: agentMode, cols: columns, statusLine: configStore.get().statusLine })] }));
1252
2249
  };
1253
2250
  export default App;