@gajae-code/coding-agent 0.14.2 → 0.15.0

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 (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -0,0 +1,531 @@
1
+ import { Container, getKeybindings, matchesKey, replaceTabs, Spacer, Text, truncateToWidth } from "@gajae-code/tui";
2
+ import { sanitizeDisplayLine } from "@gajae-code/utils";
3
+ import type {
4
+ AutoroutingProvenance,
5
+ AutoroutingProviderOrderHint,
6
+ AutoroutingSetup,
7
+ AutoroutingTier,
8
+ TierMap,
9
+ } from "../../config/autorouting-contract";
10
+ import type { AutoroutingSourceIdentity } from "../../config/autorouting-generator";
11
+ import { theme } from "../theme/theme";
12
+
13
+ /** Longest rendered panel line before truncation. */
14
+ export const MAX_PANEL_LINE_WIDTH = 200;
15
+
16
+ export type SmartRoutingPreview = {
17
+ readonly setup: AutoroutingSetup;
18
+ readonly tiers: TierMap;
19
+ readonly provenance: AutoroutingProvenance;
20
+ readonly sourceIdentity: AutoroutingSourceIdentity;
21
+ };
22
+
23
+ export type SmartRoutingIntent =
24
+ | {
25
+ kind: "apply";
26
+ draft: AutoroutingSetup;
27
+ preview: SmartRoutingPreview;
28
+ confirmHandEdit?: boolean;
29
+ }
30
+ | {
31
+ kind: "refresh";
32
+ confirmHandEdit?: boolean;
33
+ }
34
+ | {
35
+ kind: "clear";
36
+ }
37
+ | {
38
+ kind: "toggle";
39
+ enabled: boolean;
40
+ };
41
+
42
+ export type SmartRoutingPanelMode = "editing" | "confirming" | "committing" | "done" | "error";
43
+ export type SmartRoutingConfirmation = "clear" | "hand-edit" | undefined;
44
+
45
+ export interface SmartRoutingPanelOptions {
46
+ setup: AutoroutingSetup;
47
+ tiers?: TierMap;
48
+ provenance?: AutoroutingProvenance;
49
+ /** Advisory drift between the recorded declaration and current provider priority. */
50
+ providerOrderHint?: AutoroutingProviderOrderHint;
51
+ enabled: boolean;
52
+ readOnly: boolean;
53
+ stale: boolean;
54
+ preview: SmartRoutingPreview;
55
+ generatePreview: (draft: AutoroutingSetup) => SmartRoutingPreview;
56
+ onSelect: (intent: SmartRoutingIntent) => undefined | Promise<SmartRoutingPreview | undefined>;
57
+ onCancel: () => void;
58
+ }
59
+
60
+ function cloneSetup(setup: AutoroutingSetup): AutoroutingSetup {
61
+ return structuredClone(setup);
62
+ }
63
+
64
+ function clonePreview(preview: SmartRoutingPreview): SmartRoutingPreview {
65
+ return structuredClone(preview);
66
+ }
67
+
68
+ function isPrintable(data: string): boolean {
69
+ return data.length === 1 && data >= " " && data !== "\x7f";
70
+ }
71
+
72
+ function isBackspace(data: string): boolean {
73
+ return data === "\x7f" || data === "\b";
74
+ }
75
+
76
+ /**
77
+ * Provider names, allowlist entries, generated selectors, and error text all
78
+ * originate in hand-editable config or catalog data, so any of them can carry
79
+ * tabs, control bytes, line breaks, or terminal escape sequences. Any of those
80
+ * can inject extra rows or evade the width cap, so flatten and bound the value
81
+ * before it reaches a renderer.
82
+ */
83
+ function displaySafe(text: string): string {
84
+ return truncateToWidth(replaceTabs(sanitizeDisplayLine(text)), MAX_PANEL_LINE_WIDTH);
85
+ }
86
+
87
+ function formatTier(tier: AutoroutingTier, tiers: TierMap | undefined): string {
88
+ const selectors = tiers?.[tier];
89
+ return displaySafe(
90
+ `${tier}: ${selectors && selectors.length > 0 ? selectors.join(", ") : "(empty; manual fallback)"}`,
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Presentational smart-routing setup editor. It owns only ephemeral draft and
96
+ * preview state; all durable changes are emitted as typed intents.
97
+ */
98
+ export class SmartRoutingPanelComponent extends Container {
99
+ readonly #onSelect: SmartRoutingPanelOptions["onSelect"];
100
+ readonly #onCancel: () => void;
101
+ readonly #generatePreview: (draft: AutoroutingSetup) => SmartRoutingPreview;
102
+ readonly #readOnly: boolean;
103
+ #draft: AutoroutingSetup;
104
+
105
+ #tiers?: TierMap;
106
+ #provenance?: AutoroutingProvenance;
107
+ #enabled: boolean;
108
+ #stale: boolean;
109
+ #preview: SmartRoutingPreview;
110
+ #mode: SmartRoutingPanelMode = "editing";
111
+ #confirmation: SmartRoutingConfirmation;
112
+ /** Intent that triggered the current hand-edit confirmation, replayed verbatim on confirm. */
113
+ #pendingIntent: SmartRoutingIntent | undefined;
114
+ #providerCursor = 0;
115
+ #allowlistEditing = false;
116
+ #allowlistBuffer = "";
117
+ #providerOrderHint: AutoroutingProviderOrderHint | undefined;
118
+ #status = "";
119
+ #error = "";
120
+
121
+ constructor(options: SmartRoutingPanelOptions) {
122
+ super();
123
+ this.#onSelect = options.onSelect;
124
+ this.#onCancel = options.onCancel;
125
+ this.#generatePreview = options.generatePreview;
126
+ this.#readOnly = options.readOnly;
127
+ this.#draft = cloneSetup(options.setup);
128
+
129
+ this.#tiers = options.tiers ? structuredClone(options.tiers) : undefined;
130
+ this.#provenance = options.provenance ? structuredClone(options.provenance) : undefined;
131
+ this.#enabled = options.enabled;
132
+ this.#providerOrderHint = options.providerOrderHint ? structuredClone(options.providerOrderHint) : undefined;
133
+ this.#stale = options.stale;
134
+ this.#preview = clonePreview(options.preview);
135
+ this.#render();
136
+ }
137
+
138
+ get mode(): SmartRoutingPanelMode {
139
+ return this.#mode;
140
+ }
141
+
142
+ get confirmation(): SmartRoutingConfirmation {
143
+ return this.#confirmation;
144
+ }
145
+
146
+ getDraft(): AutoroutingSetup {
147
+ return cloneSetup(this.#draft);
148
+ }
149
+
150
+ getPreviewPayload(): SmartRoutingPreview {
151
+ return clonePreview(this.#preview);
152
+ }
153
+
154
+ getProviderOrder(): readonly string[] {
155
+ return [...this.#draft.providers];
156
+ }
157
+
158
+ /**
159
+ * Advisory-only, non-destructive hint update.
160
+ *
161
+ * `refreshState` deliberately replaces the draft and clears editor state, so it
162
+ * must never be used for a background settings change: an external
163
+ * `modelProviderOrder` edit would then discard the user's unsaved reordering,
164
+ * allowlist buffer, cursor, and confirmation. This replaces the hint and nothing
165
+ * else.
166
+ */
167
+ updateProviderOrderHint(hint: AutoroutingProviderOrderHint | undefined): void {
168
+ this.#providerOrderHint = hint ? structuredClone(hint) : undefined;
169
+ this.#render();
170
+ }
171
+
172
+ /** Replace the panel's persisted snapshot after a controller mutation. */
173
+ refreshState(options: {
174
+ setup: AutoroutingSetup;
175
+ tiers?: TierMap;
176
+ provenance?: AutoroutingProvenance;
177
+ enabled: boolean;
178
+ providerOrderHint?: AutoroutingProviderOrderHint;
179
+ stale: boolean;
180
+ preview: SmartRoutingPreview;
181
+ }): void {
182
+ this.#draft = cloneSetup(options.setup);
183
+ this.#tiers = options.tiers ? structuredClone(options.tiers) : undefined;
184
+ this.#provenance = options.provenance ? structuredClone(options.provenance) : undefined;
185
+ this.#enabled = options.enabled;
186
+ this.#stale = options.stale;
187
+ this.#providerOrderHint = options.providerOrderHint ? structuredClone(options.providerOrderHint) : undefined;
188
+ this.#preview = clonePreview(options.preview);
189
+ this.#providerCursor = Math.min(this.#providerCursor, Math.max(0, this.#draft.providers.length - 1));
190
+ this.#mode = "editing";
191
+ this.#confirmation = undefined;
192
+ this.#status = "";
193
+ this.#error = "";
194
+ this.#render();
195
+ }
196
+
197
+ #render(): void {
198
+ this.detachAll();
199
+ this.addChild(new Text(theme.fg("accent", "Smart routing setup"), 0, 0));
200
+ this.addChild(
201
+ new Text(theme.fg("muted", "Declare providers in priority order; generated chains are deterministic."), 0, 0),
202
+ );
203
+ if (this.#readOnly) {
204
+ this.addChild(
205
+ new Text(
206
+ theme.fg("warning", "Read-only: temporary or --models-scoped sessions cannot write autorouting."),
207
+ 0,
208
+ 0,
209
+ ),
210
+ );
211
+ }
212
+ if (this.#stale) {
213
+ this.addChild(
214
+ new Text(
215
+ theme.fg("warning", "Stale generated setup: catalog/map or hand-edited tiers differ from provenance."),
216
+ 0,
217
+ 0,
218
+ ),
219
+ );
220
+ }
221
+ // Advisory only: a changed provider priority is a new suggestion, never proof
222
+ // that the persisted tiers went stale.
223
+ if (this.#providerOrderHint?.reordered) {
224
+ this.addChild(
225
+ new Text(
226
+ theme.fg("muted", "Provider priority changed since this setup was generated. Press r to refresh."),
227
+ 0,
228
+ 0,
229
+ ),
230
+ );
231
+ }
232
+ if (this.#providerOrderHint && this.#providerOrderHint.missing.length > 0) {
233
+ this.addChild(
234
+ new Text(
235
+ theme.fg(
236
+ "muted",
237
+ displaySafe(
238
+ `Declared providers missing from the catalog: ${this.#providerOrderHint.missing.join(", ")}`,
239
+ ),
240
+ ),
241
+ 0,
242
+ 0,
243
+ ),
244
+ );
245
+ }
246
+ if (this.#provenance)
247
+ this.addChild(new Text(theme.fg("dim", "Provenance recorded for this generated setup."), 0, 0));
248
+ this.addChild(new Spacer(1));
249
+ this.addChild(new Text(theme.fg("muted", `Enabled: ${this.#enabled ? "yes" : "no"}`), 0, 0));
250
+ this.addChild(new Text(theme.fg("muted", "Providers (↑/↓ reorder; x removes; m edits allowlist):"), 0, 0));
251
+ for (let index = 0; index < this.#draft.providers.length; index++) {
252
+ const provider = this.#draft.providers[index] ?? "";
253
+ const prefix = index === this.#providerCursor ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
254
+ this.addChild(new Text(`${prefix}${displaySafe(provider)}`, 0, 0));
255
+ }
256
+ if (this.#draft.providers.length === 0)
257
+ this.addChild(new Text(theme.fg("error", " No providers declared."), 0, 0));
258
+ if (this.#allowlistEditing) {
259
+ this.addChild(
260
+ new Text(theme.fg("accent", displaySafe(`Allowlist: ${this.#allowlistBuffer || "(all models)"}`)), 0, 0),
261
+ );
262
+ this.addChild(
263
+ new Text(theme.fg("muted", " Type provider/model values separated by commas, then Enter."), 0, 0),
264
+ );
265
+ } else {
266
+ this.addChild(
267
+ new Text(
268
+ theme.fg(
269
+ "dim",
270
+ displaySafe(
271
+ `Allowlist: ${this.#draft.models && this.#draft.models.length > 0 ? this.#draft.models.join(", ") : "(all labeled models)"}`,
272
+ ),
273
+ ),
274
+ 0,
275
+ 0,
276
+ ),
277
+ );
278
+ }
279
+ this.addChild(new Spacer(1));
280
+ this.addChild(new Text(theme.fg("muted", "Preview:"), 0, 0));
281
+ for (const tier of ["fast", "balanced", "strong"] as const) {
282
+ this.addChild(new Text(` ${formatTier(tier, this.#preview.tiers)}`, 0, 0));
283
+ }
284
+ if (this.#status) this.addChild(new Text(theme.fg("success", displaySafe(this.#status)), 0, 0));
285
+ if (this.#error) this.addChild(new Text(theme.fg("error", displaySafe(this.#error)), 0, 0));
286
+ if (this.#confirmation === "clear") {
287
+ this.addChild(
288
+ new Text(
289
+ theme.fg("warning", "Clear generated tiers, setup, and provenance? Enter confirms; Esc cancels."),
290
+ 0,
291
+ 0,
292
+ ),
293
+ );
294
+ } else if (this.#confirmation === "hand-edit") {
295
+ this.addChild(
296
+ new Text(
297
+ theme.fg(
298
+ "warning",
299
+ "Stored tiers were hand-edited. Regenerate and replace them? Enter confirms; Esc cancels.",
300
+ ),
301
+ 0,
302
+ 0,
303
+ ),
304
+ );
305
+ }
306
+ this.addChild(new Spacer(1));
307
+ const actions = this.#readOnly ? "Esc: cancel" : "a: apply · r: refresh · t: toggle · c: clear · Esc: cancel";
308
+ this.addChild(new Text(theme.fg("muted", actions), 0, 0));
309
+ }
310
+
311
+ #recomputePreview(): void {
312
+ try {
313
+ this.#preview = clonePreview(this.#generatePreview(this.#draft));
314
+ this.#error = "";
315
+ } catch (error) {
316
+ this.#error = error instanceof Error ? error.message : String(error);
317
+ }
318
+ }
319
+
320
+ async #emit(intent: SmartRoutingIntent): Promise<void> {
321
+ if (this.#readOnly || this.#mode === "committing") return;
322
+ this.#mode = "committing";
323
+ this.#status = "Committing smart-routing settings…";
324
+ this.#error = "";
325
+ this.#confirmation = undefined;
326
+ this.#render();
327
+ try {
328
+ const result = await this.#onSelect(intent);
329
+ if (intent.kind === "apply") {
330
+ const appliedPreview = result ?? this.#generatePreview(intent.draft);
331
+ this.#draft = cloneSetup(appliedPreview.setup);
332
+ this.#preview = clonePreview(appliedPreview);
333
+ } else if (intent.kind === "toggle") {
334
+ this.#enabled = intent.enabled;
335
+ }
336
+ this.#mode = "done";
337
+ this.#status = intent.kind === "clear" ? "Generated setup cleared." : "Smart-routing settings saved.";
338
+ } catch (error) {
339
+ const typed = error as { code?: unknown } | undefined;
340
+ if (typed?.code === "autorouting-hand-edited") {
341
+ this.#mode = "confirming";
342
+ this.#confirmation = "hand-edit";
343
+ // Retain the intent that triggered the guard so confirming replays THAT intent. An Apply
344
+ // carrying an edited draft must not be silently downgraded to a Refresh, which would
345
+ // discard the frozen preview and commit the previously recorded setup instead (AC6).
346
+ this.#pendingIntent = intent;
347
+ this.#status = "";
348
+ this.#error = "";
349
+ } else {
350
+ this.#mode = "error";
351
+ this.#error = error instanceof Error ? error.message : String(error);
352
+ this.#status = "";
353
+ }
354
+ }
355
+ this.#render();
356
+ }
357
+
358
+ #beginClearConfirmation(): void {
359
+ if (this.#readOnly || this.#mode !== "editing") return;
360
+ this.#mode = "confirming";
361
+ this.#confirmation = "clear";
362
+ this.#render();
363
+ }
364
+
365
+ #commitAllowlistBuffer(): void {
366
+ const values = this.#allowlistBuffer
367
+ .split(",")
368
+ .map(value => value.trim())
369
+ .filter(value => value.length > 0);
370
+ this.#draft =
371
+ values.length > 0
372
+ ? { ...this.#draft, models: [...new Set(values)] }
373
+ : { schema: 1, providers: [...this.#draft.providers] };
374
+ this.#allowlistEditing = false;
375
+ this.#allowlistBuffer = "";
376
+ this.#recomputePreview();
377
+ this.#render();
378
+ }
379
+
380
+ handleInput(data: string): void {
381
+ if (this.#mode === "committing") return;
382
+ if (this.#mode === "done" || this.#mode === "error") {
383
+ if (getKeybindings().matches(data, "tui.select.cancel")) this.#onCancel();
384
+ return;
385
+ }
386
+ if (this.#confirmation) {
387
+ if (getKeybindings().matches(data, "tui.select.cancel")) {
388
+ this.#confirmation = undefined;
389
+ this.#pendingIntent = undefined;
390
+ this.#mode = "editing";
391
+ this.#render();
392
+ return;
393
+ }
394
+ if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
395
+ void this.#emitConfirmed();
396
+ }
397
+ return;
398
+ }
399
+ if (this.#allowlistEditing) {
400
+ if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
401
+ this.#commitAllowlistBuffer();
402
+ return;
403
+ }
404
+ if (getKeybindings().matches(data, "tui.select.cancel")) {
405
+ this.#allowlistEditing = false;
406
+ this.#allowlistBuffer = "";
407
+ this.#render();
408
+ return;
409
+ }
410
+ if (isBackspace(data)) {
411
+ this.#allowlistBuffer = this.#allowlistBuffer.slice(0, -1);
412
+ this.#render();
413
+ return;
414
+ }
415
+ if (isPrintable(data)) {
416
+ this.#allowlistBuffer += data;
417
+ this.#render();
418
+ }
419
+ return;
420
+ }
421
+ if (getKeybindings().matches(data, "tui.select.cancel")) {
422
+ this.#onCancel();
423
+ return;
424
+ }
425
+ if (this.#readOnly) return;
426
+ if (data === "m" || data === "M") {
427
+ this.#allowlistEditing = true;
428
+ this.#allowlistBuffer = this.#draft.models?.join(", ") ?? "";
429
+ this.#render();
430
+ return;
431
+ }
432
+ if (data === "x" || data === "X") {
433
+ if (this.#draft.providers.length <= 1) {
434
+ this.#error = "At least one provider must remain declared.";
435
+ this.#render();
436
+ return;
437
+ }
438
+ const providers = this.#draft.providers.filter((_, index) => index !== this.#providerCursor);
439
+ this.#draft = { ...this.#draft, providers };
440
+ this.#providerCursor = Math.min(this.#providerCursor, providers.length - 1);
441
+ this.#recomputePreview();
442
+ this.#render();
443
+ return;
444
+ }
445
+ if (matchesKey(data, "up")) {
446
+ if (this.#providerCursor > 0) {
447
+ const providers = [...this.#draft.providers];
448
+ [providers[this.#providerCursor - 1], providers[this.#providerCursor]] = [
449
+ providers[this.#providerCursor]!,
450
+ providers[this.#providerCursor - 1]!,
451
+ ];
452
+ this.#draft = { ...this.#draft, providers };
453
+ this.#providerCursor--;
454
+ this.#recomputePreview();
455
+ this.#render();
456
+ }
457
+ return;
458
+ }
459
+ if (matchesKey(data, "down")) {
460
+ if (this.#providerCursor < this.#draft.providers.length - 1) {
461
+ const providers = [...this.#draft.providers];
462
+ [providers[this.#providerCursor], providers[this.#providerCursor + 1]] = [
463
+ providers[this.#providerCursor + 1]!,
464
+ providers[this.#providerCursor]!,
465
+ ];
466
+ this.#draft = { ...this.#draft, providers };
467
+ this.#providerCursor++;
468
+ this.#recomputePreview();
469
+ this.#render();
470
+ }
471
+ return;
472
+ }
473
+ if (data === "a" || data === "A") {
474
+ void this.#emit({ kind: "apply", draft: cloneSetup(this.#draft), preview: clonePreview(this.#preview) });
475
+ return;
476
+ }
477
+ if (data === "r" || data === "R") {
478
+ void this.#emit({ kind: "refresh" });
479
+ return;
480
+ }
481
+ if (data === "t" || data === "T") {
482
+ void this.#emit({ kind: "toggle", enabled: !this.#enabled });
483
+ return;
484
+ }
485
+ if (data === "c" || data === "C") {
486
+ this.#beginClearConfirmation();
487
+ }
488
+ }
489
+
490
+ __testApply(): Promise<void> {
491
+ return this.#emit({ kind: "apply", draft: cloneSetup(this.#draft), preview: clonePreview(this.#preview) });
492
+ }
493
+
494
+ /** Test hook mirroring an in-panel provider reorder/edit: mutates the draft and re-previews. */
495
+ __testSetProviders(providers: string[]): void {
496
+ this.#draft = { ...this.#draft, providers: [...providers] };
497
+ this.#preview = this.#generatePreview(this.#draft);
498
+ this.#providerCursor = 0;
499
+ }
500
+
501
+ __testRefresh(confirmHandEdit = false): Promise<void> {
502
+ return this.#emit({ kind: "refresh", ...(confirmHandEdit ? { confirmHandEdit: true } : {}) });
503
+ }
504
+
505
+ __testToggle(enabled = !this.#enabled): Promise<void> {
506
+ return this.#emit({ kind: "toggle", enabled });
507
+ }
508
+
509
+ __testClear(): Promise<void> {
510
+ return this.#emit({ kind: "clear" });
511
+ }
512
+
513
+ __testConfirm(): Promise<void> {
514
+ return this.#emitConfirmed();
515
+ }
516
+
517
+ /**
518
+ * Replay the intent the confirmation was raised for. A `clear` confirmation always emits `clear`;
519
+ * a hand-edit confirmation re-emits the ORIGINAL intent with `confirmHandEdit`, so a guarded Apply
520
+ * commits its own edited draft instead of being downgraded to a Refresh of the recorded setup.
521
+ */
522
+ #emitConfirmed(): Promise<void> {
523
+ if (this.#confirmation === "clear") return this.#emit({ kind: "clear" });
524
+ if (this.#confirmation !== "hand-edit") return Promise.resolve();
525
+ const pending = this.#pendingIntent;
526
+ this.#pendingIntent = undefined;
527
+ if (pending?.kind === "apply")
528
+ return this.#emit({ kind: "apply", draft: pending.draft, preview: pending.preview, confirmHandEdit: true });
529
+ return this.#emit({ kind: "refresh", confirmHandEdit: true });
530
+ }
531
+ }
@@ -1048,9 +1048,12 @@ export class StatusLineComponent implements Component {
1048
1048
  render(width: number): string[] {
1049
1049
  const lines: string[] = [];
1050
1050
  this.#refreshSkillHudInBackground();
1051
- const skillHud = this.#settings.showSkillHud === false ? null : renderSkillHudBar(this.#skillHudEntries, width);
1052
- if (skillHud) {
1053
- lines.push(skillHud);
1051
+ const skillHudRows =
1052
+ this.#settings.showSkillHud === false ? null : renderSkillHudBar(this.#skillHudEntries, width);
1053
+ if (skillHudRows) {
1054
+ for (const skillHudRow of skillHudRows) {
1055
+ if (skillHudRow) lines.push(truncateToWidth(skillHudRow, width));
1056
+ }
1054
1057
  }
1055
1058
 
1056
1059
  const statusRows = this.#buildStatusRows(width, this.#resolveMaxRows());
@@ -188,6 +188,10 @@ export class CommandController {
188
188
  const customShare = await loadCustomShare();
189
189
  if (customShare) {
190
190
  const loader = new BorderedLoader(this.ctx.ui, theme, "Sharing...");
191
+ // The composer is reusable across overlays; detach it before clearing so
192
+ // clear() disposes only the transient loader, not the editor's
193
+ // tab-width listener / paste state (disposal is terminal).
194
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
191
195
  this.ctx.editorContainer.clear();
192
196
  this.ctx.editorContainer.addChild(loader);
193
197
  this.ctx.ui.setFocus(loader);
@@ -244,6 +248,10 @@ export class CommandController {
244
248
  return;
245
249
  }
246
250
 
251
+ // The composer is reusable across overlays; detach it before clearing so
252
+ // clear() disposes only the transient loader, not the editor's
253
+ // tab-width listener / paste state (disposal is terminal).
254
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
247
255
  const loader = new BorderedLoader(this.ctx.ui, theme, "Creating gist...");
248
256
  this.ctx.editorContainer.clear();
249
257
  this.ctx.editorContainer.addChild(loader);
@@ -1153,13 +1161,15 @@ export class CommandController {
1153
1161
  }
1154
1162
 
1155
1163
  try {
1156
- await this.ctx.sessionManager.flush();
1157
- await this.ctx.sessionManager.moveTo(resolvedPath);
1158
- setProjectDir(resolvedPath);
1159
- clearClaudePluginRootsCache(); // re-warms preloadedPluginRoots with new project dir (async)
1160
- resetCapabilities();
1161
- await this.ctx.refreshSlashCommandState(resolvedPath);
1162
- await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
1164
+ await this.ctx.sessionManager.runExclusiveCwdTransition(async () => {
1165
+ await this.ctx.sessionManager.flush();
1166
+ await this.ctx.sessionManager.moveTo(resolvedPath);
1167
+ setProjectDir(resolvedPath);
1168
+ clearClaudePluginRootsCache();
1169
+ resetCapabilities();
1170
+ await this.ctx.refreshSlashCommandState(resolvedPath);
1171
+ await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
1172
+ });
1163
1173
 
1164
1174
  this.ctx.statusLine.invalidate();
1165
1175
  this.ctx.updateEditorTopBorder();
@@ -15,6 +15,7 @@ import {
15
15
  type ComposerSubmissionOptions,
16
16
  canApplyComposerSubmission,
17
17
  type InteractiveModeContext,
18
+ stopInteractiveActivityIndicator,
18
19
  } from "../../modes/types";
19
20
  import type { AgentSessionEvent, QueuedMessageEditEntry } from "../../session/agent-session";
20
21
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
@@ -219,9 +220,13 @@ export class InputController {
219
220
  case "app.queue.togglePane":
220
221
  return true;
221
222
  case "app.message.sendNow":
223
+ // `sendNow()` falls back to `getQueuedMessageEntries()[0]`, which returns
224
+ // only visible steering/follow-up entries, so advertising the action for
225
+ // hidden next-turn context enabled a keybinding whose only outcome is
226
+ // "No visible queued message to send".
222
227
  return (
223
228
  this.ctx.session.isStreaming &&
224
- (this.ctx.editor.getText().trim().length > 0 || this.ctx.session.queuedMessageCount > 0)
229
+ (this.ctx.editor.getText().trim().length > 0 || this.ctx.session.drainableQueuedMessageCount > 0)
225
230
  );
226
231
  default:
227
232
  return true;
@@ -356,8 +361,29 @@ export class InputController {
356
361
  if (this.ctx.cancelPendingSubmission()) {
357
362
  return true;
358
363
  }
359
- this.restoreQueuedMessagesToEditor({ abort: true });
360
- return true;
364
+ // Only count queues this handler can actually drain. The aggregate
365
+ // `queuedMessageCount` also counts hidden next-turn context, which
366
+ // `restoreQueuedMessagesToEditor()` never returns and which survives turn
367
+ // completion by design, so gating on the aggregate left a permanently
368
+ // nonzero count that made every press a no-op abort — the #4741 lockout.
369
+ const hasCancellableWork =
370
+ this.ctx.hasPendingSubmission() ||
371
+ this.ctx.session.drainableQueuedMessageCount > 0 ||
372
+ (this.ctx.compactionQueuedMessages?.length ?? 0) > 0 ||
373
+ this.ctx.session.isStreaming ||
374
+ this.ctx.session.isCompacting;
375
+ if (hasCancellableWork) {
376
+ this.restoreQueuedMessagesToEditor({ abort: true });
377
+ return true;
378
+ }
379
+ // The loader outlived its turn and no submission is pending (a started
380
+ // submission still inside prompt preflight stays cancellable above, so
381
+ // Esc aborts it instead of letting it start later), so nothing is
382
+ // cancellable even though the busy indicator is still mounted (#4741).
383
+ // Stop the stale indicator so completed work clears the busy state and
384
+ // keep evaluating the remaining states; when none match, the key falls
385
+ // through to idle semantics instead of a no-op abort that swallows it.
386
+ stopInteractiveActivityIndicator(this.ctx);
361
387
  }
362
388
  if (options.processes && this.ctx.session.isBashRunning) {
363
389
  this.ctx.session.abortBash();
@@ -440,7 +466,7 @@ export class InputController {
440
466
  if (isClearKey && !isInterruptKey) {
441
467
  if (this.#handlePendingSteerInterrupt()) return { consume: true };
442
468
  if (
443
- !this.#handleCancellableWorkEscape({
469
+ this.#handleCancellableWorkEscape({
444
470
  loading: true,
445
471
  processes: true,
446
472
  modes: true,
@@ -449,10 +475,12 @@ export class InputController {
449
475
  streaming: true,
450
476
  })
451
477
  ) {
452
- return undefined;
478
+ this.#resetEscapeGestures();
479
+ return { consume: true };
453
480
  }
454
- this.#resetEscapeGestures();
455
- return { consume: true };
481
+ // A stale loader was stopped and nothing else consumed the clear key:
482
+ // release it to the editor so the idle clear path still runs (#4741).
483
+ return undefined;
456
484
  }
457
485
  if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
458
486
  this.#resetEscapeGestures();
@@ -861,8 +889,11 @@ export class InputController {
861
889
  }
862
890
  }
863
891
 
864
- // Empty submit while streaming with queued messages: flush queues immediately
865
- if (!text && this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount > 0) {
892
+ // Empty submit while streaming with queued messages: flush queues immediately.
893
+ // Only drainable queues justify the abort — aborting for hidden next-turn
894
+ // context would kill the live turn without flushing anything, since that
895
+ // context is not delivered by the resume path this abort relies on.
896
+ if (!text && this.ctx.session.isStreaming && this.ctx.session.drainableQueuedMessageCount > 0) {
866
897
  // Abort current stream and let queued messages be processed
867
898
  await this.#abortInteractive();
868
899
  return;
@@ -1341,6 +1372,10 @@ export class InputController {
1341
1372
  ),
1342
1373
  },
1343
1374
  );
1375
+ // The composer is reusable across overlays; detach it before clearing so
1376
+ // clear() disposes only the transient selector, not the editor's
1377
+ // tab-width listener (disposal is terminal).
1378
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
1344
1379
  this.ctx.editorContainer.clear();
1345
1380
  this.ctx.editorContainer.addChild(selector);
1346
1381
  this.ctx.ui.setFocus(selector);