@gajae-code/coding-agent 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +2 -2
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/config/keybindings.d.ts +11 -5
  5. package/dist/types/config/settings-schema.d.ts +25 -2
  6. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  7. package/dist/types/discovery/cursor.d.ts +4 -1
  8. package/dist/types/discovery/index.d.ts +0 -1
  9. package/dist/types/discovery/windsurf.d.ts +3 -1
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  12. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  15. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  16. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  17. package/dist/types/modes/components/skill-message.d.ts +1 -0
  18. package/dist/types/modes/components/status-line.d.ts +8 -0
  19. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  20. package/dist/types/modes/components/welcome.d.ts +9 -1
  21. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  22. package/dist/types/modes/controllers/input-controller.d.ts +5 -1
  23. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  24. package/dist/types/modes/interactive-mode.d.ts +2 -0
  25. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  26. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  27. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  28. package/dist/types/modes/types.d.ts +2 -0
  29. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  30. package/dist/types/runtime-mcp/config.d.ts +2 -0
  31. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  32. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  33. package/dist/types/runtime-mcp/types.d.ts +6 -0
  34. package/dist/types/sdk.d.ts +1 -1
  35. package/dist/types/session/agent-session.d.ts +8 -5
  36. package/dist/types/slash-commands/types.d.ts +2 -0
  37. package/dist/types/system-prompt.d.ts +7 -0
  38. package/dist/types/utils/cmux-workspace.d.ts +30 -1
  39. package/dist/types/web/search/index.d.ts +3 -1
  40. package/dist/types/web/search/provider.d.ts +9 -0
  41. package/dist/types/web/search/providers/utils.d.ts +42 -13
  42. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  43. package/package.json +10 -10
  44. package/src/capability/mcp.ts +2 -0
  45. package/src/cli/update-cli.ts +24 -0
  46. package/src/cli/web-search-cli.ts +2 -5
  47. package/src/cli.ts +1 -1
  48. package/src/commands/launch.ts +1 -1
  49. package/src/commands/ralplan.ts +1 -0
  50. package/src/config/keybindings.ts +14 -5
  51. package/src/config/mcp-schema.json +5 -1
  52. package/src/config/model-profiles.ts +16 -0
  53. package/src/config/settings-schema.ts +18 -2
  54. package/src/coordinator-mcp/server.ts +15 -3
  55. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  56. package/src/defaults/gjc-defaults.ts +16 -0
  57. package/src/discovery/builtin.ts +12 -0
  58. package/src/discovery/cursor.ts +5 -84
  59. package/src/discovery/gemini.ts +3 -89
  60. package/src/discovery/index.ts +4 -1
  61. package/src/discovery/mcp-json.ts +11 -0
  62. package/src/discovery/opencode.ts +5 -122
  63. package/src/discovery/windsurf.ts +4 -82
  64. package/src/edit/read-file.ts +9 -1
  65. package/src/extensibility/extensions/runner.ts +2 -1
  66. package/src/gjc-runtime/launch-tmux.ts +164 -42
  67. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  68. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +167 -24
  70. package/src/gjc-runtime/state-runtime.ts +8 -5
  71. package/src/gjc-runtime/state-writer.ts +5 -5
  72. package/src/gjc-runtime/team-runtime.ts +56 -51
  73. package/src/internal-urls/docs-index.generated.ts +8 -7
  74. package/src/main.ts +2 -2
  75. package/src/migrate/executor.ts +23 -6
  76. package/src/modes/components/agent-dashboard.ts +1 -34
  77. package/src/modes/components/custom-editor.ts +26 -0
  78. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  79. package/src/modes/components/model-selector.ts +64 -4
  80. package/src/modes/components/session-selector.ts +19 -9
  81. package/src/modes/components/settings-selector.ts +13 -2
  82. package/src/modes/components/skill-message.ts +18 -10
  83. package/src/modes/components/status-line.ts +215 -86
  84. package/src/modes/components/tree-selector.ts +28 -8
  85. package/src/modes/components/two-column-body.ts +36 -0
  86. package/src/modes/components/welcome.ts +171 -35
  87. package/src/modes/controllers/command-controller.ts +23 -0
  88. package/src/modes/controllers/event-controller.ts +4 -0
  89. package/src/modes/controllers/input-controller.ts +192 -39
  90. package/src/modes/controllers/selector-controller.ts +25 -28
  91. package/src/modes/interactive-mode.ts +36 -30
  92. package/src/modes/prompt-action-autocomplete.ts +23 -1
  93. package/src/modes/rpc/rpc-types.ts +1 -0
  94. package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
  95. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  96. package/src/modes/slash-command-visibility.ts +9 -0
  97. package/src/modes/types.ts +2 -0
  98. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  99. package/src/modes/utils/ui-helpers.ts +76 -3
  100. package/src/notifications/index.ts +20 -1
  101. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  102. package/src/notifications/telegram-daemon.ts +4 -4
  103. package/src/prompts/agents/architect.md +7 -4
  104. package/src/prompts/agents/critic.md +15 -6
  105. package/src/prompts/agents/planner.md +8 -3
  106. package/src/prompts/system/system-prompt.md +15 -1
  107. package/src/prompts/tools/bash.md +1 -1
  108. package/src/runtime-mcp/config-writer.ts +28 -0
  109. package/src/runtime-mcp/config.ts +7 -0
  110. package/src/runtime-mcp/loader.ts +3 -0
  111. package/src/runtime-mcp/manager.ts +3 -0
  112. package/src/runtime-mcp/types.ts +6 -0
  113. package/src/sdk.ts +7 -6
  114. package/src/session/agent-session.ts +81 -18
  115. package/src/session/session-manager.ts +58 -18
  116. package/src/slash-commands/builtin-registry.ts +30 -10
  117. package/src/slash-commands/types.ts +2 -0
  118. package/src/system-prompt.ts +8 -0
  119. package/src/task/executor.ts +5 -0
  120. package/src/tools/ast-grep.ts +10 -7
  121. package/src/tools/bash.ts +39 -14
  122. package/src/tools/index.ts +8 -1
  123. package/src/utils/cmux-workspace.ts +134 -7
  124. package/src/utils/title-generator.ts +1 -1
  125. package/src/web/kagi.ts +1 -1
  126. package/src/web/parallel.ts +2 -2
  127. package/src/web/search/index.ts +118 -45
  128. package/src/web/search/provider.ts +65 -0
  129. package/src/web/search/providers/anthropic.ts +1 -1
  130. package/src/web/search/providers/brave.ts +1 -1
  131. package/src/web/search/providers/codex.ts +1 -1
  132. package/src/web/search/providers/duckduckgo.ts +1 -1
  133. package/src/web/search/providers/exa.ts +1 -1
  134. package/src/web/search/providers/gemini.ts +7 -3
  135. package/src/web/search/providers/jina.ts +1 -1
  136. package/src/web/search/providers/kimi.ts +6 -1
  137. package/src/web/search/providers/openai-compatible.ts +1 -1
  138. package/src/web/search/providers/parallel.ts +1 -1
  139. package/src/web/search/providers/perplexity.ts +2 -2
  140. package/src/web/search/providers/searxng.ts +1 -1
  141. package/src/web/search/providers/synthetic.ts +1 -1
  142. package/src/web/search/providers/tavily.ts +1 -1
  143. package/src/web/search/providers/utils.ts +67 -17
  144. package/src/web/search/providers/xai.ts +1 -1
  145. package/src/web/search/providers/zai.ts +1 -1
  146. package/src/workflow/workflow-intent-diff.ts +199 -0
  147. package/dist/types/discovery/vscode.d.ts +0 -1
  148. package/src/discovery/vscode.ts +0 -105
@@ -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 "@gajae-code/agent-core";
4
- import type { AutocompleteProvider, SlashCommand } from "@gajae-code/tui";
4
+ import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
5
5
  import { $env, sanitizeText } from "@gajae-code/utils";
6
6
  import { isSettingsInitialized, settings } from "../../config/settings";
7
7
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
@@ -42,6 +42,127 @@ export class InputController {
42
42
  * so abort cleanup going idle cannot turn the second Esc into an idle action. */
43
43
  #steerConsumePending = false;
44
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
+
45
166
  #abortInteractive(options?: { silent?: boolean }): Promise<void> {
46
167
  return this.ctx.session.abort({
47
168
  timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS,
@@ -60,6 +181,7 @@ export class InputController {
60
181
  this.ctx.session.isStreaming ||
61
182
  this.ctx.session.isCompacting ||
62
183
  this.ctx.session.isGeneratingHandoff ||
184
+ this.ctx.session.isRetrying ||
63
185
  this.ctx.session.isBashRunning ||
64
186
  this.ctx.session.isEvalRunning ||
65
187
  this.ctx.autoCompactionLoader ||
@@ -67,6 +189,8 @@ export class InputController {
67
189
  this.ctx.autoCompactionEscapeHandler ||
68
190
  this.ctx.retryEscapeHandler,
69
191
  );
192
+ this.#installGlobalInterruptListener();
193
+
70
194
  // An open btw panel must stay dismissable with Esc even while another
71
195
  // controller (auto-compaction, auto-retry, manual compaction, etc.) has
72
196
  // temporarily replaced editor.onEscape. This priority hook is never
@@ -87,6 +211,9 @@ export class InputController {
87
211
  }
88
212
  this.#steerConsumePending = false;
89
213
  }
214
+ if (this.#handleCancellableWorkEscape({ maintenance: true, retry: true })) {
215
+ return;
216
+ }
90
217
  // Normal input state with user-typed text: Esc must not interrupt a
91
218
  // running task (streaming turn, bash/eval). A double Esc within the
92
219
  // 500ms window clears the composer instead. Bash/Python input modes
@@ -101,35 +228,19 @@ export class InputController {
101
228
  }
102
229
  return;
103
230
  }
104
- if (this.ctx.loadingAnimation) {
105
- if (this.ctx.cancelPendingSubmission()) {
106
- return;
107
- }
108
- this.restoreQueuedMessagesToEditor({ abort: true });
109
- } else if (this.ctx.session.isBashRunning) {
110
- this.ctx.session.abortBash();
111
- } else if (this.ctx.isBashMode) {
112
- this.ctx.editor.setText("");
113
- this.ctx.isBashMode = false;
114
- this.ctx.isBashNoContext = false;
115
- this.ctx.updateEditorBorderColor();
116
- } else if (this.ctx.session.isEvalRunning) {
117
- this.ctx.session.abortEval();
118
- } else if (this.ctx.isPythonMode) {
119
- this.ctx.editor.setText("");
120
- this.ctx.isPythonMode = false;
121
- this.ctx.updateEditorBorderColor();
122
- } else if (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
- } 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()) {
133
244
  // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
134
245
  const action = settings.get("doubleEscapeAction");
135
246
  if (action !== "none") {
@@ -156,6 +267,8 @@ export class InputController {
156
267
  this.ctx.editor.onSuspend = () => this.handleCtrlZ();
157
268
  this.ctx.editor.setActionKeys("app.thinking.cycle", this.ctx.keybindings.getKeys("app.thinking.cycle"));
158
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();
159
272
  this.ctx.editor.setActionKeys("app.model.cycleForward", this.ctx.keybindings.getKeys("app.model.cycleForward"));
160
273
  this.ctx.editor.onCycleModelForward = () => this.cycleRoleModel();
161
274
  this.ctx.editor.setActionKeys("app.model.cycleBackward", this.ctx.keybindings.getKeys("app.model.cycleBackward"));
@@ -200,8 +313,17 @@ export class InputController {
200
313
  this.ctx.editor.onDequeue = () => this.handleDequeue();
201
314
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
202
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
+ };
203
321
  this.ctx.editor.onTabDeclined = () => {
204
- 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();
205
327
  };
206
328
 
207
329
  this.ctx.editor.clearCustomKeyHandlers();
@@ -492,7 +614,7 @@ export class InputController {
492
614
  }
493
615
 
494
616
  handleDequeue(): void {
495
- const restored = this.restoreQueuedMessagesToEditor();
617
+ const restored = this.restoreLatestQueuedMessageToEditor();
496
618
  if (restored === 0) {
497
619
  this.ctx.showStatus("No queued messages to restore");
498
620
  } else {
@@ -603,13 +725,15 @@ export class InputController {
603
725
  const text = this.ctx.editor.getText().trim();
604
726
  if (!text) return;
605
727
 
606
- // Compaction first: while compacting, free text gets queued via
607
- // `queueCompactionMessage`, and `/skill:*` rides the same queue so a
608
- // skill typed during compaction is not lost or short-circuited through
609
- // `promptCustomMessage`. The skill text is queued verbatim; whether
610
- // the queued entry is later re-parsed into a skill invocation is a
611
- // separate concern owned by the compaction-resume path.
728
+ // Compaction first: while compacting, queue free text and `/skill:*`
729
+ // commands in the compaction-local queue. `flushCompactionQueue`
730
+ // replays skill entries through the custom-message skill path after
731
+ // compaction finishes so they are not degraded into plain prompts.
612
732
  if (this.ctx.session.isCompacting) {
733
+ if (this.ctx.pendingImages.length > 0) {
734
+ this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
735
+ return;
736
+ }
613
737
  this.ctx.queueCompactionMessage(text, "followUp");
614
738
  return;
615
739
  }
@@ -643,10 +767,28 @@ export class InputController {
643
767
  return this.handleFollowUp();
644
768
  }
645
769
 
770
+ restoreLatestQueuedMessageToEditor(options?: { currentText?: string }): number {
771
+ const compactionQueued = this.ctx.compactionQueuedMessages.pop();
772
+ const queuedText = compactionQueued?.text ?? this.ctx.session.popLastQueuedMessage();
773
+ if (!queuedText) {
774
+ this.ctx.updatePendingMessagesDisplay();
775
+ return 0;
776
+ }
777
+
778
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
779
+ const currentText = options?.currentText ?? this.ctx.editor.getText();
780
+ const combinedText = [queuedText, currentText].filter(t => t.trim()).join("\n\n");
781
+ this.ctx.editor.setText(combinedText);
782
+ this.ctx.updatePendingMessagesDisplay();
783
+ return 1;
784
+ }
785
+
646
786
  restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
647
787
  this.ctx.locallySubmittedUserSignatures.clear();
648
788
  const { steering, followUp } = this.ctx.session.clearQueue();
649
- const allQueued = [...steering, ...followUp];
789
+ const compactionQueued = (this.ctx.compactionQueuedMessages ?? []).map(entry => entry.text);
790
+ this.ctx.compactionQueuedMessages = [];
791
+ const allQueued = [...steering, ...followUp, ...compactionQueued];
650
792
  if (allQueued.length === 0) {
651
793
  this.ctx.updatePendingMessagesDisplay();
652
794
  if (options?.abort) {
@@ -705,6 +847,7 @@ export class InputController {
705
847
  this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
706
848
  this.ctx.retryEscapeHandler = undefined;
707
849
  }
850
+ this.ctx.retryEscapePrimed = false;
708
851
  this.ctx.statusContainer.clear();
709
852
  this.ctx.statusLine.dispose();
710
853
 
@@ -859,6 +1002,8 @@ export class InputController {
859
1002
  copyCurrentLine: () => this.handleCopyCurrentLine(),
860
1003
  copyPrompt: () => this.handleCopyPrompt(),
861
1004
  pasteImage: () => void this.handleImagePaste(),
1005
+ newSession: () => void this.ctx.handleClearCommand(),
1006
+ showHelp: () => this.ctx.handleHelpCommand(),
862
1007
  scrollTmuxToPreviousUserInput: () => this.scrollTmuxToPreviousUserInput(),
863
1008
  undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
864
1009
  moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
@@ -903,6 +1048,14 @@ export class InputController {
903
1048
  }
904
1049
  }
905
1050
 
1051
+ openCommandPalette(): void {
1052
+ if (this.ctx.editor.getText().trim().length > 0) {
1053
+ this.ctx.showStatus("Command palette opens from an empty prompt. Type / for inline commands.");
1054
+ return;
1055
+ }
1056
+ this.ctx.editor.handleInput("/");
1057
+ }
1058
+
906
1059
  cycleThinkingLevel(): void {
907
1060
  const newLevel = this.ctx.session.cycleThinkingLevel();
908
1061
  if (newLevel === undefined) {
@@ -15,7 +15,7 @@ import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from
15
15
  import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
16
16
  import { formatModelSelectorValue } from "../../config/model-resolver";
17
17
  import type { ModelProfileConfig } from "../../config/models-config-schema";
18
- import { settings } from "../../config/settings";
18
+ import { type Settings, settings } from "../../config/settings";
19
19
  import { DebugSelectorComponent } from "../../debug";
20
20
  import { disableProvider, enableProvider } from "../../discovery";
21
21
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -85,6 +85,7 @@ import {
85
85
  import { SessionObserverOverlayComponent } from "../components/session-observer-overlay";
86
86
  import { SessionSelectorComponent } from "../components/session-selector";
87
87
  import { SettingsSelectorComponent } from "../components/settings-selector";
88
+ import type { StatusLineSettings } from "../components/status-line";
88
89
  import { ThemeSelectorComponent } from "../components/theme-selector";
89
90
  import { ToolExecutionComponent } from "../components/tool-execution";
90
91
  import { TreeSelectorComponent } from "../components/tree-selector";
@@ -108,6 +109,24 @@ function isThemePreviewSuperseded(result: { success: boolean; error?: string }):
108
109
  return !result.success && result.error?.includes("superseded by a newer request") === true;
109
110
  }
110
111
 
112
+ /**
113
+ * Snapshot the persisted status-line settings that the status-line component
114
+ * cares about. Preview, cancel-restore, and commit paths all share this so the
115
+ * previewed row count (and every other field) can never drift out of sync.
116
+ */
117
+ export function buildStatusLineSettings(settingsInstance: Settings): StatusLineSettings {
118
+ return {
119
+ preset: settingsInstance.get("statusLine.preset"),
120
+ leftSegments: settingsInstance.get("statusLine.leftSegments"),
121
+ rightSegments: settingsInstance.get("statusLine.rightSegments"),
122
+ separator: settingsInstance.get("statusLine.separator"),
123
+ showHookStatus: settingsInstance.get("statusLine.showHookStatus"),
124
+ sessionAccent: settingsInstance.get("statusLine.sessionAccent"),
125
+ maxRows: settingsInstance.get("statusLine.maxRows"),
126
+ segmentOptions: settingsInstance.get("statusLine.segmentOptions"),
127
+ };
128
+ }
129
+
111
130
  function formatProviderOnboardingCommandGuide(): string {
112
131
  return [
113
132
  "Provider preset setup:",
@@ -442,13 +461,7 @@ export class SelectorController {
442
461
  onStatusLinePreview: previewSettings => {
443
462
  // Update status line with preview settings
444
463
  this.ctx.statusLine.updateSettings({
445
- preset: settings.get("statusLine.preset"),
446
- leftSegments: settings.get("statusLine.leftSegments"),
447
- rightSegments: settings.get("statusLine.rightSegments"),
448
- separator: settings.get("statusLine.separator"),
449
- showHookStatus: settings.get("statusLine.showHookStatus"),
450
- sessionAccent: settings.get("statusLine.sessionAccent"),
451
- segmentOptions: settings.get("statusLine.segmentOptions"),
464
+ ...buildStatusLineSettings(settings),
452
465
  ...previewSettings,
453
466
  });
454
467
  this.ctx.updateEditorTopBorder();
@@ -458,7 +471,7 @@ export class SelectorController {
458
471
  // Return the rendered status line for inline preview
459
472
  const availableWidth =
460
473
  width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
461
- return this.ctx.statusLine.getTopBorder(availableWidth).content;
474
+ return this.ctx.statusLine.getPreviewContent(availableWidth);
462
475
  },
463
476
  onPluginsChanged: () => {
464
477
  this.ctx.ui.requestRender();
@@ -466,15 +479,7 @@ export class SelectorController {
466
479
  onCancel: () => {
467
480
  done();
468
481
  // Restore status line to saved settings
469
- this.ctx.statusLine.updateSettings({
470
- preset: settings.get("statusLine.preset"),
471
- leftSegments: settings.get("statusLine.leftSegments"),
472
- rightSegments: settings.get("statusLine.rightSegments"),
473
- separator: settings.get("statusLine.separator"),
474
- showHookStatus: settings.get("statusLine.showHookStatus"),
475
- sessionAccent: settings.get("statusLine.sessionAccent"),
476
- segmentOptions: settings.get("statusLine.segmentOptions"),
477
- });
482
+ this.ctx.statusLine.updateSettings(buildStatusLineSettings(settings));
478
483
  this.ctx.updateEditorTopBorder();
479
484
  this.ctx.ui.requestRender();
480
485
  },
@@ -718,6 +723,7 @@ export class SelectorController {
718
723
  case "statusLineShowHooks":
719
724
  case "statusLine.showHookStatus":
720
725
  case "statusLine.sessionAccent":
726
+ case "statusLine.maxRows":
721
727
  case "statusLine.leftSegments":
722
728
  case "statusLine.rightSegments":
723
729
  case "statusLine.segmentOptions":
@@ -732,16 +738,7 @@ export class SelectorController {
732
738
  case "statusLineGitShowUntracked":
733
739
  case "statusLineTimeFormat":
734
740
  case "statusLineTimeShowSeconds": {
735
- const statusLineSettings = {
736
- preset: settings.get("statusLine.preset"),
737
- leftSegments: settings.get("statusLine.leftSegments"),
738
- rightSegments: settings.get("statusLine.rightSegments"),
739
- separator: settings.get("statusLine.separator"),
740
- showHookStatus: settings.get("statusLine.showHookStatus"),
741
- sessionAccent: settings.get("statusLine.sessionAccent"),
742
- segmentOptions: settings.get("statusLine.segmentOptions"),
743
- };
744
- this.ctx.statusLine.updateSettings(statusLineSettings);
741
+ this.ctx.statusLine.updateSettings(buildStatusLineSettings(settings));
745
742
  this.ctx.updateEditorTopBorder();
746
743
  this.ctx.ui.requestRender();
747
744
  break;
@@ -71,7 +71,6 @@ import { normalizeLocalScheme } from "../tools/path-utils";
71
71
  import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
72
72
  import { formatPhaseDisplayName } from "../tools/todo-write";
73
73
  import { ToolError } from "../tools/tool-errors";
74
-
75
74
  import type { EventBus } from "../utils/event-bus";
76
75
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
77
76
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
@@ -103,6 +102,7 @@ import { JobsObserver } from "./jobs-observer";
103
102
  import { OAuthManualInputManager } from "./oauth-manual-input";
104
103
  import { SessionObserverRegistry } from "./session-observer-registry";
105
104
  import { interruptHint } from "./shared";
105
+ import { shouldShowExtensionCommand } from "./slash-command-visibility";
106
106
  import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
107
107
  import type { Theme } from "./theme/theme";
108
108
  import {
@@ -321,6 +321,7 @@ export class InteractiveMode implements InteractiveModeContext {
321
321
  }
322
322
  autoCompactionEscapeHandler?: () => void;
323
323
  retryEscapeHandler?: () => void;
324
+ retryEscapePrimed = false;
324
325
  retryCountdownTimer?: NodeJS.Timeout;
325
326
  unsubscribe?: () => void;
326
327
  onInputCallback?: (input: SubmittedUserInput) => void;
@@ -422,7 +423,7 @@ export class InteractiveMode implements InteractiveModeContext {
422
423
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
423
424
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
424
425
  this.editor.onAutocompleteCancel = () => {
425
- this.ui.requestRender(true);
426
+ this.ui.requestRender();
426
427
  };
427
428
  this.editor.onAutocompleteUpdate = () => {
428
429
  this.ui.requestRender();
@@ -432,7 +433,7 @@ export class InteractiveMode implements InteractiveModeContext {
432
433
  this.#syncEditorMaxHeight();
433
434
  this.updateEditorChrome();
434
435
  this.editor.invalidate();
435
- this.ui.requestRender(true, "resize");
436
+ this.ui.requestResizeRender();
436
437
  };
437
438
  process.stdout.on("resize", this.#resizeHandler);
438
439
  try {
@@ -452,13 +453,16 @@ export class InteractiveMode implements InteractiveModeContext {
452
453
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
453
454
 
454
455
  const builtinCommandNames = new Set(BUILTIN_SLASH_COMMANDS.map(c => c.name));
456
+ const activeProvider = this.session.model?.provider;
455
457
  const hookCommands: SlashCommand[] = (
456
458
  this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []
457
- ).map(cmd => ({
458
- name: cmd.name,
459
- description: cmd.description ?? "(hook command)",
460
- getArgumentCompletions: cmd.getArgumentCompletions,
461
- }));
459
+ )
460
+ .filter(cmd => shouldShowExtensionCommand(cmd.name, activeProvider))
461
+ .map(cmd => ({
462
+ name: cmd.name,
463
+ description: cmd.description ?? "(hook command)",
464
+ getArgumentCompletions: cmd.getArgumentCompletions,
465
+ }));
462
466
 
463
467
  // Convert custom commands (TypeScript) to SlashCommand format
464
468
  const customCommands: SlashCommand[] = this.session.customCommands.map(loaded => ({
@@ -527,6 +531,14 @@ export class InteractiveMode implements InteractiveModeContext {
527
531
  }
528
532
 
529
533
  if (!startupQuiet) {
534
+ const getWelcomeReservedBottomRows = (width: number): number =>
535
+ [
536
+ this.statusLine,
537
+ this.hookWidgetContainerAbove,
538
+ this.editorContainer,
539
+ this.hookWidgetContainerBelow,
540
+ ].reduce((rows, component) => rows + component.render(width).length, 0);
541
+
530
542
  // Add welcome header
531
543
  this.#welcomeComponent = new WelcomeComponent(
532
544
  this.#version,
@@ -535,30 +547,16 @@ export class InteractiveMode implements InteractiveModeContext {
535
547
  recentSessions,
536
548
  this.#getWelcomeLspServers(),
537
549
  welcomeLogoMode,
550
+ {
551
+ getViewportRows: () => this.ui.terminal.rows,
552
+ getReservedBottomRows: getWelcomeReservedBottomRows,
553
+ changelogMarkdown: this.#changelogMarkdown,
554
+ collapseChangelog: settings.get("collapseChangelog"),
555
+ },
538
556
  );
539
557
 
540
- // Setup UI layout
541
- this.ui.addChild(new Spacer(1));
542
558
  this.ui.addChild(this.#welcomeComponent);
543
- this.ui.addChild(new Spacer(1));
544
559
  this.#welcomeComponent.playIntro(() => this.ui.requestRender());
545
-
546
- // Add changelog if provided
547
- if (this.#changelogMarkdown) {
548
- this.ui.addChild(new DynamicBorder());
549
- if (settings.get("collapseChangelog")) {
550
- const versionMatch = this.#changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
551
- const latestVersion = versionMatch ? versionMatch[1] : this.#version;
552
- const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
553
- this.ui.addChild(new Text(condensedText, 1, 0));
554
- } else {
555
- this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
556
- this.ui.addChild(new Spacer(1));
557
- this.ui.addChild(new Markdown(this.#changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
558
- this.ui.addChild(new Spacer(1));
559
- }
560
- this.ui.addChild(new DynamicBorder());
561
- }
562
560
  }
563
561
 
564
562
  this.ui.addChild(this.chatContainer);
@@ -1960,6 +1958,8 @@ export class InteractiveMode implements InteractiveModeContext {
1960
1958
  this.loadingAnimation.stop();
1961
1959
  this.loadingAnimation = undefined;
1962
1960
  }
1961
+ this.#welcomeComponent?.dispose();
1962
+ this.#welcomeComponent = undefined;
1963
1963
  this.#cleanupMicAnimation();
1964
1964
  this.#cancelGoalContinuation();
1965
1965
  if (this.#sttController) {
@@ -2035,7 +2035,9 @@ export class InteractiveMode implements InteractiveModeContext {
2035
2035
  const sessionId = this.sessionManager.getSessionId();
2036
2036
  const sessionFile = this.sessionManager.getSessionFile();
2037
2037
  if (sessionId && sessionFile) {
2038
- process.stderr.write(`\n${chalk.dim(`Resume this session with ${APP_NAME} --resume ${sessionId}`)}\n`);
2038
+ process.stderr.write(
2039
+ `\n${chalk.dim("Resume this session with:")}\n${chalk.dim(`${APP_NAME} --resume ${sessionId}`)}\n`,
2040
+ );
2039
2041
  }
2040
2042
 
2041
2043
  await postmortem.quit(0);
@@ -2071,7 +2073,7 @@ export class InteractiveMode implements InteractiveModeContext {
2071
2073
  nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
2072
2074
  nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
2073
2075
  nextEditor.onAutocompleteCancel = () => {
2074
- this.ui.requestRender(true);
2076
+ this.ui.requestRender();
2075
2077
  };
2076
2078
  nextEditor.onAutocompleteUpdate = () => {
2077
2079
  this.ui.requestRender();
@@ -2311,6 +2313,10 @@ export class InteractiveMode implements InteractiveModeContext {
2311
2313
  this.#commandController.handleHotkeysCommand();
2312
2314
  }
2313
2315
 
2316
+ handleHelpCommand(): void {
2317
+ this.#commandController.handleHelpCommand();
2318
+ }
2319
+
2314
2320
  handleToolsCommand(): void {
2315
2321
  this.#commandController.handleToolsCommand();
2316
2322
  }
@@ -29,6 +29,8 @@ interface PromptActionAutocompleteOptions {
29
29
  copyCurrentLine: () => void;
30
30
  copyPrompt: () => void;
31
31
  pasteImage: () => void;
32
+ newSession: () => void;
33
+ showHelp: () => void;
32
34
  scrollTmuxToPreviousUserInput: () => void;
33
35
  undo: (prefix: string) => void;
34
36
  moveCursorToMessageEnd: () => void;
@@ -101,6 +103,12 @@ function mergeAutocompleteSuggestions(
101
103
 
102
104
  return { items, prefix: primary.prefix };
103
105
  }
106
+ const ADVANCED_SLASH_COMMAND_PRIORITIES = new Map<string, number>([["grok-build-usage", -100]]);
107
+
108
+ function getSlashCommandPriority(command: SlashCommand | undefined, item: AutocompleteItem): number {
109
+ if (command?.priority !== undefined) return command.priority;
110
+ return ADVANCED_SLASH_COMMAND_PRIORITIES.get(item.value) ?? 0;
111
+ }
104
112
 
105
113
  function sortSlashCommandSuggestions(
106
114
  suggestions: { items: AutocompleteItem[]; prefix: string } | null,
@@ -123,7 +131,7 @@ function sortSlashCommandSuggestions(
123
131
  index,
124
132
  commandIndex,
125
133
  matchRank: getSlashCommandMatchRank(query, lowerName),
126
- priority: command?.priority ?? 0,
134
+ priority: getSlashCommandPriority(command, item),
127
135
  score: Math.max(nameScore, descScore),
128
136
  };
129
137
  })
@@ -358,6 +366,20 @@ export function createPromptActionAutocompleteProvider(
358
366
  ): PromptActionAutocompleteProvider {
359
367
  const editorKeybindings = getKeybindings();
360
368
  const actions: PromptActionDefinition[] = [
369
+ {
370
+ id: "new-session",
371
+ label: "Start new session",
372
+ description: formatKeyHints(options.keybindings.getKeys("app.session.new")) || "/new",
373
+ keywords: ["new", "session", "fresh", "clear", "start", "conversation"],
374
+ execute: options.newSession,
375
+ },
376
+ {
377
+ id: "help",
378
+ label: "Open command help",
379
+ description: "/help",
380
+ keywords: ["help", "commands", "command", "palette", "shortcuts", "beginner"],
381
+ execute: options.showHelp,
382
+ },
361
383
  {
362
384
  id: "copy-line",
363
385
  label: "Copy current line",
@@ -435,6 +435,7 @@ export interface RpcJsonSchema {
435
435
  items?: RpcJsonSchema;
436
436
  minLength?: number;
437
437
  maxLength?: number;
438
+ pattern?: string;
438
439
  minItems?: number;
439
440
  maxItems?: number;
440
441
  uniqueItems?: boolean;