@gajae-code/coding-agent 0.11.1 → 0.11.3

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 (413) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/launch-disposition.d.ts +23 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +141 -11
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  54. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  55. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  56. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  57. package/dist/types/goals/runtime.d.ts +1 -0
  58. package/dist/types/goals/state.d.ts +8 -0
  59. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  60. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  61. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  62. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  63. package/dist/types/hindsight/state.d.ts +11 -4
  64. package/dist/types/lsp/lspmux.d.ts +5 -3
  65. package/dist/types/lsp/path-trust.d.ts +1 -0
  66. package/dist/types/main.d.ts +4 -2
  67. package/dist/types/memories/storage.d.ts +1 -0
  68. package/dist/types/modes/action-registry.d.ts +29 -0
  69. package/dist/types/modes/components/chrome.d.ts +14 -0
  70. package/dist/types/modes/components/command-palette.d.ts +27 -0
  71. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  72. package/dist/types/modes/components/index.d.ts +2 -1
  73. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  74. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  75. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  76. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  77. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  78. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  79. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  80. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  81. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  82. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  83. package/dist/types/modes/components/status-line.d.ts +1 -94
  84. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  85. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  86. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  87. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  88. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  89. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  90. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  91. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  92. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  93. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  94. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  95. package/dist/types/modes/interactive-mode.d.ts +19 -17
  96. package/dist/types/modes/jobs-observer.d.ts +2 -0
  97. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  98. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  99. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  100. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  101. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  102. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  103. package/dist/types/modes/types.d.ts +20 -13
  104. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  105. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  106. package/dist/types/sdk/broker/broker.d.ts +78 -12
  107. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  108. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  109. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  110. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  111. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  112. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  113. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  114. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  117. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  118. package/dist/types/sdk/bus/index.d.ts +29 -0
  119. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  120. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  121. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  122. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  123. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  124. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  125. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  126. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  127. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  128. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  129. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  130. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  131. package/dist/types/sdk/host/host.d.ts +2 -0
  132. package/dist/types/sdk/index.d.ts +1 -0
  133. package/dist/types/sdk/session-directory.d.ts +56 -0
  134. package/dist/types/sdk/session.d.ts +2 -0
  135. package/dist/types/session/agent-session.d.ts +82 -47
  136. package/dist/types/session/agent-storage.d.ts +7 -0
  137. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  138. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  139. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  140. package/dist/types/session/messages.d.ts +16 -0
  141. package/dist/types/session/session-manager-internal.d.ts +14 -0
  142. package/dist/types/session/session-manager.d.ts +82 -12
  143. package/dist/types/session/session-storage.d.ts +43 -3
  144. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  145. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  146. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  147. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  148. package/dist/types/system-prompt.d.ts +2 -11
  149. package/dist/types/task/executor.d.ts +3 -1
  150. package/dist/types/thinking.d.ts +7 -0
  151. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  152. package/dist/types/tools/ask.d.ts +64 -2
  153. package/dist/types/tools/bash.d.ts +3 -0
  154. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  155. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  156. package/dist/types/tools/cron.d.ts +2 -0
  157. package/dist/types/tools/fetch.d.ts +1 -0
  158. package/dist/types/tools/index.d.ts +30 -15
  159. package/dist/types/tools/output-meta.d.ts +1 -0
  160. package/dist/types/tools/read.d.ts +5 -0
  161. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  162. package/dist/types/tools/subagent.d.ts +1 -0
  163. package/dist/types/utils/event-bus.d.ts +1 -1
  164. package/dist/types/utils/file-mentions.d.ts +3 -3
  165. package/dist/types/web/search/index.d.ts +3 -1
  166. package/package.json +21 -9
  167. package/scripts/build-sdk-package-smoke.ts +34 -1
  168. package/scripts/generate-hotkeys-docs.ts +74 -0
  169. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  170. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  171. package/src/async/job-manager.ts +49 -6
  172. package/src/cli/config-cli.ts +75 -2
  173. package/src/cli/launch-disposition.ts +42 -0
  174. package/src/cli/notify-cli.ts +92 -26
  175. package/src/cli/session-picker.ts +20 -3
  176. package/src/cli/setup-cli.ts +1 -1
  177. package/src/cli.ts +1 -1
  178. package/src/commands/config.ts +1 -1
  179. package/src/commands/harness.ts +34 -106
  180. package/src/commands/launch.ts +6 -4
  181. package/src/commands/mcp-serve.ts +85 -8
  182. package/src/commands/mcp.ts +2 -2
  183. package/src/commands/notify.ts +8 -4
  184. package/src/commands/sdk.ts +61 -40
  185. package/src/commands/ultragoal.ts +4 -1
  186. package/src/config/atomic-yaml-patch.ts +80 -13
  187. package/src/config/file-lock.ts +105 -43
  188. package/src/config/keybindings.ts +64 -20
  189. package/src/config/model-auth.ts +9 -0
  190. package/src/config/model-bindings-applier.ts +139 -0
  191. package/src/config/model-discovery-manager.ts +224 -0
  192. package/src/config/model-equivalence.ts +66 -0
  193. package/src/config/model-registry.ts +169 -302
  194. package/src/config/model-resolver.ts +361 -369
  195. package/src/config/models-config-schema.ts +5 -4
  196. package/src/config/settings-schema.ts +214 -12
  197. package/src/config/settings.ts +458 -482
  198. package/src/coordinator-mcp/policy.ts +76 -11
  199. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  200. package/src/coordinator-mcp/question-state.ts +778 -0
  201. package/src/coordinator-mcp/server.ts +1702 -238
  202. package/src/debug/index.ts +1 -1
  203. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  204. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  205. package/src/discovery/helpers.ts +1 -50
  206. package/src/edit/index.ts +64 -0
  207. package/src/eval/py/env.ts +26 -0
  208. package/src/eval/py/kernel.ts +5 -3
  209. package/src/extensibility/extensions/loader.ts +9 -1
  210. package/src/extensibility/extensions/runner.ts +12 -3
  211. package/src/extensibility/extensions/types.ts +39 -1
  212. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  213. package/src/extensibility/hooks/runner.ts +2 -2
  214. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  215. package/src/extensibility/shared-events.ts +2 -0
  216. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  217. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  218. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  219. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  220. package/src/gjc-runtime/launch-tmux.ts +2 -5
  221. package/src/gjc-runtime/launch-worktree.ts +5 -5
  222. package/src/gjc-runtime/linux-proc.ts +71 -0
  223. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  224. package/src/gjc-runtime/session-resolution.ts +18 -3
  225. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  226. package/src/gjc-runtime/state-migrations.ts +1 -1
  227. package/src/gjc-runtime/state-runtime.ts +327 -249
  228. package/src/gjc-runtime/state-writer.ts +36 -40
  229. package/src/gjc-runtime/team-launch.ts +319 -0
  230. package/src/gjc-runtime/team-notify.ts +326 -0
  231. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  232. package/src/gjc-runtime/team-store.ts +828 -0
  233. package/src/gjc-runtime/team-workers.ts +962 -0
  234. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  235. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  236. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  237. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  238. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  239. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  240. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  241. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  242. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  243. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  244. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  245. package/src/goals/continuation-timeout-guard.ts +79 -0
  246. package/src/goals/runtime.ts +7 -5
  247. package/src/goals/state.ts +18 -0
  248. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  249. package/src/harness-control-plane/phase-rollup.ts +4 -7
  250. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  251. package/src/harness-control-plane/receipt-spool.ts +51 -7
  252. package/src/harness-control-plane/session-lease.ts +12 -130
  253. package/src/hindsight/backend.ts +26 -27
  254. package/src/hindsight/state.ts +101 -32
  255. package/src/hooks/skill-state.ts +17 -11
  256. package/src/internal-urls/docs-index.generated.ts +21 -18
  257. package/src/lsp/client.ts +1 -1
  258. package/src/lsp/config.ts +145 -26
  259. package/src/lsp/index.ts +1 -1
  260. package/src/lsp/lspmux.ts +39 -10
  261. package/src/lsp/path-trust.ts +81 -0
  262. package/src/main.ts +127 -30
  263. package/src/memories/index.ts +1 -0
  264. package/src/memories/storage.ts +6 -2
  265. package/src/modes/action-registry.ts +165 -0
  266. package/src/modes/components/chrome.ts +40 -0
  267. package/src/modes/components/command-palette.ts +168 -0
  268. package/src/modes/components/custom-editor.ts +1 -1
  269. package/src/modes/components/custom-provider-wizard.ts +51 -14
  270. package/src/modes/components/index.ts +2 -1
  271. package/src/modes/components/pet-selector.ts +11 -17
  272. package/src/modes/components/plan-preview-overlay.ts +295 -0
  273. package/src/modes/components/queue-mode-selector.ts +10 -27
  274. package/src/modes/components/queue-pane.ts +79 -0
  275. package/src/modes/components/read-tool-group.ts +13 -0
  276. package/src/modes/components/session-observer-overlay.ts +207 -772
  277. package/src/modes/components/sessions-dashboard.ts +181 -0
  278. package/src/modes/components/settings-selector.ts +1 -1
  279. package/src/modes/components/show-images-selector.ts +10 -24
  280. package/src/modes/components/status-line/types.ts +1 -1
  281. package/src/modes/components/status-line.ts +1 -969
  282. package/src/modes/components/tasks-pane.ts +73 -0
  283. package/src/modes/components/theme-selector.ts +11 -31
  284. package/src/modes/components/tool-execution.ts +22 -0
  285. package/src/modes/components/tool-status-header.ts +1056 -0
  286. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  287. package/src/modes/controllers/btw-controller.ts +1 -0
  288. package/src/modes/controllers/event-controller.ts +3 -2
  289. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  290. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  291. package/src/modes/controllers/input-controller.ts +764 -229
  292. package/src/modes/controllers/mode-gate.ts +24 -0
  293. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  294. package/src/modes/controllers/selector-controller.ts +225 -10
  295. package/src/modes/controllers/stt-controller.ts +125 -0
  296. package/src/modes/interactive-mode.ts +289 -1150
  297. package/src/modes/jobs-observer.ts +3 -0
  298. package/src/modes/prompt-action-autocomplete.ts +7 -0
  299. package/src/modes/runtime-init.ts +8 -0
  300. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  301. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  302. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  303. package/src/modes/tasks-aggregator.ts +199 -0
  304. package/src/modes/transcript-item-registry.ts +229 -0
  305. package/src/modes/types.ts +45 -18
  306. package/src/modes/utils/ui-helpers.ts +17 -9
  307. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  308. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  309. package/src/prompts/agents/architect.md +2 -8
  310. package/src/prompts/agents/critic.md +2 -8
  311. package/src/prompts/agents/executor.md +3 -0
  312. package/src/prompts/agents/planner.md +4 -12
  313. package/src/prompts/system/custom-system-prompt.md +1 -23
  314. package/src/prompts/system/plan-mode-approved.md +8 -0
  315. package/src/prompts/system/system-prompt.md +17 -96
  316. package/src/prompts/tools/bash.md +4 -4
  317. package/src/prompts/tools/browser.md +15 -52
  318. package/src/prompts/tools/find.md +2 -2
  319. package/src/prompts/tools/hashline.md +13 -55
  320. package/src/prompts/tools/read.md +3 -6
  321. package/src/prompts/tools/search-tool-bm25.md +0 -2
  322. package/src/prompts/tools/search.md +2 -4
  323. package/src/prompts/tools/skill-discovery.md +1 -2
  324. package/src/prompts/tools/task.md +1 -1
  325. package/src/runtime-mcp/manager.ts +1 -1
  326. package/src/sdk/broker/broker.ts +206 -78
  327. package/src/sdk/broker/discovery.ts +1 -1
  328. package/src/sdk/broker/ensure.ts +3 -1
  329. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  330. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  331. package/src/sdk/broker/lifecycle.ts +1399 -142
  332. package/src/sdk/broker/process-incarnation.ts +5 -13
  333. package/src/sdk/broker/session-index.ts +145 -56
  334. package/src/sdk/broker/state-version.ts +21 -0
  335. package/src/sdk/broker/transport.ts +1 -1
  336. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  337. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  338. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  339. package/src/sdk/bus/config-commands.ts +5 -4
  340. package/src/sdk/bus/config.ts +98 -0
  341. package/src/sdk/bus/conversation-store.ts +5 -29
  342. package/src/sdk/bus/daemon-paths.ts +2 -0
  343. package/src/sdk/bus/html-format.ts +20 -3
  344. package/src/sdk/bus/index.ts +782 -94
  345. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  346. package/src/sdk/bus/natives-augment.d.ts +26 -1
  347. package/src/sdk/bus/notification-service.ts +168 -20
  348. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  349. package/src/sdk/bus/recent-activity.ts +188 -74
  350. package/src/sdk/bus/rich-document.ts +258 -0
  351. package/src/sdk/bus/rich-render.ts +62 -70
  352. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  353. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  354. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  355. package/src/sdk/bus/threaded-inbound.ts +8 -3
  356. package/src/sdk/bus/threaded-render.ts +57 -1
  357. package/src/sdk/bus/topic-registry.ts +120 -13
  358. package/src/sdk/host/control/dispatch.ts +14 -2
  359. package/src/sdk/host/control/operations.ts +6 -1
  360. package/src/sdk/host/host.ts +44 -2
  361. package/src/sdk/host/query/handlers.ts +20 -22
  362. package/src/sdk/host/query/revision-store.ts +80 -57
  363. package/src/sdk/index.ts +1 -0
  364. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  365. package/src/sdk/session-directory.ts +141 -0
  366. package/src/sdk/session.ts +462 -480
  367. package/src/session/agent-session.ts +1700 -583
  368. package/src/session/agent-storage.ts +18 -0
  369. package/src/session/cache-economics.ts +25 -8
  370. package/src/session/fallback-chain-controller.ts +53 -3
  371. package/src/session/internal/managed-session-scope.ts +2226 -0
  372. package/src/session/internal/managed-session-storage.ts +453 -0
  373. package/src/session/messages.ts +91 -2
  374. package/src/session/session-manager-internal.ts +33 -0
  375. package/src/session/session-manager.ts +1112 -333
  376. package/src/session/session-storage.ts +444 -39
  377. package/src/session/volatile-context-pruning.ts +78 -0
  378. package/src/setup/credential-auto-import.ts +2 -1
  379. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  380. package/src/setup/provider-onboarding.ts +36 -61
  381. package/src/setup/provider-presets.json +5 -0
  382. package/src/skill-state/active-state.ts +3 -13
  383. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  384. package/src/skill-state/workflow-state-contract.ts +6 -41
  385. package/src/slash-commands/builtin-registry.ts +46 -11
  386. package/src/system-prompt.ts +37 -44
  387. package/src/task/agents.ts +19 -1
  388. package/src/task/executor.ts +90 -6
  389. package/src/task/index.ts +36 -21
  390. package/src/thinking.ts +21 -6
  391. package/src/tool-discovery/tool-index.ts +2 -134
  392. package/src/tools/ask.ts +157 -21
  393. package/src/tools/ast-edit.ts +1 -1
  394. package/src/tools/bash.ts +38 -0
  395. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  396. package/src/tools/browser/tab-supervisor.ts +282 -36
  397. package/src/tools/computer.ts +10 -3
  398. package/src/tools/cron.ts +30 -16
  399. package/src/tools/eval.ts +1 -2
  400. package/src/tools/fetch.ts +11 -4
  401. package/src/tools/index.ts +76 -24
  402. package/src/tools/output-meta.ts +19 -7
  403. package/src/tools/read.ts +219 -64
  404. package/src/tools/resource-gc.ts +2 -2
  405. package/src/tools/search-tool-bm25.ts +11 -74
  406. package/src/tools/subagent.ts +43 -2
  407. package/src/utils/event-bus.ts +9 -4
  408. package/src/utils/file-mentions.ts +4 -4
  409. package/src/web/search/index.ts +3 -3
  410. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  411. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  412. package/src/discovery/plugin-dir-roots.ts +0 -28
  413. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
5
5
  import { $env, sanitizeText } from "@gajae-code/utils";
6
+ import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
6
7
  import { isSettingsInitialized, settings } from "../../config/settings";
7
8
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
8
9
  import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
@@ -10,9 +11,14 @@ import { expandEmoticons } from "../../modes/emoji-autocomplete";
10
11
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
11
12
  import { theme } from "../../modes/theme/theme";
12
13
  import { scrollTmuxToPreviousUserInput as scrollTmuxPaneToPreviousUserInput } from "../../modes/tmux-scroll";
13
- import type { InteractiveModeContext } from "../../modes/types";
14
+ import {
15
+ type ComposerSubmissionOptions,
16
+ canApplyComposerSubmission,
17
+ type InteractiveModeContext,
18
+ } from "../../modes/types";
14
19
  import type { AgentSessionEvent, QueuedMessageEditEntry } from "../../session/agent-session";
15
20
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
21
+ import { getUserMessageViewportAnchorIds } from "../../session/session-manager";
16
22
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
17
23
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
18
24
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
@@ -20,13 +26,21 @@ import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } fr
20
26
  import { resizeImage } from "../../utils/image-resize";
21
27
  import { formatPastedImageReference, resolvePastedImagePath } from "../../utils/pasted-image-path";
22
28
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
29
+ import { ActionRegistry, APP_ACTION_METADATA } from "../action-registry";
30
+ import { CommandPalette, type CommandPaletteAction, type CommandPaletteEntry } from "../components/command-palette";
31
+ import { QueuePaneComponent } from "../components/queue-pane";
23
32
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
24
33
 
25
34
  interface Expandable {
26
35
  setExpanded(expanded: boolean): void;
36
+ setManuallyExpanded?(expanded: boolean): void;
27
37
  }
28
38
 
29
- const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
39
+ export const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
40
+ export const BACKGROUND_FOLD_DOUBLE_PRESS_MS = 750;
41
+
42
+ const DRAFT_CLEAR_DOUBLE_ESCAPE_WINDOW_MS = 800;
43
+ const EMPTY_EDITOR_DOUBLE_ESCAPE_WINDOW_MS = 500;
30
44
  const IMAGE_PLACEHOLDER_PATTERN = /\[image ([1-9]\d*)\]/g;
31
45
  const IMAGE_PLACEHOLDER_PRESENT_PATTERN = /\[image [1-9]\d*\]/;
32
46
 
@@ -35,16 +49,222 @@ function isExpandable(obj: unknown): obj is Expandable {
35
49
  }
36
50
 
37
51
  export class InputController {
38
- constructor(private ctx: InteractiveModeContext) {}
52
+ readonly actionRegistry: ActionRegistry<void>;
53
+
54
+ constructor(private ctx: InteractiveModeContext) {
55
+ this.actionRegistry = new ActionRegistry({
56
+ context: undefined,
57
+ showError: actionId => this.ctx.showError(actionId),
58
+ });
59
+ this.#registerActions();
60
+ }
61
+
62
+ #registerActions(): void {
63
+ const callbacks: Partial<Record<(typeof APP_ACTION_METADATA)[number]["id"], () => void | Promise<void>>> = {
64
+ "app.interrupt": () => this.ctx.editor.onEscape?.(),
65
+ "app.clear": () => this.handleCtrlC(),
66
+ "app.exit": () => this.handleCtrlD(),
67
+ "app.suspend": () => this.handleCtrlZ(),
68
+ "app.thinking.cycle": () => this.cycleThinkingLevel(),
69
+ "app.thinking.toggle": () => this.toggleThinkingBlockVisibility(),
70
+ "app.commandPalette.open": () => this.openCommandPalette(),
71
+ "app.model.cycleForward": () => this.cycleRoleModel(),
72
+ "app.model.cycleBackward": () => this.cycleRoleModel({ temporary: true }),
73
+ "app.model.select": () => this.ctx.showModelSelector(),
74
+ "app.model.selectTemporary": () => this.ctx.showModelSelector({ temporaryOnly: true }),
75
+ "app.tools.expand": () => this.toggleToolOutputExpansion(),
76
+ "app.tool.backgroundFold": () => {
77
+ this.handleForegroundToolBackgroundFold();
78
+ },
79
+ "app.editor.external": () => this.openExternalEditor(),
80
+ "app.message.followUp": () => this.handleFollowUp(),
81
+ "app.message.queue": () => this.handleQueueSubmit(),
82
+ "app.message.dequeue": () => this.handleDequeue(),
83
+ "app.clipboard.pasteImage": async () => {
84
+ await this.handleImagePaste();
85
+ },
86
+ "app.clipboard.copyLine": () => this.handleCopyCurrentLine(),
87
+ "app.clipboard.copyPrompt": () => this.handleCopyPrompt(),
88
+ "app.session.new": async () => {
89
+ await this.ctx.handleClearCommand();
90
+ },
91
+ "app.session.tree": () => this.ctx.showTreeSelector(),
92
+ "app.session.fork": () => this.ctx.showUserMessageSelector(),
93
+ "app.session.resume": () => this.ctx.showSessionSelector(),
94
+ "app.session.observe": async () => {
95
+ await this.ctx.showSessionObserver();
96
+ },
97
+ "app.session.dashboard": () => this.ctx.showSessionsDashboard(),
98
+ "app.transcript.browse": () => this.ctx.showTranscriptViewer(),
99
+ "app.jobs.open": () => this.ctx.showJobsOverlay(),
100
+ "app.plan.toggle": () => this.ctx.planModeController.handleCommand(),
101
+ "app.mode.cycle": () => this.ctx.planModeController.handleCommand(),
102
+ "app.history.search": () => this.ctx.showHistorySearch(),
103
+ "app.stt.toggle": () => this.ctx.handleSTTToggle(),
104
+ "app.irc.sidebar.toggle": () => this.ctx.toggleIrcSidebar(),
105
+ "app.transcript.prevTurn": () => this.#jumpTranscriptTurn(-1),
106
+ "app.transcript.nextTurn": () => this.#jumpTranscriptTurn(1),
107
+ "app.tasks.toggle": () => this.ctx.showTasksPane(),
108
+ "app.queue.togglePane": () => this.toggleQueuePane(),
109
+ "app.message.sendNow": () => this.sendNow(),
110
+ };
111
+ for (const metadata of APP_ACTION_METADATA) {
112
+ const callback = callbacks[metadata.id];
113
+ this.actionRegistry.register({
114
+ ...metadata,
115
+ availability: () => Boolean(callback) && this.#isActionAvailable(metadata.id),
116
+ execute: async () => {
117
+ if (!callback) throw new Error(`Unavailable action executed: ${metadata.id}`);
118
+ await callback();
119
+ },
120
+ });
121
+ }
122
+ }
123
+
124
+ #isActionAvailable(id: (typeof APP_ACTION_METADATA)[number]["id"]): boolean {
125
+ switch (id) {
126
+ case "app.suspend":
127
+ return process.platform !== "win32";
128
+ case "app.thinking.cycle":
129
+ case "app.thinking.toggle":
130
+ return Boolean(this.ctx.session.model?.reasoning);
131
+ case "app.commandPalette.open":
132
+ return this.ctx.editor.getText().trim().length === 0;
133
+ case "app.model.cycleForward":
134
+ case "app.model.cycleBackward":
135
+ return this.ctx.session.getRoleModelCycleCandidateCount() > 1;
136
+ case "app.tools.expand":
137
+ return this.ctx.chatContainer.children.some(isExpandable);
138
+ case "app.tool.backgroundFold":
139
+ return Boolean(this.ctx.session.hasForegroundBashBackgroundRequestHandler?.());
140
+ case "app.editor.external":
141
+ return Boolean(getEditorCommand());
142
+ case "app.message.followUp":
143
+ case "app.message.queue":
144
+ return (
145
+ this.ctx.session.isStreaming ||
146
+ this.ctx.session.isCompacting ||
147
+ this.ctx.session.isBashRunning ||
148
+ this.ctx.session.isEvalRunning
149
+ );
150
+ case "app.message.dequeue":
151
+ return this.ctx.session.queuedMessageCount > 0;
152
+ case "app.clipboard.copyPrompt":
153
+ return this.ctx.editor.getText().length > 0;
154
+ case "app.session.tree":
155
+ case "app.session.fork":
156
+ return this.ctx.session.messages.length > 0;
157
+ case "app.plan.toggle":
158
+ return this.ctx.planModeController.enabled && !this.ctx.goalModeController.enabled;
159
+ case "app.history.search":
160
+ return (this.ctx.historyStorage?.getRecent(1).length ?? 0) > 0;
161
+ case "app.stt.toggle":
162
+ return Boolean(this.ctx.settings.get("stt.enabled"));
163
+ case "app.transcript.browse":
164
+ return this.ctx.session.messages.length > 0;
165
+ case "app.transcript.prevTurn":
166
+ case "app.transcript.nextTurn":
167
+ return getUserMessageViewportAnchorIds(this.ctx.session.messages).length > 0;
168
+ case "app.mode.cycle":
169
+ return (
170
+ Boolean(this.ctx.settings.get("plan.enabled")) &&
171
+ !this.ctx.goalModeController.enabled &&
172
+ !this.ctx.goalModeController.paused
173
+ );
174
+ case "app.queue.togglePane":
175
+ return true;
176
+ case "app.message.sendNow":
177
+ return (
178
+ this.ctx.session.isStreaming &&
179
+ (this.ctx.editor.getText().trim().length > 0 || this.ctx.session.queuedMessageCount > 0)
180
+ );
181
+ default:
182
+ return true;
183
+ }
184
+ }
185
+
186
+ #executeAction(id: (typeof APP_ACTION_METADATA)[number]["id"]): void {
187
+ void this.actionRegistry.execute(id);
188
+ }
189
+
190
+ #transcriptTurnAnchorIds: readonly string[] = [];
191
+ #transcriptTurnPosition = 0;
192
+
193
+ #syncTranscriptTurnPosition(): readonly string[] {
194
+ const anchorIds = getUserMessageViewportAnchorIds(this.ctx.session.messages);
195
+ if (
196
+ anchorIds.length !== this.#transcriptTurnAnchorIds.length ||
197
+ anchorIds.some(id => !this.#transcriptTurnAnchorIds.includes(id))
198
+ ) {
199
+ const hadAnchors = this.#transcriptTurnAnchorIds.length > 0;
200
+ const currentAnchorId = this.#transcriptTurnAnchorIds[this.#transcriptTurnPosition];
201
+ const retainedAnchorIds = this.#transcriptTurnAnchorIds.filter(id => anchorIds.includes(id));
202
+ this.#transcriptTurnAnchorIds = [
203
+ ...retainedAnchorIds,
204
+ ...anchorIds.filter(id => !retainedAnchorIds.includes(id)),
205
+ ];
206
+ const currentPosition = currentAnchorId ? this.#transcriptTurnAnchorIds.indexOf(currentAnchorId) : -1;
207
+ this.#transcriptTurnPosition =
208
+ currentPosition >= 0
209
+ ? currentPosition
210
+ : currentAnchorId || hadAnchors
211
+ ? Math.min(this.#transcriptTurnPosition, this.#transcriptTurnAnchorIds.length)
212
+ : this.#transcriptTurnAnchorIds.length;
213
+ }
214
+ return this.#transcriptTurnAnchorIds;
215
+ }
216
+
217
+ #jumpTranscriptTurn(direction: -1 | 1): void {
218
+ const anchorIds = this.#syncTranscriptTurnPosition();
219
+ const targetPosition = this.#transcriptTurnPosition + direction;
220
+ if (targetPosition < 0 || targetPosition >= anchorIds.length) return;
221
+ if (this.ctx.ui.revealViewportAnchor(anchorIds[targetPosition], "top")) {
222
+ this.#transcriptTurnPosition = targetPosition;
223
+ }
224
+ }
39
225
 
40
226
  #lastBackgroundFoldKeyTime = 0;
227
+ #slashCommands: SlashCommand[] = [];
41
228
 
42
229
  /** Set after a first Esc silently consumes a queued steer. Kept until the
43
230
  * queued steer is either cancelled by a second Esc or drained by continuation,
44
231
  * so abort cleanup going idle cannot turn the second Esc into an idle action. */
45
232
  #steerConsumePending = false;
233
+ #commandPaletteActions = new Map<AppKeybinding, CommandPaletteAction>();
234
+ #paletteCommandInFlight = false;
235
+
236
+ #registerCommandPaletteAction(action: AppKeybinding, handler: () => void | Promise<void>): void {
237
+ this.#commandPaletteActions.set(action, {
238
+ id: action,
239
+ label: KEYBINDINGS[action].description,
240
+ handler,
241
+ });
242
+ }
46
243
 
47
244
  #globalInterruptUnsubscribe: (() => void) | undefined;
245
+ #draftClearEscapeText: string | undefined;
246
+
247
+ #resetEscapeGestures(): void {
248
+ this.ctx.lastEscapeTime = 0;
249
+ this.ctx.lastComposerClearEscapeTime = 0;
250
+ this.#draftClearEscapeText = undefined;
251
+ }
252
+
253
+ #armDraftClearEscape(text: string, now: number): void {
254
+ this.ctx.lastEscapeTime = 0;
255
+ this.ctx.lastComposerClearEscapeTime = now;
256
+ this.#draftClearEscapeText = text;
257
+ this.ctx.showStatus("press Esc again to clear");
258
+ }
259
+
260
+ #resetDraftClearEscape(): void {
261
+ this.ctx.lastComposerClearEscapeTime = 0;
262
+ this.#draftClearEscapeText = undefined;
263
+ }
264
+
265
+ #resetEmptyEditorEscape(): void {
266
+ this.ctx.lastEscapeTime = 0;
267
+ }
48
268
 
49
269
  #matchesInterruptKey(data: string): boolean {
50
270
  return this.ctx.keybindings.getKeys("app.interrupt").some(key => matchesKey(data, key));
@@ -146,6 +366,7 @@ export class InputController {
146
366
  return undefined;
147
367
  }
148
368
  if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
369
+ this.#resetEscapeGestures();
149
370
  return { consume: true };
150
371
  }
151
372
  const hookDialogActive = this.#hasHookDialog();
@@ -153,6 +374,7 @@ export class InputController {
153
374
  // Inline ask/custom-input editors use Esc to return to the option list.
154
375
  // Let the focused selector see the key instead of converting a typo
155
376
  // into a full workflow/session abort while the agent is streaming.
377
+ this.#resetEscapeGestures();
156
378
  return undefined;
157
379
  }
158
380
  if (
@@ -165,6 +387,7 @@ export class InputController {
165
387
  streaming: hookDialogActive,
166
388
  })
167
389
  ) {
390
+ this.#resetEscapeGestures();
168
391
  return { consume: true };
169
392
  }
170
393
  return undefined;
@@ -178,11 +401,18 @@ export class InputController {
178
401
  silent: options?.silent,
179
402
  });
180
403
  }
404
+ #clearComposer(): void {
405
+ const text = this.ctx.editor.getText();
406
+ if (text.trim()) {
407
+ this.ctx.editor.addToHistory(text);
408
+ }
409
+ this.ctx.clearEditor();
410
+ }
181
411
 
182
412
  setupKeyHandlers(): void {
183
413
  this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
184
- this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
185
- Boolean(
414
+ this.ctx.editor.shouldBypassAutocompleteOnEscape = () => {
415
+ const bypassAutocomplete = Boolean(
186
416
  this.ctx.loadingAnimation ||
187
417
  this.ctx.hasActiveBtw() ||
188
418
  (this.#steerConsumePending && this.ctx.session.hasQueuedSteering) ||
@@ -197,18 +427,28 @@ export class InputController {
197
427
  this.ctx.autoCompactionEscapeHandler ||
198
428
  this.ctx.retryEscapeHandler,
199
429
  );
430
+ if (!bypassAutocomplete) this.#resetEscapeGestures();
431
+ return bypassAutocomplete;
432
+ };
200
433
  this.#installGlobalInterruptListener();
201
434
 
202
435
  // An open btw panel must stay dismissable with Esc even while another
203
436
  // controller (auto-compaction, auto-retry, manual compaction, etc.) has
204
437
  // temporarily replaced editor.onEscape. This priority hook is never
205
438
  // swapped out, so it always wins for the interrupt key.
206
- this.ctx.editor.onInterruptPriority = () => (this.ctx.hasActiveBtw() ? this.ctx.handleBtwEscape() : false);
439
+ this.ctx.editor.onInterruptPriority = () => {
440
+ if (!this.ctx.hasActiveBtw()) return false;
441
+ const consumed = this.ctx.handleBtwEscape();
442
+ if (consumed) this.#resetEscapeGestures();
443
+ return consumed;
444
+ };
207
445
  this.ctx.editor.onEscape = () => {
208
446
  if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
447
+ this.#resetEscapeGestures();
209
448
  return;
210
449
  }
211
450
  if (this.#steerConsumePending) {
451
+ this.#resetEscapeGestures();
212
452
  if (this.ctx.session.hasQueuedSteering) {
213
453
  // Second Esc before the scheduled steer continuation drains the
214
454
  // queue: restore/drop the queued steer and perform a real abort,
@@ -220,20 +460,7 @@ export class InputController {
220
460
  this.#steerConsumePending = false;
221
461
  }
222
462
  if (this.#handleCancellableWorkEscape({ maintenance: true, retry: true })) {
223
- return;
224
- }
225
- // Normal input state with user-typed text: Esc must not interrupt a
226
- // running task (streaming turn, bash/eval). A double Esc within the
227
- // 500ms window clears the composer instead. Bash/Python input modes
228
- // keep their own Esc handling in the chain below.
229
- if (!this.ctx.isBashMode && !this.ctx.isPythonMode && this.ctx.editor.getText().trim()) {
230
- const now = Date.now();
231
- if (now - this.ctx.lastComposerClearEscapeTime < 500) {
232
- this.ctx.clearEditor();
233
- this.ctx.lastComposerClearEscapeTime = 0;
234
- } else {
235
- this.ctx.lastComposerClearEscapeTime = now;
236
- }
463
+ this.#resetEscapeGestures();
237
464
  return;
238
465
  }
239
466
  if (
@@ -246,68 +473,118 @@ export class InputController {
246
473
  streaming: true,
247
474
  })
248
475
  ) {
476
+ this.#resetEscapeGestures();
477
+ return;
478
+ }
479
+
480
+ const text = this.ctx.editor.getText();
481
+ if (!this.ctx.isBashMode && !this.ctx.isPythonMode && text.trim()) {
482
+ this.#resetEmptyEditorEscape();
483
+ const now = Date.now();
484
+ if (
485
+ now - this.ctx.lastComposerClearEscapeTime < DRAFT_CLEAR_DOUBLE_ESCAPE_WINDOW_MS &&
486
+ this.#draftClearEscapeText === text
487
+ ) {
488
+ this.#clearComposer();
489
+ this.#resetDraftClearEscape();
490
+ } else {
491
+ this.#armDraftClearEscape(text, now);
492
+ }
249
493
  return;
250
494
  }
251
- if (!this.ctx.editor.getText().trim()) {
495
+
496
+ if (!text.trim()) {
497
+ this.#resetDraftClearEscape();
252
498
  // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
253
499
  const action = settings.get("doubleEscapeAction");
254
- if (action !== "none") {
255
- const now = Date.now();
256
- if (now - this.ctx.lastEscapeTime < 500) {
257
- if (action === "tree") {
258
- this.ctx.showTreeSelector();
259
- } else {
260
- this.ctx.showUserMessageSelector();
261
- }
262
- this.ctx.lastEscapeTime = 0;
500
+ if (action === "none") {
501
+ this.#resetEmptyEditorEscape();
502
+ return;
503
+ }
504
+
505
+ const now = Date.now();
506
+ if (now - this.ctx.lastEscapeTime < EMPTY_EDITOR_DOUBLE_ESCAPE_WINDOW_MS) {
507
+ if (action === "tree") {
508
+ this.ctx.showTreeSelector();
263
509
  } else {
264
- this.ctx.lastEscapeTime = now;
510
+ this.ctx.showUserMessageSelector();
265
511
  }
512
+ this.#resetEmptyEditorEscape();
513
+ } else {
514
+ this.ctx.lastEscapeTime = now;
266
515
  }
267
516
  }
268
517
  };
269
518
 
519
+ const clear = () => this.handleCtrlC();
270
520
  this.ctx.editor.setActionKeys("app.clear", this.ctx.keybindings.getKeys("app.clear"));
271
- this.ctx.editor.onClear = () => this.handleCtrlC();
521
+ this.ctx.editor.onClear = () => this.#executeAction("app.clear");
522
+ this.#registerCommandPaletteAction("app.clear", clear);
523
+ const exit = () => this.handleCtrlD();
272
524
  this.ctx.editor.setActionKeys("app.exit", this.ctx.keybindings.getKeys("app.exit"));
273
- this.ctx.editor.onExit = () => this.handleCtrlD();
525
+ this.ctx.editor.onExit = () => this.#executeAction("app.exit");
526
+ this.#registerCommandPaletteAction("app.exit", exit);
527
+ const suspend = () => this.handleCtrlZ();
274
528
  this.ctx.editor.setActionKeys("app.suspend", this.ctx.keybindings.getKeys("app.suspend"));
275
- this.ctx.editor.onSuspend = () => this.handleCtrlZ();
529
+ this.ctx.editor.onSuspend = () => this.#executeAction("app.suspend");
530
+ this.#registerCommandPaletteAction("app.suspend", suspend);
531
+ const cycleThinking = () => this.cycleThinkingLevel();
276
532
  this.ctx.editor.setActionKeys("app.thinking.cycle", this.ctx.keybindings.getKeys("app.thinking.cycle"));
277
- this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel();
533
+ this.ctx.editor.onCycleThinkingLevel = () => this.#executeAction("app.thinking.cycle");
534
+ this.#registerCommandPaletteAction("app.thinking.cycle", cycleThinking);
278
535
  this.ctx.editor.setActionKeys("app.commandPalette.open", this.ctx.keybindings.getKeys("app.commandPalette.open"));
279
- this.ctx.editor.onOpenCommandPalette = () => this.openCommandPalette();
536
+ this.ctx.editor.onOpenCommandPalette = () => this.#executeAction("app.commandPalette.open");
537
+ const cycleModelForward = () => this.cycleRoleModel();
280
538
  this.ctx.editor.setActionKeys("app.model.cycleForward", this.ctx.keybindings.getKeys("app.model.cycleForward"));
281
- this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
539
+ this.ctx.editor.onCycleModelForward = () => this.#executeAction("app.model.cycleForward");
540
+ this.#registerCommandPaletteAction("app.model.cycleForward", cycleModelForward);
541
+ const cycleModelBackward = () => this.cycleRoleModel({ temporary: true });
282
542
  this.ctx.editor.setActionKeys("app.model.cycleBackward", this.ctx.keybindings.getKeys("app.model.cycleBackward"));
283
- this.ctx.editor.onCycleModelBackward = () => this.cycleRoleModel({ temporary: true });
543
+ this.ctx.editor.onCycleModelBackward = () => this.#executeAction("app.model.cycleBackward");
544
+ this.#registerCommandPaletteAction("app.model.cycleBackward", cycleModelBackward);
545
+ const selectTemporaryModel = () => this.ctx.showModelSelector({ temporaryOnly: true });
284
546
  this.ctx.editor.setActionKeys(
285
547
  "app.model.selectTemporary",
286
548
  this.ctx.keybindings.getKeys("app.model.selectTemporary"),
287
549
  );
288
- this.ctx.editor.onSelectModelTemporary = () => this.ctx.showModelSelector({ temporaryOnly: true });
550
+ this.ctx.editor.onSelectModelTemporary = () => this.#executeAction("app.model.selectTemporary");
551
+ this.#registerCommandPaletteAction("app.model.selectTemporary", selectTemporaryModel);
289
552
 
290
553
  // Global debug handler on TUI (works regardless of focus)
291
554
  this.ctx.ui.onDebug = () => this.ctx.showDebugSelector();
555
+ const selectModel = () => this.ctx.showModelSelector();
292
556
  this.ctx.editor.setActionKeys("app.model.select", this.ctx.keybindings.getKeys("app.model.select"));
293
- this.ctx.editor.onSelectModel = () => this.ctx.showModelSelector();
557
+ this.ctx.editor.onSelectModel = () => this.#executeAction("app.model.select");
558
+ this.#registerCommandPaletteAction("app.model.select", selectModel);
559
+ const searchHistory = () => this.ctx.showHistorySearch();
294
560
  this.ctx.editor.setActionKeys("app.history.search", this.ctx.keybindings.getKeys("app.history.search"));
295
- this.ctx.editor.onHistorySearch = () => this.ctx.showHistorySearch();
561
+ this.ctx.editor.onHistorySearch = () => this.#executeAction("app.history.search");
562
+ this.#registerCommandPaletteAction("app.history.search", searchHistory);
563
+ const toggleThinking = () => this.ctx.toggleThinkingBlockVisibility();
296
564
  this.ctx.editor.setActionKeys("app.thinking.toggle", this.ctx.keybindings.getKeys("app.thinking.toggle"));
297
- this.ctx.editor.onToggleThinking = () => this.ctx.toggleThinkingBlockVisibility();
565
+ this.ctx.editor.onToggleThinking = () => this.#executeAction("app.thinking.toggle");
566
+ this.#registerCommandPaletteAction("app.thinking.toggle", toggleThinking);
567
+ const externalEditor = () => this.openExternalEditor();
298
568
  this.ctx.editor.setActionKeys("app.editor.external", this.ctx.keybindings.getKeys("app.editor.external"));
299
- this.ctx.editor.onExternalEditor = () => void this.openExternalEditor();
569
+ this.ctx.editor.onExternalEditor = () => this.#executeAction("app.editor.external");
570
+ this.#registerCommandPaletteAction("app.editor.external", externalEditor);
300
571
  this.ctx.editor.onShowHotkeys = () => this.ctx.handleHotkeysCommand();
572
+ const pasteImageFromPalette = async () => {
573
+ await this.handleImagePaste();
574
+ };
301
575
  this.ctx.editor.setActionKeys(
302
576
  "app.clipboard.pasteImage",
303
577
  this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
304
578
  );
305
- this.ctx.editor.onPasteImage = () => this.handleImagePaste();
579
+ this.ctx.editor.onPasteImage = () => this.actionRegistry.execute("app.clipboard.pasteImage");
580
+ this.#registerCommandPaletteAction("app.clipboard.pasteImage", pasteImageFromPalette);
581
+ const copyPrompt = () => this.handleCopyPrompt();
306
582
  this.ctx.editor.setActionKeys(
307
583
  "app.clipboard.copyPrompt",
308
584
  this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
309
585
  );
310
- this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
586
+ this.ctx.editor.onCopyPrompt = () => this.#executeAction("app.clipboard.copyPrompt");
587
+ this.#registerCommandPaletteAction("app.clipboard.copyPrompt", copyPrompt);
311
588
  this.ctx.editor.onPasteText = text => this.handleTextPaste(text);
312
589
  this.ctx.editor.onPastePendingInputCleared = (reason, droppedInputCount) => {
313
590
  const reasonText = reason === "timeout" ? "timed out" : "exceeded the input queue limit";
@@ -315,12 +592,18 @@ export class InputController {
315
592
  `Paste handling ${reasonText}; discarded ${droppedInputCount} buffered input event${droppedInputCount === 1 ? "" : "s"}.`,
316
593
  );
317
594
  };
595
+ const expandTools = () => this.toggleToolOutputExpansion();
318
596
  this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
319
- this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
597
+ this.ctx.editor.onExpandTools = () => this.#executeAction("app.tools.expand");
598
+ this.#registerCommandPaletteAction("app.tools.expand", expandTools);
599
+ const dequeue = () => this.handleDequeue();
320
600
  this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
321
- this.ctx.editor.onDequeue = () => this.handleDequeue();
601
+ this.ctx.editor.onDequeue = () => this.#executeAction("app.message.dequeue");
602
+ this.#registerCommandPaletteAction("app.message.dequeue", dequeue);
603
+ const queue = () => this.handleQueueSubmit();
322
604
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
323
- this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
605
+ this.ctx.editor.onQueue = () => this.#executeAction("app.message.queue");
606
+ this.#registerCommandPaletteAction("app.message.queue", queue);
324
607
 
325
608
  this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
326
609
  this.ctx.editor.onViewportFollowLive = () => {
@@ -333,64 +616,106 @@ export class InputController {
333
616
 
334
617
  for (const key of this.ctx.keybindings.getKeys("app.irc.sidebar.toggle")) {
335
618
  this.ctx.editor.setCustomKeyHandler(key, () => {
336
- this.ctx.toggleIrcSidebar();
619
+ this.#executeAction("app.irc.sidebar.toggle");
337
620
  return true;
338
621
  });
339
622
  }
340
623
 
341
- const planModeKeys = this.ctx.keybindings.getKeys("app.plan.toggle");
342
- for (const key of planModeKeys) {
343
- this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handlePlanModeCommand());
624
+ const togglePlanMode = () => this.ctx.planModeController.handleCommand();
625
+ this.#registerCommandPaletteAction("app.plan.toggle", togglePlanMode);
626
+ for (const key of this.ctx.keybindings.getKeys("app.plan.toggle")) {
627
+ this.ctx.editor.setCustomKeyHandler(key, () => {
628
+ this.#executeAction("app.plan.toggle");
629
+ return true;
630
+ });
344
631
  }
345
-
632
+ const newSession = async () => {
633
+ await this.ctx.handleClearCommand();
634
+ };
635
+ this.#registerCommandPaletteAction("app.session.new", newSession);
346
636
  for (const key of this.ctx.keybindings.getKeys("app.session.new")) {
347
- this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleClearCommand());
637
+ this.ctx.editor.setCustomKeyHandler(key, () => {
638
+ this.#executeAction("app.session.new");
639
+ return true;
640
+ });
348
641
  }
642
+ const showTree = () => {
643
+ this.ctx.showTreeSelector();
644
+ return undefined;
645
+ };
646
+ this.#registerCommandPaletteAction("app.session.tree", showTree);
349
647
  for (const key of this.ctx.keybindings.getKeys("app.session.tree")) {
350
648
  this.ctx.editor.setCustomKeyHandler(key, () => {
351
- this.ctx.showTreeSelector();
649
+ this.#executeAction("app.session.tree");
650
+ return true;
352
651
  });
353
652
  }
653
+ const forkSession = () => {
654
+ this.ctx.showUserMessageSelector();
655
+ return undefined;
656
+ };
657
+ this.#registerCommandPaletteAction("app.session.fork", forkSession);
354
658
  for (const key of this.ctx.keybindings.getKeys("app.session.fork")) {
355
659
  this.ctx.editor.setCustomKeyHandler(key, () => {
356
- this.ctx.showUserMessageSelector();
660
+ this.#executeAction("app.session.fork");
661
+ return true;
357
662
  });
358
663
  }
664
+ const resumeSession = () => {
665
+ this.ctx.showSessionSelector();
666
+ return undefined;
667
+ };
668
+ this.#registerCommandPaletteAction("app.session.resume", resumeSession);
359
669
  for (const key of this.ctx.keybindings.getKeys("app.session.resume")) {
360
670
  this.ctx.editor.setCustomKeyHandler(key, () => {
361
- this.ctx.showSessionSelector();
671
+ this.#executeAction("app.session.resume");
672
+ return true;
362
673
  });
363
674
  }
364
675
  for (const key of this.ctx.keybindings.getKeys("app.message.followUp")) {
365
676
  this.ctx.editor.setCustomKeyHandler(key, () => {
366
- if (!this.#isFollowUpShortcutActive()) return false;
367
- void this.handleFollowUp();
677
+ if (!this.actionRegistry.isAvailable("app.message.followUp")) return false;
678
+ this.#executeAction("app.message.followUp");
368
679
  return true;
369
680
  });
370
681
  }
371
682
  for (const key of this.ctx.keybindings.getKeys("app.stt.toggle")) {
372
- this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
683
+ this.ctx.editor.setCustomKeyHandler(key, () => {
684
+ this.#executeAction("app.stt.toggle");
685
+ return true;
686
+ });
373
687
  }
374
688
  for (const key of this.ctx.keybindings.getKeys("app.clipboard.copyLine")) {
375
689
  this.ctx.editor.setCustomKeyHandler(key, () => {
376
- this.handleCopyCurrentLine();
690
+ this.#executeAction("app.clipboard.copyLine");
377
691
  });
378
692
  }
379
693
  for (const key of this.ctx.keybindings.getKeys("app.session.observe")) {
380
694
  this.ctx.editor.setCustomKeyHandler(key, () => {
381
- this.ctx.showSessionObserver();
695
+ this.#executeAction("app.session.observe");
382
696
  });
383
697
  }
384
698
  for (const key of this.ctx.keybindings.getKeys("app.jobs.open")) {
385
699
  this.ctx.editor.setCustomKeyHandler(key, () => {
386
- this.ctx.showJobsOverlay();
700
+ this.#executeAction("app.jobs.open");
701
+ });
702
+ }
703
+ for (const key of this.ctx.keybindings.getKeys("app.tasks.toggle")) {
704
+ this.ctx.editor.setCustomKeyHandler(key, () => {
705
+ this.#executeAction("app.tasks.toggle");
706
+ return true;
387
707
  });
388
708
  }
389
709
  for (const key of this.ctx.keybindings.getKeys("app.tool.backgroundFold")) {
390
- this.ctx.editor.setCustomKeyHandler(key, () => this.handleForegroundToolBackgroundFold());
710
+ this.ctx.editor.setCustomKeyHandler(key, () => {
711
+ if (!this.actionRegistry.isAvailable("app.tool.backgroundFold")) return false;
712
+ this.#executeAction("app.tool.backgroundFold");
713
+ return true;
714
+ });
391
715
  }
392
716
 
393
717
  this.ctx.editor.onChange = (text: string) => {
718
+ this.#resetEscapeGestures();
394
719
  const wasBashMode = this.ctx.isBashMode;
395
720
  const wasBashNoContext = this.ctx.isBashNoContext;
396
721
  const wasPythonMode = this.ctx.isPythonMode;
@@ -410,188 +735,205 @@ export class InputController {
410
735
  }
411
736
 
412
737
  setupEditorSubmitHandler(): void {
413
- this.ctx.editor.onSubmit = async (text: string) => {
414
- text = text.trim();
415
- if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
416
-
417
- // Empty submit while streaming with queued messages: flush queues immediately
418
- if (!text && this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount > 0) {
419
- // Abort current stream and let queued messages be processed
420
- await this.#abortInteractive();
421
- return;
422
- }
738
+ this.ctx.editor.onSubmit = text => this.submitText(text, { ownsComposer: true, editor: this.ctx.editor });
739
+ }
423
740
 
424
- if (!text) return;
741
+ async submitText(text: string, composer: ComposerSubmissionOptions): Promise<void> {
742
+ text = text.trim();
743
+ if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
425
744
 
426
- // Continue shortcuts: "." or "c" sends empty message (agent continues, no visible message)
427
- if (text === "." || text === "c") {
428
- if (this.ctx.onInputCallback) {
429
- this.ctx.editor.setText("");
430
- this.ctx.pendingImages = [];
431
- this.ctx.onInputCallback({ text: "", cancelled: false, started: true });
432
- }
433
- return;
434
- }
745
+ // Empty submit while streaming with queued messages: flush queues immediately
746
+ if (!text && this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount > 0) {
747
+ // Abort current stream and let queued messages be processed
748
+ await this.#abortInteractive();
749
+ return;
750
+ }
435
751
 
436
- const runner = this.ctx.session.extensionRunner;
437
- const pendingImages = this.ctx.pendingImages;
438
- let inputImages = this.#visiblePendingImagesForText(text);
752
+ if (!text) return;
439
753
 
440
- if (runner?.hasHandlers("input")) {
441
- const result = await runner.emitInput(text, inputImages, "interactive");
442
- if (result?.handled) {
443
- this.ctx.editor.setText("");
444
- this.#clearPendingImagesIfOwnedBy(pendingImages);
445
- return;
446
- }
447
- if (result?.text !== undefined) {
448
- text = result.text.trim();
449
- }
450
- if (result?.images !== undefined) {
451
- inputImages = result.images;
452
- }
754
+ // Continue shortcuts: "." or "c" sends empty message (agent continues, no visible message)
755
+ if (text === "." || text === "c") {
756
+ if (this.ctx.onInputCallback) {
757
+ this.ctx.editor.setText("");
758
+ this.ctx.pendingImages = [];
759
+ this.ctx.onInputCallback({ text: "", cancelled: false, started: true });
453
760
  }
761
+ return;
762
+ }
454
763
 
455
- if (!text) return;
764
+ const runner = this.ctx.session.extensionRunner;
765
+ const pendingImages = this.ctx.pendingImages;
766
+ let inputImages = this.#visiblePendingImagesForText(text);
456
767
 
457
- // Handle built-in slash commands
458
- const slashResult = await executeBuiltinSlashCommand(text, {
459
- ctx: this.ctx,
460
- handleBackgroundCommand: () => this.handleBackgroundCommand(),
461
- });
462
- if (slashResult === true) {
768
+ if (runner?.hasHandlers("input")) {
769
+ const result = await runner.emitInput(text, inputImages, "interactive");
770
+ if (result?.handled) {
771
+ if (this.#canModifyComposer(composer)) {
772
+ this.ctx.editor.setText("");
773
+ }
774
+ this.#clearPendingImagesIfOwnedBy(pendingImages, composer);
463
775
  return;
464
776
  }
465
- if (typeof slashResult === "string") {
466
- // Command handled but returned remaining text to use as prompt
467
- text = slashResult;
777
+ if (result?.text !== undefined) {
778
+ text = result.text.trim();
468
779
  }
469
-
470
- // Handle skill commands (/skill:name [args]). While streaming, Enter
471
- // honors `busyPromptMode`: "steer" interrupts the active turn, "queue"
472
- // runs after it completes (matches the free-text Enter semantics applied
473
- // a few lines below at the streaming branch). Explicit queue shortcuts
474
- // route through `handleFollowUp` and dispatch as `followUp`.
475
- if (await this.#invokeSkillCommand(text, this.#busyStreamingBehavior())) {
476
- return;
780
+ if (result?.images !== undefined) {
781
+ inputImages = result.images;
477
782
  }
783
+ }
478
784
 
479
- // Handle bash command (! for normal, !! for excluded from context)
480
- if (text.startsWith("!")) {
481
- const isExcluded = text.startsWith("!!");
482
- const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
483
- if (command) {
484
- if (this.ctx.session.isBashRunning) {
485
- this.ctx.showWarning("A bash command is already running. Press Esc to cancel it first.");
785
+ if (!text) return;
786
+
787
+ // Handle built-in slash commands
788
+ const slashResult = await executeBuiltinSlashCommand(text, {
789
+ ctx: this.ctx,
790
+ handleBackgroundCommand: () => this.handleBackgroundCommand(),
791
+ composer,
792
+ });
793
+ if (slashResult === true) {
794
+ return;
795
+ }
796
+ if (typeof slashResult === "string") {
797
+ // Command handled but returned remaining text to use as prompt
798
+ text = slashResult;
799
+ }
800
+
801
+ // Handle skill commands (/skill:name [args]). While streaming, Enter
802
+ // honors `busyPromptMode`: "steer" interrupts the active turn, "queue"
803
+ // runs after it completes (matches the free-text Enter semantics applied
804
+ // a few lines below at the streaming branch). Explicit queue shortcuts
805
+ // route through `handleFollowUp` and dispatch as `followUp`.
806
+ if (await this.#invokeSkillCommand(text, this.#busyStreamingBehavior(), composer)) {
807
+ return;
808
+ }
809
+
810
+ // Handle bash command (! for normal, !! for excluded from context)
811
+ if (text.startsWith("!")) {
812
+ const isExcluded = text.startsWith("!!");
813
+ const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
814
+ if (command) {
815
+ if (this.ctx.session.isBashRunning) {
816
+ this.ctx.showWarning("A bash command is already running. Press Esc to cancel it first.");
817
+ if (this.#canModifyComposer(composer)) {
486
818
  this.ctx.editor.setText(text);
487
- return;
488
819
  }
489
- this.ctx.editor.addToHistory(text);
490
- await this.ctx.handleBashCommand(command, isExcluded);
491
- this.ctx.isBashMode = false;
492
- this.ctx.isBashNoContext = false;
493
- this.ctx.updateEditorBorderColor();
494
820
  return;
495
821
  }
822
+ if (this.#canModifyComposer(composer)) {
823
+ this.ctx.editor.addToHistory(text);
824
+ }
825
+ await this.ctx.handleBashCommand(command, isExcluded);
826
+ this.ctx.isBashMode = false;
827
+ this.ctx.isBashNoContext = false;
828
+ this.ctx.updateEditorBorderColor();
829
+ return;
496
830
  }
831
+ }
497
832
 
498
- // Handle python command ($ for normal, $$ for excluded from context)
499
- if (text.startsWith("$")) {
500
- const isExcluded = text.startsWith("$$");
501
- const code = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
502
- if (code) {
503
- if (this.ctx.session.isEvalRunning) {
504
- this.ctx.showWarning("A Python execution is already running. Press Esc to cancel it first.");
833
+ // Handle python command ($ for normal, $$ for excluded from context)
834
+ if (text.startsWith("$")) {
835
+ const isExcluded = text.startsWith("$$");
836
+ const code = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
837
+ if (code) {
838
+ if (this.ctx.session.isEvalRunning) {
839
+ this.ctx.showWarning("A Python execution is already running. Press Esc to cancel it first.");
840
+ if (this.#canModifyComposer(composer)) {
505
841
  this.ctx.editor.setText(text);
506
- return;
507
842
  }
508
- this.ctx.editor.addToHistory(text);
509
- await this.ctx.handlePythonCommand(code, isExcluded);
510
- this.ctx.isPythonMode = false;
511
- this.ctx.updateEditorBorderColor();
512
843
  return;
513
844
  }
845
+ if (this.#canModifyComposer(composer)) {
846
+ this.ctx.editor.addToHistory(text);
847
+ }
848
+ await this.ctx.handlePythonCommand(code, isExcluded);
849
+ this.ctx.isPythonMode = false;
850
+ this.ctx.updateEditorBorderColor();
851
+ return;
514
852
  }
853
+ }
515
854
 
516
- // Queue input during compaction
517
- if (this.ctx.session.isCompacting) {
518
- if ((inputImages?.length ?? 0) > 0) {
519
- this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
520
- return;
521
- }
522
- this.ctx.queueCompactionMessage(text, "steer");
855
+ // Queue input during compaction
856
+ if (this.ctx.session.isCompacting) {
857
+ if ((inputImages?.length ?? 0) > 0) {
858
+ this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
523
859
  return;
524
860
  }
861
+ this.ctx.queueCompactionMessage(text, "steer", composer);
862
+ return;
863
+ }
525
864
 
526
- // If streaming, use prompt() with the busy-prompt behavior the user
527
- // selected: "steer" interrupts the active turn, "queue" defers the
528
- // prompt to run after the active turn completes (in submission order).
529
- // This handles extension commands (execute immediately), prompt template expansion, and queueing
530
- if (this.ctx.session.isStreaming) {
865
+ // If streaming, use prompt() with the busy-prompt behavior the user
866
+ // selected: "steer" interrupts the active turn, "queue" defers the
867
+ // prompt to run after the active turn completes (in submission order).
868
+ // This handles extension commands (execute immediately), prompt template expansion, and queueing
869
+ if (this.ctx.session.isStreaming) {
870
+ if (this.#canModifyComposer(composer)) {
531
871
  this.ctx.editor.addToHistory(text);
532
872
  this.ctx.editor.setText("");
533
- const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
534
- this.#clearPendingImagesIfOwnedBy(pendingImages);
535
- // Record the signature so the queued message's eventual delivery
536
- // (a user-role `message_start` event) leaves any draft the user has
537
- // typed since queuing intact. Same protection as #783, applied to
538
- // the streaming/queue path.
539
- const streamingBehavior = this.#busyStreamingBehavior();
540
- const promptOptions =
541
- streamingBehavior === "followUp"
542
- ? { streamingBehavior, images, followUpQueuePolicy: "sequential" as const }
543
- : { streamingBehavior, images };
544
- await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, promptOptions), {
545
- imageCount: images?.length ?? 0,
546
- });
547
- this.ctx.updatePendingMessagesDisplay();
548
- this.ctx.ui.requestRender();
549
- return;
550
873
  }
874
+ const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
875
+ this.#clearPendingImagesIfOwnedBy(pendingImages, composer);
876
+ // Record the signature so the queued message's eventual delivery
877
+ // (a user-role `message_start` event) leaves any draft the user has
878
+ // typed since queuing intact. Same protection as #783, applied to
879
+ // the streaming/queue path.
880
+ const streamingBehavior = this.#busyStreamingBehavior();
881
+ const promptOptions =
882
+ streamingBehavior === "followUp"
883
+ ? { streamingBehavior, images, followUpQueuePolicy: "sequential" as const }
884
+ : { streamingBehavior, images };
885
+ await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, promptOptions), {
886
+ imageCount: images?.length ?? 0,
887
+ });
888
+ this.ctx.updatePendingMessagesDisplay();
889
+ this.ctx.ui.requestRender();
890
+ return;
891
+ }
551
892
 
552
- // Normal message submission
553
- // First, move any pending bash components to chat
554
- this.ctx.flushPendingBashComponents();
555
-
556
- // Generate session title on first message
557
- const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
558
- if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE) {
559
- const registry = this.ctx.session.modelRegistry;
560
- generateSessionTitle(
561
- text,
562
- registry,
563
- this.ctx.settings,
564
- this.ctx.session.sessionId,
565
- this.ctx.session.model,
566
- provider => this.ctx.session.agent.metadataForProvider(provider),
567
- )
568
- .then(async title => {
569
- if (title) {
570
- const applied = await this.ctx.sessionManager.setSessionName(title, "auto");
571
- if (applied) {
572
- setSessionTerminalTitle(
573
- this.ctx.sessionManager.getSessionName()!,
574
- this.ctx.sessionManager.getCwd(),
575
- );
576
- this.ctx.updateEditorBorderColor();
577
- }
893
+ // Normal message submission
894
+ // First, move any pending bash components to chat
895
+ this.ctx.flushPendingBashComponents();
896
+
897
+ // Generate session title on first message
898
+ const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
899
+ if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE) {
900
+ const registry = this.ctx.session.modelRegistry;
901
+ generateSessionTitle(
902
+ text,
903
+ registry,
904
+ this.ctx.settings,
905
+ this.ctx.session.sessionId,
906
+ this.ctx.session.model,
907
+ provider => this.ctx.session.agent.metadataForProvider(provider),
908
+ )
909
+ .then(async title => {
910
+ if (title) {
911
+ const applied = await this.ctx.sessionManager.setSessionName(title, "auto");
912
+ if (applied) {
913
+ setSessionTerminalTitle(
914
+ this.ctx.sessionManager.getSessionName()!,
915
+ this.ctx.sessionManager.getCwd(),
916
+ );
917
+ this.ctx.updateEditorBorderColor();
578
918
  }
579
- })
580
- .catch(() => {});
581
- }
919
+ }
920
+ })
921
+ .catch(() => {});
922
+ }
582
923
 
583
- if (this.ctx.onInputCallback) {
584
- // Include any pending images from clipboard paste
585
- const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
586
- this.#clearPendingImagesIfOwnedBy(pendingImages);
924
+ if (this.ctx.onInputCallback) {
925
+ // Include any pending images from clipboard paste
926
+ const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
927
+ this.#clearPendingImagesIfOwnedBy(pendingImages, composer);
587
928
 
588
- // Render user message immediately, then let session events catch up
589
- const submission = this.ctx.startPendingSubmission({ text, images });
929
+ // Render user message immediately, then let session events catch up
930
+ const submission = this.ctx.startPendingSubmission({ text, images }, composer);
590
931
 
591
- this.ctx.onInputCallback(submission);
592
- }
932
+ this.ctx.onInputCallback(submission);
933
+ }
934
+ if (this.#canModifyComposer(composer)) {
593
935
  this.ctx.editor.addToHistory(text);
594
- };
936
+ }
595
937
  }
596
938
 
597
939
  handleCtrlC(): void {
@@ -625,6 +967,106 @@ export class InputController {
625
967
  process.kill(0, "SIGTSTP");
626
968
  }
627
969
 
970
+ #queuePaneOverlay: ReturnType<typeof this.ctx.ui.showOverlay> | undefined;
971
+
972
+ toggleQueuePane(): void {
973
+ if (this.#queuePaneOverlay) {
974
+ this.#queuePaneOverlay.hide();
975
+ this.#queuePaneOverlay = undefined;
976
+ this.ctx.ui.setFocus(this.ctx.editor);
977
+ this.ctx.ui.requestRender(true);
978
+ return;
979
+ }
980
+ this.#showQueuePane();
981
+ }
982
+
983
+ #showQueuePane(selectedIndex = 0): void {
984
+ const entries = this.ctx.session.getQueuedMessageEntries();
985
+ if (entries.length === 0) {
986
+ this.ctx.showStatus("No queued messages");
987
+ return;
988
+ }
989
+ const close = () => {
990
+ this.#queuePaneOverlay?.hide();
991
+ this.#queuePaneOverlay = undefined;
992
+ this.ctx.ui.setFocus(this.ctx.editor);
993
+ this.ctx.ui.requestRender(true);
994
+ };
995
+ const refresh = (nextIndex: number) => {
996
+ this.#queuePaneOverlay?.hide();
997
+ this.#queuePaneOverlay = undefined;
998
+ this.#showQueuePane(nextIndex);
999
+ };
1000
+ const pane = new QueuePaneComponent(entries, {
1001
+ selectedIndex,
1002
+ onDelete: (entry, index) => {
1003
+ const deleted = this.ctx.session.removeQueuedMessageForEditing(entry.id) !== undefined;
1004
+ const remaining = this.ctx.session.getQueuedMessageEntries();
1005
+ this.ctx.updatePendingMessagesDisplay();
1006
+ if (remaining.length === 0) {
1007
+ close();
1008
+ this.ctx.showStatus(deleted ? "Deleted queued message" : "Queued message is no longer available");
1009
+ return;
1010
+ }
1011
+ this.ctx.showStatus(deleted ? "Deleted queued message" : "Queued message is no longer available");
1012
+ refresh(Math.min(index, remaining.length - 1));
1013
+ },
1014
+ onMove: (entry, index, direction) => {
1015
+ const moved = this.ctx.session.moveQueuedMessageForEditing(entry.id, direction);
1016
+ this.ctx.updatePendingMessagesDisplay();
1017
+ this.ctx.showStatus(moved ? "Moved queued message" : "Queued message cannot move further");
1018
+ refresh(Math.max(0, Math.min(index + (direction === "up" ? -1 : 1), entries.length - 1)));
1019
+ },
1020
+ onClose: close,
1021
+ });
1022
+ this.#queuePaneOverlay = this.ctx.ui.showOverlay(pane, {
1023
+ anchor: "bottom-center",
1024
+ width: "100%",
1025
+ maxHeight: "100%",
1026
+ margin: 0,
1027
+ });
1028
+ this.ctx.ui.setFocus(pane);
1029
+ this.ctx.ui.requestRender();
1030
+ }
1031
+
1032
+ async sendNow(): Promise<void> {
1033
+ const composerText = this.ctx.editor.getText().trim();
1034
+ let text = composerText;
1035
+ let queuedEntryId: string | undefined;
1036
+ if (!text) {
1037
+ if (this.ctx.session.isCompacting) {
1038
+ this.ctx.showWarning("Cannot send immediately while compaction is in progress");
1039
+ return;
1040
+ }
1041
+ const entry = this.ctx.session.getQueuedMessageEntries()[0];
1042
+ if (!entry) {
1043
+ this.ctx.showStatus("No visible queued message to send");
1044
+ return;
1045
+ }
1046
+ text = entry.text;
1047
+ queuedEntryId = entry.id;
1048
+ }
1049
+ const outcome = await this.ctx.session.cancelAndSubmit(text, { queuedEntryId });
1050
+ if (outcome.kind === "submitted") {
1051
+ if (composerText) this.ctx.clearEditor();
1052
+ this.ctx.updatePendingMessagesDisplay();
1053
+ return;
1054
+ }
1055
+ if (outcome.kind === "rolled_back") {
1056
+ this.ctx.showWarning(
1057
+ outcome.outcome.kind === "timeout"
1058
+ ? "Send was cancelled after forced recovery; queued messages were restored"
1059
+ : "Send failed; queued messages were restored",
1060
+ );
1061
+ return;
1062
+ }
1063
+ if (outcome.reason === "compaction") {
1064
+ this.ctx.showWarning("Cannot send immediately while compaction is in progress");
1065
+ } else {
1066
+ this.ctx.showStatus("Send already in progress");
1067
+ }
1068
+ }
1069
+
628
1070
  handleDequeue(): void {
629
1071
  const entries = this.#getEditableQueuedMessages();
630
1072
  if (entries.length === 0) {
@@ -815,15 +1257,6 @@ export class InputController {
815
1257
  return this.ctx.settings.get("busyPromptMode") === "steer" ? "steer" : "followUp";
816
1258
  }
817
1259
 
818
- #isFollowUpShortcutActive(): boolean {
819
- return (
820
- this.ctx.session.isStreaming ||
821
- this.ctx.session.isCompacting ||
822
- this.ctx.session.isBashRunning ||
823
- this.ctx.session.isEvalRunning
824
- );
825
- }
826
-
827
1260
  /**
828
1261
  * Dispatch skill slash invocation(s) (`/skill:<name>`) through custom messages
829
1262
  * using the supplied `streamingBehavior`. Returns true if the text contains a
@@ -837,11 +1270,17 @@ export class InputController {
837
1270
  * while the agent is streaming; the idle path of `promptCustomMessage`
838
1271
  * ignores it.
839
1272
  */
840
- async #invokeSkillCommand(text: string, streamingBehavior: "steer" | "followUp"): Promise<boolean> {
1273
+ async #invokeSkillCommand(
1274
+ text: string,
1275
+ streamingBehavior: "steer" | "followUp",
1276
+ options?: ComposerSubmissionOptions,
1277
+ ): Promise<boolean> {
841
1278
  const invocations = parseSkillInvocations(text, this.ctx.skillCommands ?? new Map());
842
1279
  if (invocations.length === 0) return false;
843
- this.ctx.editor.addToHistory(text);
844
- this.ctx.editor.setText("");
1280
+ if (!options || this.#canModifyComposer(options)) {
1281
+ this.ctx.editor.addToHistory(text);
1282
+ this.ctx.editor.setText("");
1283
+ }
845
1284
  try {
846
1285
  for (let index = 0; index < invocations.length; index += 1) {
847
1286
  const invocation = invocations[index];
@@ -1065,7 +1504,7 @@ export class InputController {
1065
1504
  }
1066
1505
 
1067
1506
  const now = Date.now();
1068
- if (now - this.#lastBackgroundFoldKeyTime > 750) {
1507
+ if (now - this.#lastBackgroundFoldKeyTime > BACKGROUND_FOLD_DOUBLE_PRESS_MS) {
1069
1508
  this.#lastBackgroundFoldKeyTime = now;
1070
1509
  this.ctx.showStatus("Press Ctrl+B again to fold supported foreground bash into a background job");
1071
1510
  return true;
@@ -1155,8 +1594,15 @@ export class InputController {
1155
1594
  return images.length > 0 ? images : undefined;
1156
1595
  }
1157
1596
 
1158
- #clearPendingImagesIfOwnedBy(pendingImages: InteractiveModeContext["pendingImages"]): void {
1159
- if (this.ctx.pendingImages === pendingImages) {
1597
+ #canModifyComposer(options: ComposerSubmissionOptions): boolean {
1598
+ return canApplyComposerSubmission(options, this.ctx.editor);
1599
+ }
1600
+
1601
+ #clearPendingImagesIfOwnedBy(
1602
+ pendingImages: InteractiveModeContext["pendingImages"],
1603
+ options: ComposerSubmissionOptions,
1604
+ ): void {
1605
+ if (this.#canModifyComposer(options) && this.ctx.pendingImages === pendingImages) {
1160
1606
  this.ctx.pendingImages = [];
1161
1607
  }
1162
1608
  }
@@ -1235,6 +1681,7 @@ export class InputController {
1235
1681
  }
1236
1682
 
1237
1683
  createAutocompleteProvider(commands: SlashCommand[], basePath: string): AutocompleteProvider {
1684
+ this.#slashCommands = commands;
1238
1685
  return createPromptActionAutocompleteProvider({
1239
1686
  commands,
1240
1687
  basePath,
@@ -1288,12 +1735,90 @@ export class InputController {
1288
1735
  }
1289
1736
  }
1290
1737
 
1738
+ async #dispatchPaletteSlashCommand(name: string, restoreComposer: () => void): Promise<void> {
1739
+ if (this.#paletteCommandInFlight) {
1740
+ this.ctx.showStatus("A palette command is still running.");
1741
+ return;
1742
+ }
1743
+
1744
+ if (this.ctx.editor.getText() || this.ctx.pendingImages.length > 0) {
1745
+ this.ctx.showStatus("Send or clear the draft before running a palette command.");
1746
+ return;
1747
+ }
1748
+
1749
+ restoreComposer();
1750
+ this.#paletteCommandInFlight = true;
1751
+ try {
1752
+ await this.submitText(`/${name}`, { ownsComposer: false, editor: this.ctx.editor });
1753
+ } finally {
1754
+ this.#paletteCommandInFlight = false;
1755
+ }
1756
+ }
1757
+
1291
1758
  openCommandPalette(): void {
1292
- if (this.ctx.editor.getText().trim().length > 0) {
1293
- this.ctx.showStatus("Command palette opens from an empty prompt. Type / for inline commands.");
1759
+ if (this.ctx.isTranscriptViewerOpen?.()) return;
1760
+ if (this.#paletteCommandInFlight) {
1761
+ this.ctx.showStatus("A palette command is still running.");
1762
+ return;
1763
+ }
1764
+
1765
+ const actions = [...this.#commandPaletteActions.values()];
1766
+ const slashCommands = [...(this.ctx.getSlashCommands?.() ?? this.#slashCommands)];
1767
+ if (!this.ctx.showCommandPalette) {
1768
+ let overlayHandle: ReturnType<typeof this.ctx.ui.showOverlay> | undefined;
1769
+ const close = () => {
1770
+ overlayHandle?.hide();
1771
+ this.ctx.ui.setFocus(this.ctx.editor);
1772
+ this.ctx.ui.requestRender(true);
1773
+ };
1774
+ const entries: CommandPaletteEntry[] = [
1775
+ ...this.actionRegistry
1776
+ .all()
1777
+ .filter(action => this.actionRegistry.isAvailable(action.id))
1778
+ .map(action => ({
1779
+ id: `action:${action.id}`,
1780
+ label: action.title,
1781
+ description: action.category,
1782
+ keybinding: action.bindingId
1783
+ ? this.ctx.keybindings.getKeys(action.bindingId).join(", ") || undefined
1784
+ : undefined,
1785
+ handler: () => this.actionRegistry.execute(action.id),
1786
+ })),
1787
+ ...slashCommands.map(command => ({
1788
+ id: `slash:/${command.name}`,
1789
+ label: `/${command.name}`,
1790
+ description: command.description,
1791
+ })),
1792
+ ];
1793
+ const palette = new CommandPalette(
1794
+ entries,
1795
+ entry => {
1796
+ if (entry.handler) {
1797
+ close();
1798
+ void entry.handler();
1799
+ } else {
1800
+ const command = entry.id.slice("slash:/".length);
1801
+ void this.#dispatchPaletteSlashCommand(command, close);
1802
+ }
1803
+ },
1804
+ close,
1805
+ );
1806
+ overlayHandle = this.ctx.ui.showOverlay(palette, {
1807
+ anchor: "bottom-center",
1808
+ width: "100%",
1809
+ maxHeight: "100%",
1810
+ margin: 0,
1811
+ });
1812
+ this.ctx.ui.setFocus(palette);
1813
+ this.ctx.ui.requestRender();
1294
1814
  return;
1295
1815
  }
1296
- this.ctx.editor.handleInput("/");
1816
+ this.ctx.showCommandPalette(slashCommands, actions, name =>
1817
+ this.#dispatchPaletteSlashCommand(name, () => {
1818
+ this.ctx.ui.setFocus?.(this.ctx.editor);
1819
+ this.ctx.ui.requestRender(true);
1820
+ }),
1821
+ );
1297
1822
  }
1298
1823
 
1299
1824
  cycleThinkingLevel(): void {
@@ -1364,7 +1889,17 @@ export class InputController {
1364
1889
  this.ctx.toolOutputExpanded = expanded;
1365
1890
  for (const child of this.ctx.chatContainer.children) {
1366
1891
  if (isExpandable(child)) {
1367
- child.setExpanded(expanded);
1892
+ // Callable guard: setManuallyExpanded is optional on the exported
1893
+ // contract, and hostile/legacy children may expose it as a
1894
+ // non-function property or an unstable getter. Read once, then
1895
+ // invoke only a real function; everything else falls back to the
1896
+ // automatic path.
1897
+ const setManuallyExpanded = child.setManuallyExpanded;
1898
+ if (typeof setManuallyExpanded === "function") {
1899
+ setManuallyExpanded.call(child, expanded);
1900
+ } else {
1901
+ child.setExpanded(expanded);
1902
+ }
1368
1903
  }
1369
1904
  }
1370
1905
  this.ctx.ui.requestRender();