@sayknow-cli/coding-agent 0.3.5 → 0.3.7

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 (238) hide show
  1. package/README.md +76 -0
  2. package/dist/types/capability/mcp.d.ts +2 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/telegram-cli.d.ts +13 -0
  6. package/dist/types/cli/update-cli.d.ts +24 -1
  7. package/dist/types/commands/local-provider.d.ts +34 -0
  8. package/dist/types/commands/telegram.d.ts +22 -0
  9. package/dist/types/config/keybindings.d.ts +12 -6
  10. package/dist/types/config/model-registry.d.ts +5 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +284 -4
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/config/telegram-autostart.d.ts +14 -0
  15. package/dist/types/config/telegram-env-bridge.d.ts +18 -0
  16. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +7 -0
  18. package/dist/types/discovery/cursor.d.ts +4 -1
  19. package/dist/types/discovery/index.d.ts +0 -1
  20. package/dist/types/discovery/windsurf.d.ts +3 -1
  21. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  22. package/dist/types/i18n/messages/en.d.ts +1 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  24. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  25. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  26. package/dist/types/modes/components/model-selector.d.ts +2 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +5 -1
  28. package/dist/types/modes/components/skill-message.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  30. package/dist/types/modes/components/status-line.d.ts +15 -3
  31. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  32. package/dist/types/modes/components/welcome.d.ts +9 -1
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/input-controller.d.ts +6 -1
  35. package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
  38. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  40. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  41. package/dist/types/modes/theme/theme.d.ts +1 -1
  42. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  43. package/dist/types/modes/types.d.ts +5 -1
  44. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  45. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  46. package/dist/types/notifications/html-format.d.ts +2 -0
  47. package/dist/types/notifications/recent-activity.d.ts +5 -1
  48. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  49. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  50. package/dist/types/notifications/telegram-daemon.d.ts +4 -10
  51. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  52. package/dist/types/notifications/threaded-render.d.ts +1 -0
  53. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  54. package/dist/types/runtime-mcp/config.d.ts +2 -0
  55. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  56. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  57. package/dist/types/runtime-mcp/types.d.ts +6 -0
  58. package/dist/types/sdk.d.ts +1 -1
  59. package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
  60. package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
  61. package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
  62. package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
  63. package/dist/types/skc-runtime/state-writer.d.ts +1 -0
  64. package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
  65. package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
  66. package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
  67. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
  68. package/dist/types/slash-commands/types.d.ts +2 -0
  69. package/dist/types/system-prompt.d.ts +7 -0
  70. package/dist/types/tools/skill.d.ts +1 -1
  71. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  72. package/dist/types/web/search/index.d.ts +4 -2
  73. package/dist/types/web/search/provider.d.ts +14 -0
  74. package/dist/types/web/search/providers/utils.d.ts +42 -13
  75. package/dist/types/web/search/render.d.ts +1 -0
  76. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  77. package/package.json +8 -8
  78. package/scripts/build-binary.ts +3 -0
  79. package/scripts/verify-insane-vendor.ts +0 -5
  80. package/src/capability/mcp.ts +2 -0
  81. package/src/cli/local-provider-smoke.ts +632 -0
  82. package/src/cli/notify-cli.ts +4 -2
  83. package/src/cli/telegram-cli.ts +156 -0
  84. package/src/cli/update-cli.ts +179 -13
  85. package/src/cli/web-search-cli.ts +5 -7
  86. package/src/cli.ts +20 -1
  87. package/src/commands/launch.ts +47 -2
  88. package/src/commands/local-provider.ts +62 -0
  89. package/src/commands/notify.ts +1 -1
  90. package/src/commands/ralplan.ts +1 -0
  91. package/src/commands/telegram.ts +47 -0
  92. package/src/config/keybindings.ts +15 -6
  93. package/src/config/mcp-schema.json +4 -0
  94. package/src/config/model-registry.ts +55 -1
  95. package/src/config/models-config-schema.ts +9 -0
  96. package/src/config/settings-schema.ts +256 -4
  97. package/src/config/settings.ts +11 -0
  98. package/src/config/telegram-autostart.ts +107 -0
  99. package/src/config/telegram-env-bridge.ts +92 -0
  100. package/src/coordinator-mcp/server.ts +276 -34
  101. package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
  103. package/src/defaults/skc-defaults.ts +16 -0
  104. package/src/discovery/builtin.ts +12 -0
  105. package/src/discovery/cursor.ts +5 -84
  106. package/src/discovery/gemini.ts +3 -89
  107. package/src/discovery/index.ts +4 -1
  108. package/src/discovery/mcp-json.ts +11 -0
  109. package/src/discovery/opencode.ts +5 -122
  110. package/src/discovery/windsurf.ts +4 -82
  111. package/src/edit/read-file.ts +9 -1
  112. package/src/extensibility/extensions/runner.ts +2 -1
  113. package/src/extensibility/extensions/types.ts +11 -0
  114. package/src/extensibility/skills.ts +30 -2
  115. package/src/hooks/skill-state.ts +33 -73
  116. package/src/i18n/messages/de.ts +1 -0
  117. package/src/i18n/messages/en.ts +1 -0
  118. package/src/i18n/messages/es.ts +1 -0
  119. package/src/i18n/messages/fr.ts +1 -0
  120. package/src/i18n/messages/ja.settings.ts +28 -0
  121. package/src/i18n/messages/ja.ts +1 -0
  122. package/src/i18n/messages/ko.settings.ts +28 -0
  123. package/src/i18n/messages/ko.ts +1 -0
  124. package/src/i18n/messages/zh.settings.ts +28 -0
  125. package/src/i18n/messages/zh.ts +1 -0
  126. package/src/internal-urls/docs-index.generated.ts +12 -7
  127. package/src/main.ts +5 -2
  128. package/src/migrate/executor.ts +23 -6
  129. package/src/modes/acp/acp-agent.ts +7 -6
  130. package/src/modes/components/agent-dashboard.ts +3 -35
  131. package/src/modes/components/custom-editor.ts +26 -0
  132. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  133. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  134. package/src/modes/components/hook-selector.ts +21 -3
  135. package/src/modes/components/model-selector.ts +246 -11
  136. package/src/modes/components/session-observer-overlay.ts +3 -3
  137. package/src/modes/components/session-selector.ts +19 -9
  138. package/src/modes/components/settings-selector.ts +551 -11
  139. package/src/modes/components/skill-message.ts +18 -10
  140. package/src/modes/components/status-line/presets.ts +11 -0
  141. package/src/modes/components/status-line/segments.ts +16 -19
  142. package/src/modes/components/status-line/types.ts +6 -2
  143. package/src/modes/components/status-line.ts +281 -117
  144. package/src/modes/components/tree-selector.ts +28 -8
  145. package/src/modes/components/two-column-body.ts +36 -0
  146. package/src/modes/components/welcome.ts +165 -30
  147. package/src/modes/controllers/command-controller.ts +26 -2
  148. package/src/modes/controllers/event-controller.ts +102 -4
  149. package/src/modes/controllers/extension-ui-controller.ts +21 -2
  150. package/src/modes/controllers/input-controller.ts +214 -47
  151. package/src/modes/controllers/selector-controller.ts +35 -33
  152. package/src/modes/interactive-mode.ts +36 -30
  153. package/src/modes/prompt-action-autocomplete.ts +48 -8
  154. package/src/modes/rpc/rpc-types.ts +1 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
  156. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  157. package/src/modes/slash-command-visibility.ts +9 -0
  158. package/src/modes/theme/theme.ts +5 -1
  159. package/src/modes/tmux-scroll.ts +92 -0
  160. package/src/modes/types.ts +8 -1
  161. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  162. package/src/modes/utils/terminal-bell.ts +48 -0
  163. package/src/modes/utils/ui-helpers.ts +76 -3
  164. package/src/notifications/daemon-paths.ts +22 -0
  165. package/src/notifications/html-format.ts +75 -1
  166. package/src/notifications/index.ts +32 -2
  167. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  168. package/src/notifications/recent-activity.ts +32 -8
  169. package/src/notifications/telegram-daemon-cli.ts +125 -13
  170. package/src/notifications/telegram-daemon-control.ts +9 -0
  171. package/src/notifications/telegram-daemon.ts +88 -46
  172. package/src/notifications/telegram-reference.ts +36 -14
  173. package/src/notifications/threaded-render.ts +14 -6
  174. package/src/prompts/agents/architect.md +7 -4
  175. package/src/prompts/agents/critic.md +15 -6
  176. package/src/prompts/agents/planner.md +8 -3
  177. package/src/prompts/system/system-prompt.md +20 -2
  178. package/src/prompts/tools/bash.md +1 -1
  179. package/src/runtime-mcp/config-writer.ts +28 -0
  180. package/src/runtime-mcp/config.ts +7 -0
  181. package/src/runtime-mcp/loader.ts +3 -0
  182. package/src/runtime-mcp/manager.ts +3 -0
  183. package/src/runtime-mcp/types.ts +6 -0
  184. package/src/sdk.ts +10 -11
  185. package/src/session/agent-session.ts +103 -13
  186. package/src/session/session-manager.ts +58 -18
  187. package/src/skc-runtime/launch-tmux.ts +34 -8
  188. package/src/skc-runtime/launch-worktree.ts +16 -2
  189. package/src/skc-runtime/ralplan-runtime.ts +22 -6
  190. package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
  191. package/src/skc-runtime/session-state-sidecar.ts +167 -24
  192. package/src/skc-runtime/state-runtime.ts +8 -5
  193. package/src/skc-runtime/state-writer.ts +5 -5
  194. package/src/skc-runtime/team-runtime.ts +21 -6
  195. package/src/skc-runtime/tmux-common.ts +16 -0
  196. package/src/skc-runtime/tmux-sessions.ts +13 -9
  197. package/src/skc-runtime/ultragoal-guard.ts +150 -24
  198. package/src/skc-runtime/ultragoal-runtime.ts +16 -124
  199. package/src/skc-runtime/workflow-manifest.generated.json +0 -8
  200. package/src/skc-runtime/workflow-manifest.ts +0 -1
  201. package/src/slash-commands/builtin-registry.ts +136 -23
  202. package/src/slash-commands/types.ts +2 -0
  203. package/src/system-prompt.ts +8 -0
  204. package/src/task/executor.ts +5 -0
  205. package/src/tools/ast-grep.ts +10 -7
  206. package/src/tools/bash.ts +39 -14
  207. package/src/tools/index.ts +8 -1
  208. package/src/tools/skill.ts +1 -1
  209. package/src/utils/cmux-workspace.ts +233 -0
  210. package/src/utils/title-generator.ts +2 -0
  211. package/src/web/kagi.ts +1 -1
  212. package/src/web/parallel.ts +2 -2
  213. package/src/web/search/index.ts +137 -52
  214. package/src/web/search/provider.ts +80 -0
  215. package/src/web/search/providers/anthropic.ts +1 -1
  216. package/src/web/search/providers/brave.ts +1 -1
  217. package/src/web/search/providers/codex.ts +1 -1
  218. package/src/web/search/providers/duckduckgo.ts +1 -1
  219. package/src/web/search/providers/exa.ts +1 -1
  220. package/src/web/search/providers/gemini.ts +7 -3
  221. package/src/web/search/providers/jina.ts +1 -1
  222. package/src/web/search/providers/kimi.ts +6 -1
  223. package/src/web/search/providers/openai-compatible.ts +1 -1
  224. package/src/web/search/providers/parallel.ts +1 -1
  225. package/src/web/search/providers/perplexity.ts +2 -2
  226. package/src/web/search/providers/searxng.ts +1 -1
  227. package/src/web/search/providers/synthetic.ts +1 -1
  228. package/src/web/search/providers/tavily.ts +1 -1
  229. package/src/web/search/providers/utils.ts +67 -17
  230. package/src/web/search/providers/xai.ts +1 -1
  231. package/src/web/search/providers/zai.ts +1 -1
  232. package/src/web/search/render.ts +3 -0
  233. package/src/workflow/workflow-intent-diff.ts +199 -0
  234. package/vendor/insane-search/MANIFEST.json +1 -3
  235. package/vendor/insane-search/engine/tests/test_u1.py +0 -28
  236. package/vendor/insane-search/engine/transport.py +2 -3
  237. package/vendor/insane-search/engine/validators.py +0 -14
  238. package/src/defaults/skc/rules/ponytail.md +0 -68
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { type AgentMessage, ThinkingLevel } from "@sayknow-cli/agent-core";
4
- import type { AutocompleteProvider, SlashCommand } from "@sayknow-cli/tui";
4
+ import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@sayknow-cli/tui";
5
5
  import { $env, sanitizeText } from "@sayknow-cli/utils";
6
6
  import { isSettingsInitialized, settings } from "../../config/settings";
7
7
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/skc-plugins";
@@ -9,6 +9,7 @@ import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibil
9
9
  import { expandEmoticons } from "../../modes/emoji-autocomplete";
10
10
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
11
11
  import { theme } from "../../modes/theme/theme";
12
+ import { scrollTmuxToPreviousUserInput as scrollTmuxPaneToPreviousUserInput } from "../../modes/tmux-scroll";
12
13
  import type { InteractiveModeContext } from "../../modes/types";
13
14
  import type { AgentSessionEvent } from "../../session/agent-session";
14
15
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
@@ -41,6 +42,127 @@ export class InputController {
41
42
  * so abort cleanup going idle cannot turn the second Esc into an idle action. */
42
43
  #steerConsumePending = false;
43
44
 
45
+ #globalInterruptUnsubscribe: (() => void) | undefined;
46
+
47
+ #matchesInterruptKey(data: string): boolean {
48
+ return this.ctx.keybindings.getKeys("app.interrupt").some(key => matchesKey(data, key));
49
+ }
50
+
51
+ #hasHookDialog(): boolean {
52
+ return Boolean(this.ctx.hookSelector || this.ctx.hookInput || this.ctx.hookEditor);
53
+ }
54
+
55
+ #isRetryBackoffActive(): boolean {
56
+ return Boolean(
57
+ this.ctx.retryLoader ||
58
+ this.ctx.retryEscapeHandler ||
59
+ (this.ctx.session.isRetrying && !this.ctx.session.isStreaming),
60
+ );
61
+ }
62
+
63
+ #handleCancellableWorkEscape(options: {
64
+ loading?: boolean;
65
+ processes?: boolean;
66
+ modes?: boolean;
67
+ maintenance?: boolean;
68
+ retry?: boolean;
69
+ streaming?: boolean;
70
+ }): boolean {
71
+ if (options.loading && this.ctx.loadingAnimation) {
72
+ if (this.ctx.cancelPendingSubmission()) {
73
+ return true;
74
+ }
75
+ this.restoreQueuedMessagesToEditor({ abort: true });
76
+ return true;
77
+ }
78
+ if (options.processes && this.ctx.session.isBashRunning) {
79
+ this.ctx.session.abortBash();
80
+ return true;
81
+ }
82
+ if (options.modes && this.ctx.isBashMode) {
83
+ this.ctx.editor.setText("");
84
+ this.ctx.isBashMode = false;
85
+ this.ctx.isBashNoContext = false;
86
+ this.ctx.updateEditorBorderColor();
87
+ return true;
88
+ }
89
+ if (options.processes && this.ctx.session.isEvalRunning) {
90
+ this.ctx.session.abortEval();
91
+ return true;
92
+ }
93
+ if (options.modes && this.ctx.isPythonMode) {
94
+ this.ctx.editor.setText("");
95
+ this.ctx.isPythonMode = false;
96
+ this.ctx.updateEditorBorderColor();
97
+ return true;
98
+ }
99
+ if (
100
+ options.maintenance &&
101
+ (this.ctx.session.isCompacting || this.ctx.autoCompactionLoader || this.ctx.autoCompactionEscapeHandler)
102
+ ) {
103
+ this.ctx.session.abortCompaction();
104
+ return true;
105
+ }
106
+ if (options.maintenance && this.ctx.session.isGeneratingHandoff) {
107
+ this.ctx.session.abortHandoff();
108
+ return true;
109
+ }
110
+ if (options.retry) {
111
+ if (this.#isRetryBackoffActive()) {
112
+ if (this.ctx.retryEscapePrimed) {
113
+ this.ctx.session.abortRetry();
114
+ } else {
115
+ this.ctx.retryEscapePrimed = true;
116
+ this.ctx.session.retryNow();
117
+ }
118
+ return true;
119
+ }
120
+ this.ctx.retryEscapePrimed = false;
121
+ }
122
+ if (options.streaming && this.ctx.session.isStreaming) {
123
+ if (this.ctx.session.hasQueuedSteering && !this.#steerConsumePending) {
124
+ // First Esc with a queued steer: silently consume it and
125
+ // auto-continue via steer-on-interrupt instead of stalling on
126
+ // "Operation aborted".
127
+ this.#steerConsumePending = true;
128
+ void this.#abortInteractive({ silent: true });
129
+ } else {
130
+ void this.#abortInteractive();
131
+ }
132
+ return true;
133
+ }
134
+ return false;
135
+ }
136
+
137
+ #installGlobalInterruptListener(): void {
138
+ if (typeof this.ctx.ui.addInputListener !== "function") {
139
+ return;
140
+ }
141
+ this.#globalInterruptUnsubscribe?.();
142
+ this.#globalInterruptUnsubscribe = this.ctx.ui.addInputListener(data => {
143
+ if (!this.#matchesInterruptKey(data)) {
144
+ return undefined;
145
+ }
146
+ if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
147
+ return { consume: true };
148
+ }
149
+ const hookDialogActive = this.#hasHookDialog();
150
+ if (
151
+ this.#handleCancellableWorkEscape({
152
+ loading: hookDialogActive,
153
+ processes: hookDialogActive,
154
+ modes: false,
155
+ maintenance: true,
156
+ retry: true,
157
+ streaming: hookDialogActive,
158
+ })
159
+ ) {
160
+ return { consume: true };
161
+ }
162
+ return undefined;
163
+ });
164
+ }
165
+
44
166
  #abortInteractive(options?: { silent?: boolean }): Promise<void> {
45
167
  return this.ctx.session.abort({
46
168
  timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS,
@@ -59,6 +181,7 @@ export class InputController {
59
181
  this.ctx.session.isStreaming ||
60
182
  this.ctx.session.isCompacting ||
61
183
  this.ctx.session.isGeneratingHandoff ||
184
+ this.ctx.session.isRetrying ||
62
185
  this.ctx.session.isBashRunning ||
63
186
  this.ctx.session.isEvalRunning ||
64
187
  this.ctx.autoCompactionLoader ||
@@ -66,6 +189,8 @@ export class InputController {
66
189
  this.ctx.autoCompactionEscapeHandler ||
67
190
  this.ctx.retryEscapeHandler,
68
191
  );
192
+ this.#installGlobalInterruptListener();
193
+
69
194
  // An open btw panel must stay dismissable with Esc even while another
70
195
  // controller (auto-compaction, auto-retry, manual compaction, etc.) has
71
196
  // temporarily replaced editor.onEscape. This priority hook is never
@@ -86,6 +211,9 @@ export class InputController {
86
211
  }
87
212
  this.#steerConsumePending = false;
88
213
  }
214
+ if (this.#handleCancellableWorkEscape({ maintenance: true, retry: true })) {
215
+ return;
216
+ }
89
217
  // Normal input state with user-typed text: Esc must not interrupt a
90
218
  // running task (streaming turn, bash/eval). A double Esc within the
91
219
  // 500ms window clears the composer instead. Bash/Python input modes
@@ -100,35 +228,19 @@ export class InputController {
100
228
  }
101
229
  return;
102
230
  }
103
- if (this.ctx.loadingAnimation) {
104
- if (this.ctx.cancelPendingSubmission()) {
105
- return;
106
- }
107
- this.restoreQueuedMessagesToEditor({ abort: true });
108
- } else if (this.ctx.session.isBashRunning) {
109
- this.ctx.session.abortBash();
110
- } else if (this.ctx.isBashMode) {
111
- this.ctx.editor.setText("");
112
- this.ctx.isBashMode = false;
113
- this.ctx.isBashNoContext = false;
114
- this.ctx.updateEditorBorderColor();
115
- } else if (this.ctx.session.isEvalRunning) {
116
- this.ctx.session.abortEval();
117
- } else if (this.ctx.isPythonMode) {
118
- this.ctx.editor.setText("");
119
- this.ctx.isPythonMode = false;
120
- this.ctx.updateEditorBorderColor();
121
- } else if (this.ctx.session.isStreaming) {
122
- if (this.ctx.session.hasQueuedSteering && !this.#steerConsumePending) {
123
- // First Esc with a queued steer: silently consume it and
124
- // auto-continue via steer-on-interrupt instead of stalling on
125
- // "Operation aborted".
126
- this.#steerConsumePending = true;
127
- void this.#abortInteractive({ silent: true });
128
- } else {
129
- void this.#abortInteractive();
130
- }
131
- } else if (!this.ctx.editor.getText().trim()) {
231
+ if (
232
+ this.#handleCancellableWorkEscape({
233
+ loading: true,
234
+ processes: true,
235
+ modes: true,
236
+ maintenance: true,
237
+ retry: true,
238
+ streaming: true,
239
+ })
240
+ ) {
241
+ return;
242
+ }
243
+ if (!this.ctx.editor.getText().trim()) {
132
244
  // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
133
245
  const action = settings.get("doubleEscapeAction");
134
246
  if (action !== "none") {
@@ -155,6 +267,8 @@ export class InputController {
155
267
  this.ctx.editor.onSuspend = () => this.handleCtrlZ();
156
268
  this.ctx.editor.setActionKeys("app.thinking.cycle", this.ctx.keybindings.getKeys("app.thinking.cycle"));
157
269
  this.ctx.editor.onCycleThinkingLevel = () => this.cycleThinkingLevel();
270
+ this.ctx.editor.setActionKeys("app.commandPalette.open", this.ctx.keybindings.getKeys("app.commandPalette.open"));
271
+ this.ctx.editor.onOpenCommandPalette = () => this.openCommandPalette();
158
272
  this.ctx.editor.setActionKeys("app.model.cycleForward", this.ctx.keybindings.getKeys("app.model.cycleForward"));
159
273
  this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
160
274
  this.ctx.editor.setActionKeys("app.model.cycleBackward", this.ctx.keybindings.getKeys("app.model.cycleBackward"));
@@ -199,8 +313,17 @@ export class InputController {
199
313
  this.ctx.editor.onDequeue = () => this.handleDequeue();
200
314
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
201
315
  this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
316
+ this.ctx.editor.onTab = () => {
317
+ if (!this.ctx.session.isStreaming && !this.ctx.session.isCompacting) return false;
318
+ void this.handleQueueSubmit();
319
+ return true;
320
+ };
202
321
  this.ctx.editor.onTabDeclined = () => {
203
- if (this.ctx.session.isStreaming) void this.handleQueueSubmit();
322
+ if (this.ctx.session.isStreaming || this.ctx.session.isCompacting) void this.handleQueueSubmit();
323
+ };
324
+ this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
325
+ this.ctx.editor.onViewportFollowLive = () => {
326
+ this.ctx.ui.followLiveViewport();
204
327
  };
205
328
 
206
329
  this.ctx.editor.clearCustomKeyHandlers();
@@ -491,7 +614,7 @@ export class InputController {
491
614
  }
492
615
 
493
616
  handleDequeue(): void {
494
- const restored = this.restoreQueuedMessagesToEditor();
617
+ const restored = this.restoreLatestQueuedMessageToEditor();
495
618
  if (restored === 0) {
496
619
  this.ctx.showStatus("No queued messages to restore");
497
620
  } else {
@@ -521,19 +644,18 @@ export class InputController {
521
644
 
522
645
  /**
523
646
  * Dispatch skill slash invocation(s) (`/skill:<name>`) through custom messages
524
- * using the supplied `streamingBehavior`. Returns true if the text was a
525
- * recognised skill command chain and was dispatched. A failure to load a skill
526
- * file is surfaced via `showError` but still returns true — the editor was
527
- * already cleared on the success path, so falling through to plain-text
528
- * handling at that point would double-submit. Returns false when the text
529
- * isn't a `/skill:` prefix or the command name isn't a registered skill,
530
- * so the caller can fall through to plain-text handling (this branch
647
+ * using the supplied `streamingBehavior`. Returns true if the text contains a
648
+ * recognised canonical skill command or command chain and was dispatched. A
649
+ * failure to load a skill file is surfaced via `showError` but still returns
650
+ * true — the editor was already cleared on the success path, so falling
651
+ * through to plain-text handling at that point would double-submit. Returns
652
+ * false when the text has no registered canonical skill invocation, so the
653
+ * caller can fall through to plain-text handling (this branch
531
654
  * leaves the editor state untouched). `streamingBehavior` is only consulted
532
655
  * while the agent is streaming; the idle path of `promptCustomMessage`
533
656
  * ignores it.
534
657
  */
535
658
  async #invokeSkillCommand(text: string, streamingBehavior: "steer" | "followUp"): Promise<boolean> {
536
- if (!text.startsWith("/")) return false;
537
659
  const invocations = parseSkillInvocations(text, this.ctx.skillCommands ?? new Map());
538
660
  if (invocations.length === 0) return false;
539
661
  this.ctx.editor.addToHistory(text);
@@ -602,13 +724,15 @@ export class InputController {
602
724
  const text = this.ctx.editor.getText().trim();
603
725
  if (!text) return;
604
726
 
605
- // Compaction first: while compacting, free text gets queued via
606
- // `queueCompactionMessage`, and `/skill:*` rides the same queue so a
607
- // skill typed during compaction is not lost or short-circuited through
608
- // `promptCustomMessage`. The skill text is queued verbatim; whether
609
- // the queued entry is later re-parsed into a skill invocation is a
610
- // separate concern owned by the compaction-resume path.
727
+ // Compaction first: while compacting, queue free text and `/skill:*`
728
+ // commands in the compaction-local queue. `flushCompactionQueue`
729
+ // replays skill entries through the custom-message skill path after
730
+ // compaction finishes so they are not degraded into plain prompts.
611
731
  if (this.ctx.session.isCompacting) {
732
+ if (this.ctx.pendingImages.length > 0) {
733
+ this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
734
+ return;
735
+ }
612
736
  this.ctx.queueCompactionMessage(text, "followUp");
613
737
  return;
614
738
  }
@@ -642,10 +766,28 @@ export class InputController {
642
766
  return this.handleFollowUp();
643
767
  }
644
768
 
769
+ restoreLatestQueuedMessageToEditor(options?: { currentText?: string }): number {
770
+ const compactionQueued = this.ctx.compactionQueuedMessages.pop();
771
+ const queuedText = compactionQueued?.text ?? this.ctx.session.popLastQueuedMessage();
772
+ if (!queuedText) {
773
+ this.ctx.updatePendingMessagesDisplay();
774
+ return 0;
775
+ }
776
+
777
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
778
+ const currentText = options?.currentText ?? this.ctx.editor.getText();
779
+ const combinedText = [queuedText, currentText].filter(t => t.trim()).join("\n\n");
780
+ this.ctx.editor.setText(combinedText);
781
+ this.ctx.updatePendingMessagesDisplay();
782
+ return 1;
783
+ }
784
+
645
785
  restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
646
786
  this.ctx.locallySubmittedUserSignatures.clear();
647
787
  const { steering, followUp } = this.ctx.session.clearQueue();
648
- const allQueued = [...steering, ...followUp];
788
+ const compactionQueued = (this.ctx.compactionQueuedMessages ?? []).map(entry => entry.text);
789
+ this.ctx.compactionQueuedMessages = [];
790
+ const allQueued = [...steering, ...followUp, ...compactionQueued];
649
791
  if (allQueued.length === 0) {
650
792
  this.ctx.updatePendingMessagesDisplay();
651
793
  if (options?.abort) {
@@ -704,6 +846,7 @@ export class InputController {
704
846
  this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
705
847
  this.ctx.retryEscapeHandler = undefined;
706
848
  }
849
+ this.ctx.retryEscapePrimed = false;
707
850
  this.ctx.statusContainer.clear();
708
851
  this.ctx.statusLine.dispose();
709
852
 
@@ -858,6 +1001,9 @@ export class InputController {
858
1001
  copyCurrentLine: () => this.handleCopyCurrentLine(),
859
1002
  copyPrompt: () => this.handleCopyPrompt(),
860
1003
  pasteImage: () => void this.handleImagePaste(),
1004
+ newSession: () => void this.ctx.handleClearCommand(),
1005
+ showHelp: () => this.ctx.handleHelpCommand(),
1006
+ scrollTmuxToPreviousUserInput: () => this.scrollTmuxToPreviousUserInput(),
861
1007
  undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
862
1008
  moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
863
1009
  moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
@@ -901,6 +1047,14 @@ export class InputController {
901
1047
  }
902
1048
  }
903
1049
 
1050
+ openCommandPalette(): void {
1051
+ if (this.ctx.editor.getText().trim().length > 0) {
1052
+ this.ctx.showStatus("Command palette opens from an empty prompt. Type / for inline commands.");
1053
+ return;
1054
+ }
1055
+ this.ctx.editor.handleInput("/");
1056
+ }
1057
+
904
1058
  cycleThinkingLevel(): void {
905
1059
  const newLevel = this.ctx.session.cycleThinkingLevel();
906
1060
  if (newLevel === undefined) {
@@ -952,6 +1106,19 @@ export class InputController {
952
1106
  this.setToolsExpanded(!this.ctx.toolOutputExpanded);
953
1107
  }
954
1108
 
1109
+ scrollTmuxToPreviousUserInput(): void {
1110
+ const result = scrollTmuxPaneToPreviousUserInput();
1111
+ if (result.ok) return;
1112
+
1113
+ if (result.reason === "not_inside_tmux") {
1114
+ this.ctx.showWarning("Previous-input scroll works only inside tmux.");
1115
+ return;
1116
+ }
1117
+
1118
+ const detail = result.error ? `: ${result.error}` : ".";
1119
+ this.ctx.showWarning(`Failed to scroll tmux to previous user input${detail}`);
1120
+ }
1121
+
955
1122
  setToolsExpanded(expanded: boolean): void {
956
1123
  this.ctx.toolOutputExpanded = expanded;
957
1124
  for (const child of this.ctx.chatContainer.children) {
@@ -8,7 +8,8 @@ import { activateModelProfile, materializeActiveModelProfileAssignment } from ".
8
8
  import { recommendModelProfileForProvider } from "../../config/model-profiles";
9
9
  import { SKC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
10
10
  import { formatModelSelectorValue } from "../../config/model-resolver";
11
- import { settings } from "../../config/settings";
11
+ import type { ModelProfileConfig } from "../../config/models-config-schema";
12
+ import { type Settings, settings } from "../../config/settings";
12
13
  import { DebugSelectorComponent } from "../../debug";
13
14
  import { disableProvider, enableProvider } from "../../discovery";
14
15
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -74,6 +75,7 @@ import {
74
75
  import { SessionObserverOverlayComponent } from "../components/session-observer-overlay";
75
76
  import { SessionSelectorComponent } from "../components/session-selector";
76
77
  import { SettingsSelectorComponent } from "../components/settings-selector";
78
+ import type { StatusLineSettings } from "../components/status-line";
77
79
  import { ThemeSelectorComponent } from "../components/theme-selector";
78
80
  import { ToolExecutionComponent } from "../components/tool-execution";
79
81
  import { TreeSelectorComponent } from "../components/tree-selector";
@@ -97,6 +99,24 @@ function isThemePreviewSuperseded(result: { success: boolean; error?: string }):
97
99
  return !result.success && result.error?.includes("superseded by a newer request") === true;
98
100
  }
99
101
 
102
+ /**
103
+ * Snapshot the persisted status-line settings that the status-line component
104
+ * cares about. Preview, cancel-restore, and commit paths all share this so the
105
+ * previewed row count (and every other field) can never drift out of sync.
106
+ */
107
+ export function buildStatusLineSettings(settingsInstance: Settings): StatusLineSettings {
108
+ return {
109
+ preset: settingsInstance.get("statusLine.preset"),
110
+ leftSegments: settingsInstance.get("statusLine.leftSegments"),
111
+ rightSegments: settingsInstance.get("statusLine.rightSegments"),
112
+ separator: settingsInstance.get("statusLine.separator"),
113
+ showHookStatus: settingsInstance.get("statusLine.showHookStatus"),
114
+ sessionAccent: settingsInstance.get("statusLine.sessionAccent"),
115
+ maxRows: settingsInstance.get("statusLine.maxRows"),
116
+ segmentOptions: settingsInstance.get("statusLine.segmentOptions"),
117
+ };
118
+ }
119
+
100
120
  function formatProviderOnboardingCommandGuide(): string {
101
121
  return [
102
122
  "Provider preset setup:",
@@ -243,7 +263,7 @@ export class SelectorController {
243
263
  this.ctx.ui.requestRender();
244
264
  }
245
265
 
246
- showCustomModelPresetWizard(): void {
266
+ showCustomModelPresetWizard(snapshot: ModelProfileConfig): void {
247
267
  this.showSelector(done => {
248
268
  let wizard: CustomModelPresetWizardComponent;
249
269
  const submit = async (input: CustomModelPresetWizardSubmit): Promise<void> => {
@@ -260,6 +280,7 @@ export class SelectorController {
260
280
  }
261
281
  };
262
282
  wizard = new CustomModelPresetWizardComponent(
283
+ snapshot,
263
284
  input => {
264
285
  void submit(input);
265
286
  },
@@ -334,21 +355,17 @@ export class SelectorController {
334
355
  onStatusLinePreview: previewSettings => {
335
356
  // Update status line with preview settings
336
357
  this.ctx.statusLine.updateSettings({
337
- preset: settings.get("statusLine.preset"),
338
- leftSegments: settings.get("statusLine.leftSegments"),
339
- rightSegments: settings.get("statusLine.rightSegments"),
340
- separator: settings.get("statusLine.separator"),
341
- showHookStatus: settings.get("statusLine.showHookStatus"),
342
- sessionAccent: settings.get("statusLine.sessionAccent"),
358
+ ...buildStatusLineSettings(settings),
343
359
  ...previewSettings,
344
360
  });
345
361
  this.ctx.updateEditorTopBorder();
346
362
  this.ctx.ui.requestRender();
347
363
  },
348
- getStatusLinePreview: () => {
364
+ getStatusLinePreview: (width?: number) => {
349
365
  // Return the rendered status line for inline preview
350
- const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
351
- return this.ctx.statusLine.getTopBorder(availableWidth).content;
366
+ const availableWidth =
367
+ width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
368
+ return this.ctx.statusLine.getPreviewContent(availableWidth);
352
369
  },
353
370
  onPluginsChanged: () => {
354
371
  this.ctx.ui.requestRender();
@@ -356,14 +373,7 @@ export class SelectorController {
356
373
  onCancel: () => {
357
374
  done();
358
375
  // Restore status line to saved settings
359
- this.ctx.statusLine.updateSettings({
360
- preset: settings.get("statusLine.preset"),
361
- leftSegments: settings.get("statusLine.leftSegments"),
362
- rightSegments: settings.get("statusLine.rightSegments"),
363
- separator: settings.get("statusLine.separator"),
364
- showHookStatus: settings.get("statusLine.showHookStatus"),
365
- sessionAccent: settings.get("statusLine.sessionAccent"),
366
- });
376
+ this.ctx.statusLine.updateSettings(buildStatusLineSettings(settings));
367
377
  this.ctx.updateEditorTopBorder();
368
378
  this.ctx.ui.requestRender();
369
379
  },
@@ -607,6 +617,10 @@ export class SelectorController {
607
617
  case "statusLineShowHooks":
608
618
  case "statusLine.showHookStatus":
609
619
  case "statusLine.sessionAccent":
620
+ case "statusLine.maxRows":
621
+ case "statusLine.leftSegments":
622
+ case "statusLine.rightSegments":
623
+ case "statusLine.segmentOptions":
610
624
  case "statusLineSegments":
611
625
  case "statusLineModelThinking":
612
626
  case "statusLinePathAbbreviate":
@@ -618,16 +632,7 @@ export class SelectorController {
618
632
  case "statusLineGitShowUntracked":
619
633
  case "statusLineTimeFormat":
620
634
  case "statusLineTimeShowSeconds": {
621
- const statusLineSettings = {
622
- preset: settings.get("statusLine.preset"),
623
- leftSegments: settings.get("statusLine.leftSegments"),
624
- rightSegments: settings.get("statusLine.rightSegments"),
625
- separator: settings.get("statusLine.separator"),
626
- showHookStatus: settings.get("statusLine.showHookStatus"),
627
- sessionAccent: settings.get("statusLine.sessionAccent"),
628
- segmentOptions: settings.get("statusLine.segmentOptions"),
629
- };
630
- this.ctx.statusLine.updateSettings(statusLineSettings);
635
+ this.ctx.statusLine.updateSettings(buildStatusLineSettings(settings));
631
636
  this.ctx.updateEditorTopBorder();
632
637
  this.ctx.ui.requestRender();
633
638
  break;
@@ -685,9 +690,6 @@ export class SelectorController {
685
690
  async selection => {
686
691
  try {
687
692
  if (selection.kind === "login") {
688
- // User picked an unauthenticated preset/model. Close the model
689
- // selector and start OAuth for the provider if it supports it;
690
- // otherwise guide them to the right command.
691
693
  const oauth = getOAuthProviders().find(p => p.id === selection.providerId);
692
694
  done();
693
695
  if (oauth) {
@@ -702,7 +704,7 @@ export class SelectorController {
702
704
  }
703
705
  if (selection.kind === "createProfile") {
704
706
  done();
705
- this.showCustomModelPresetWizard();
707
+ this.showCustomModelPresetWizard(selection.profile);
706
708
  return;
707
709
  }
708
710
  if (selection.kind === "profile") {
@@ -72,7 +72,6 @@ import { normalizeLocalScheme } from "../tools/path-utils";
72
72
  import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
73
73
  import { formatPhaseDisplayName } from "../tools/todo-write";
74
74
  import { ToolError } from "../tools/tool-errors";
75
-
76
75
  import type { EventBus } from "../utils/event-bus";
77
76
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
78
77
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
@@ -104,6 +103,7 @@ import { JobsObserver } from "./jobs-observer";
104
103
  import { OAuthManualInputManager } from "./oauth-manual-input";
105
104
  import { SessionObserverRegistry } from "./session-observer-registry";
106
105
  import { interruptHint } from "./shared";
106
+ import { shouldShowExtensionCommand } from "./slash-command-visibility";
107
107
  import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
108
108
  import type { Theme } from "./theme/theme";
109
109
  import {
@@ -322,6 +322,7 @@ export class InteractiveMode implements InteractiveModeContext {
322
322
  }
323
323
  autoCompactionEscapeHandler?: () => void;
324
324
  retryEscapeHandler?: () => void;
325
+ retryEscapePrimed = false;
325
326
  retryCountdownTimer?: NodeJS.Timeout;
326
327
  unsubscribe?: () => void;
327
328
  onInputCallback?: (input: SubmittedUserInput) => void;
@@ -423,7 +424,7 @@ export class InteractiveMode implements InteractiveModeContext {
423
424
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
424
425
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
425
426
  this.editor.onAutocompleteCancel = () => {
426
- this.ui.requestRender(true);
427
+ this.ui.requestRender();
427
428
  };
428
429
  this.editor.onAutocompleteUpdate = () => {
429
430
  this.ui.requestRender();
@@ -433,7 +434,7 @@ export class InteractiveMode implements InteractiveModeContext {
433
434
  this.#syncEditorMaxHeight();
434
435
  this.updateEditorChrome();
435
436
  this.editor.invalidate();
436
- this.ui.requestRender(true, "resize");
437
+ this.ui.requestResizeRender();
437
438
  };
438
439
  process.stdout.on("resize", this.#resizeHandler);
439
440
  try {
@@ -453,13 +454,16 @@ export class InteractiveMode implements InteractiveModeContext {
453
454
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
454
455
 
455
456
  const builtinCommandNames = new Set(BUILTIN_SLASH_COMMANDS.map(c => c.name));
457
+ const activeProvider = this.session.model?.provider;
456
458
  const hookCommands: SlashCommand[] = (
457
459
  this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []
458
- ).map(cmd => ({
459
- name: cmd.name,
460
- description: cmd.description ?? "(hook command)",
461
- getArgumentCompletions: cmd.getArgumentCompletions,
462
- }));
460
+ )
461
+ .filter(cmd => shouldShowExtensionCommand(cmd.name, activeProvider))
462
+ .map(cmd => ({
463
+ name: cmd.name,
464
+ description: cmd.description ?? "(hook command)",
465
+ getArgumentCompletions: cmd.getArgumentCompletions,
466
+ }));
463
467
 
464
468
  // Convert custom commands (TypeScript) to SlashCommand format
465
469
  const customCommands: SlashCommand[] = this.session.customCommands.map(loaded => ({
@@ -528,6 +532,14 @@ export class InteractiveMode implements InteractiveModeContext {
528
532
  }
529
533
 
530
534
  if (!startupQuiet) {
535
+ const getWelcomeReservedBottomRows = (width: number): number =>
536
+ [
537
+ this.statusLine,
538
+ this.hookWidgetContainerAbove,
539
+ this.editorContainer,
540
+ this.hookWidgetContainerBelow,
541
+ ].reduce((rows, component) => rows + component.render(width).length, 0);
542
+
531
543
  // Add welcome header
532
544
  this.#welcomeComponent = new WelcomeComponent(
533
545
  this.#version,
@@ -536,30 +548,16 @@ export class InteractiveMode implements InteractiveModeContext {
536
548
  recentSessions,
537
549
  this.#getWelcomeLspServers(),
538
550
  welcomeLogoMode,
551
+ {
552
+ getViewportRows: () => this.ui.terminal.rows,
553
+ getReservedBottomRows: getWelcomeReservedBottomRows,
554
+ changelogMarkdown: this.#changelogMarkdown,
555
+ collapseChangelog: settings.get("collapseChangelog"),
556
+ },
539
557
  );
540
558
 
541
- // Setup UI layout
542
- this.ui.addChild(new Spacer(1));
543
559
  this.ui.addChild(this.#welcomeComponent);
544
- this.ui.addChild(new Spacer(1));
545
560
  this.#welcomeComponent.playIntro(() => this.ui.requestRender());
546
-
547
- // Add changelog if provided
548
- if (this.#changelogMarkdown) {
549
- this.ui.addChild(new DynamicBorder());
550
- if (settings.get("collapseChangelog")) {
551
- const versionMatch = this.#changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
552
- const latestVersion = versionMatch ? versionMatch[1] : this.#version;
553
- const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
554
- this.ui.addChild(new Text(condensedText, 1, 0));
555
- } else {
556
- this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
557
- this.ui.addChild(new Spacer(1));
558
- this.ui.addChild(new Markdown(this.#changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
559
- this.ui.addChild(new Spacer(1));
560
- }
561
- this.ui.addChild(new DynamicBorder());
562
- }
563
561
  }
564
562
 
565
563
  this.ui.addChild(this.chatContainer);
@@ -1961,6 +1959,8 @@ export class InteractiveMode implements InteractiveModeContext {
1961
1959
  this.loadingAnimation.stop();
1962
1960
  this.loadingAnimation = undefined;
1963
1961
  }
1962
+ this.#welcomeComponent?.dispose();
1963
+ this.#welcomeComponent = undefined;
1964
1964
  this.#cleanupMicAnimation();
1965
1965
  this.#cancelGoalContinuation();
1966
1966
  if (this.#sttController) {
@@ -2036,7 +2036,9 @@ export class InteractiveMode implements InteractiveModeContext {
2036
2036
  const sessionId = this.sessionManager.getSessionId();
2037
2037
  const sessionFile = this.sessionManager.getSessionFile();
2038
2038
  if (sessionId && sessionFile) {
2039
- process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
2039
+ process.stderr.write(
2040
+ `\n${chalk.dim("Resume this session with:")}\n${chalk.dim(`${APP_NAME} --resume ${sessionId}`)}\n`,
2041
+ );
2040
2042
  }
2041
2043
 
2042
2044
  await postmortem.quit(0);
@@ -2072,7 +2074,7 @@ export class InteractiveMode implements InteractiveModeContext {
2072
2074
  nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
2073
2075
  nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
2074
2076
  nextEditor.onAutocompleteCancel = () => {
2075
- this.ui.requestRender(true);
2077
+ this.ui.requestRender();
2076
2078
  };
2077
2079
  nextEditor.onAutocompleteUpdate = () => {
2078
2080
  this.ui.requestRender();
@@ -2312,6 +2314,10 @@ export class InteractiveMode implements InteractiveModeContext {
2312
2314
  this.#commandController.handleHotkeysCommand();
2313
2315
  }
2314
2316
 
2317
+ handleHelpCommand(): void {
2318
+ this.#commandController.handleHelpCommand();
2319
+ }
2320
+
2315
2321
  handleToolsCommand(): void {
2316
2322
  this.#commandController.handleToolsCommand();
2317
2323
  }