@kolisachint/hoocode-agent 0.4.107 → 0.4.108

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 (26) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/modes/interactive/bash-execution-controller.d.ts +34 -0
  3. package/dist/modes/interactive/bash-execution-controller.d.ts.map +1 -0
  4. package/dist/modes/interactive/bash-execution-controller.js +98 -0
  5. package/dist/modes/interactive/bash-execution-controller.js.map +1 -0
  6. package/dist/modes/interactive/interactive-mode.d.ts +5 -45
  7. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  8. package/dist/modes/interactive/interactive-mode.js +91 -779
  9. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  10. package/dist/modes/interactive/login-controller.d.ts +50 -0
  11. package/dist/modes/interactive/login-controller.d.ts.map +1 -0
  12. package/dist/modes/interactive/login-controller.js +352 -0
  13. package/dist/modes/interactive/login-controller.js.map +1 -0
  14. package/dist/modes/interactive/message-queue-controller.d.ts +49 -0
  15. package/dist/modes/interactive/message-queue-controller.d.ts.map +1 -0
  16. package/dist/modes/interactive/message-queue-controller.js +184 -0
  17. package/dist/modes/interactive/message-queue-controller.js.map +1 -0
  18. package/dist/modes/interactive/model-controller.d.ts +43 -0
  19. package/dist/modes/interactive/model-controller.d.ts.map +1 -0
  20. package/dist/modes/interactive/model-controller.js +184 -0
  21. package/dist/modes/interactive/model-controller.js.map +1 -0
  22. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  23. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  24. package/examples/extensions/sandbox/package.json +1 -1
  25. package/examples/extensions/with-deps/package.json +1 -1
  26. package/package.json +4 -4
@@ -7,15 +7,12 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { createCompactionSummaryMessage } from "@kolisachint/hoocode-agent-core";
10
- import { getProviders, } from "@kolisachint/hoocode-ai";
11
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, } from "@kolisachint/hoocode-tui";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TUI, } from "@kolisachint/hoocode-tui";
12
11
  import { spawnSync } from "child_process";
13
- import { APP_NAME, APP_TITLE, getAuthPath, getDocsPath, VERSION } from "../../config.js";
12
+ import { APP_NAME, APP_TITLE, VERSION } from "../../config.js";
14
13
  import { parseSkillBlock } from "../../core/agent-session.js";
15
14
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
16
15
  import { KeybindingsManager } from "../../core/keybindings.js";
17
- import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
18
- import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
19
16
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
20
17
  import { SessionManager } from "../../core/session-manager.js";
21
18
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
@@ -26,6 +23,7 @@ import { parseGitUrl } from "../../utils/git.js";
26
23
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
27
24
  import { ensureTool } from "../../utils/tools-manager.js";
28
25
  import { checkForNewHooCodeVersion } from "../../utils/version-check.js";
26
+ import { BashExecutionController } from "./bash-execution-controller.js";
29
27
  import { CommandExecutor } from "./command-executor.js";
30
28
  import { AssistantMessageComponent } from "./components/assistant-message.js";
31
29
  import { BashExecutionComponent } from "./components/bash-execution.js";
@@ -35,13 +33,8 @@ import { CountdownTimer } from "./components/countdown-timer.js";
35
33
  import { CustomEditor } from "./components/custom-editor.js";
36
34
  import { CustomMessageComponent } from "./components/custom-message.js";
37
35
  import { DynamicBorder } from "./components/dynamic-border.js";
38
- import { ExtensionSelectorComponent } from "./components/extension-selector.js";
39
36
  import { FooterComponent } from "./components/footer.js";
40
- import { keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
41
- import { LoginDialogComponent } from "./components/login-dialog.js";
42
- import { ModelSelectorComponent } from "./components/model-selector.js";
43
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
44
- import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
37
+ import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
45
38
  import { SessionSelectorComponent } from "./components/session-selector.js";
46
39
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
47
40
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
@@ -52,11 +45,15 @@ import { UserMessageComponent } from "./components/user-message.js";
52
45
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
53
46
  import { ExtensionChrome } from "./extension-chrome.js";
54
47
  import { ExtensionDialogs } from "./extension-dialogs.js";
48
+ import { LoginController } from "./login-controller.js";
49
+ import { MessageQueueController } from "./message-queue-controller.js";
50
+ import { ModelController } from "./model-controller.js";
55
51
  import { ExpandableText, formatDisplayPath, isExpandable, showLoadedResources as renderLoadedResources, } from "./resource-display.js";
56
52
  import { checkForPackageUpdates, checkTmuxKeyboardSetup, getChangelogForDisplay } from "./startup-checks.js";
57
53
  import { TeamFocusController } from "./team-focus.js";
58
54
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
59
55
  import { VoiceController } from "./voice/voice-controller.js";
56
+ /** Interface for components that can be expanded/collapsed */
60
57
  const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
61
58
  function isDeadTerminalError(error) {
62
59
  if (!error || typeof error !== "object" || !("code" in error)) {
@@ -65,27 +62,6 @@ function isDeadTerminalError(error) {
65
62
  const code = error.code;
66
63
  return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
67
64
  }
68
- const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth: billed per token as extra usage, not plan limits.";
69
- function isAnthropicSubscriptionAuthKey(apiKey) {
70
- return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
71
- }
72
- function isUnknownModel(model) {
73
- return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown";
74
- }
75
- function hasDefaultModelProvider(providerId) {
76
- return providerId in defaultModelPerProvider;
77
- }
78
- const BEDROCK_PROVIDER_ID = "amazon-bedrock";
79
- const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
80
- export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
81
- if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
82
- return true;
83
- }
84
- if (builtInProviderIds.has(providerId)) {
85
- return false;
86
- }
87
- return !oauthProviderIds.has(providerId);
88
- }
89
65
  export class InteractiveMode {
90
66
  options;
91
67
  runtimeHost;
@@ -119,7 +95,6 @@ export class InteractiveMode {
119
95
  lastEscapeTime = 0;
120
96
  changelogMarkdown = undefined;
121
97
  startupNoticesShown = false;
122
- anthropicSubscriptionWarningShown = false;
123
98
  // Status line tracking (for mutating immediately-sequential status updates)
124
99
  lastStatusSpacer = undefined;
125
100
  lastStatusText = undefined;
@@ -141,10 +116,6 @@ export class InteractiveMode {
141
116
  signalCleanupHandlers = [];
142
117
  // Track if editor is in bash mode (text starts with !)
143
118
  isBashMode = false;
144
- // Track current bash execution component
145
- bashComponent = undefined;
146
- // Track pending bash components (shown in pending area, moved to chat on submit)
147
- pendingBashComponents = [];
148
119
  // Auto-compaction state
149
120
  autoCompactionLoader = undefined;
150
121
  autoCompactionEscapeHandler;
@@ -152,8 +123,6 @@ export class InteractiveMode {
152
123
  retryLoader = undefined;
153
124
  retryCountdown = undefined;
154
125
  retryEscapeHandler;
155
- // Messages queued while compaction is running
156
- compactionQueuedMessages = [];
157
126
  // Shutdown state
158
127
  shutdownRequested = false;
159
128
  // Extension UI state
@@ -167,6 +136,14 @@ export class InteractiveMode {
167
136
  taskPanel;
168
137
  // hooteams team client (--team): steering + attach share its single SSE stream
169
138
  teamFocus;
139
+ // Bash command execution (the `!cmd` prompt mode)
140
+ bashExecution;
141
+ // Message queueing (compaction queue + pending-messages display)
142
+ messageQueue;
143
+ // Model selection (/model, /models, cycle keys)
144
+ modelController;
145
+ // Auth / login flows (/login, /logout)
146
+ loginController;
170
147
  // Header container that holds the built-in or custom header
171
148
  headerContainer;
172
149
  // Built-in header (logo + keybinding hints + changelog)
@@ -223,9 +200,9 @@ export class InteractiveMode {
223
200
  rebuildChatFromMessages: () => self.rebuildChatFromMessages(),
224
201
  getMarkdownThemeWithSettings: () => self.getMarkdownThemeWithSettings(),
225
202
  stopLoadingAnimation: () => self.stopLoadingAnimation(),
226
- findExactModelMatch: (searchTerm) => self.findExactModelMatch(searchTerm),
227
- maybeWarnAboutAnthropicSubscriptionAuth: (model) => self.maybeWarnAboutAnthropicSubscriptionAuth(model),
228
- showModelSelector: (searchTerm) => self.showModelSelector(searchTerm),
203
+ findExactModelMatch: (searchTerm) => self.modelController.findExactModelMatch(searchTerm),
204
+ maybeWarnAboutAnthropicSubscriptionAuth: (model) => self.modelController.maybeWarnAboutAnthropicSubscriptionAuth(model),
205
+ showModelSelector: (searchTerm) => self.modelController.showModelSelector(searchTerm),
229
206
  showExtensionConfirm: (title, message) => self.dialogs.confirm(title, message),
230
207
  promptForMissingSessionCwd: (error) => self.promptForMissingSessionCwd(error),
231
208
  handleFatalRuntimeError: (prefix, error) => self.handleFatalRuntimeError(prefix, error),
@@ -316,6 +293,53 @@ export class InteractiveMode {
316
293
  sendEditorInput: (data) => this.editor.handleInput(data),
317
294
  showError: (message) => this.showError(message),
318
295
  });
296
+ const self = this;
297
+ this.bashExecution = new BashExecutionController({
298
+ get session() {
299
+ return self.session;
300
+ },
301
+ ui: this.ui,
302
+ pendingMessagesContainer: this.pendingMessagesContainer,
303
+ chatContainer: this.chatContainer,
304
+ showError: (message) => this.showError(message),
305
+ });
306
+ this.messageQueue = new MessageQueueController({
307
+ get session() {
308
+ return self.session;
309
+ },
310
+ getEditor: () => this.editor,
311
+ pendingMessagesContainer: this.pendingMessagesContainer,
312
+ showStatus: (message) => this.showStatus(message),
313
+ showError: (message) => this.showError(message),
314
+ });
315
+ this.modelController = new ModelController({
316
+ ui: this.ui,
317
+ get session() {
318
+ return self.session;
319
+ },
320
+ showSelector: (create) => this.showSelector(create),
321
+ showStatus: (message) => this.showStatus(message),
322
+ showError: (message) => this.showError(message),
323
+ showWarning: (message) => this.showWarning(message),
324
+ updateEditorBorderColor: () => this.updateEditorBorderColor(),
325
+ invalidateFooter: () => this.footer.invalidate(),
326
+ setAvailableProviderCount: (count) => this.footerDataProvider.setAvailableProviderCount(count),
327
+ });
328
+ this.loginController = new LoginController({
329
+ ui: this.ui,
330
+ get session() {
331
+ return self.session;
332
+ },
333
+ getEditor: () => this.editor,
334
+ editorContainer: this.editorContainer,
335
+ showSelector: (create) => this.showSelector(create),
336
+ showStatus: (message) => this.showStatus(message),
337
+ showError: (message) => this.showError(message),
338
+ updateAvailableProviderCount: () => this.modelController.updateAvailableProviderCount(),
339
+ updateEditorBorderColor: () => this.updateEditorBorderColor(),
340
+ invalidateFooter: () => this.footer.invalidate(),
341
+ maybeWarnAboutAnthropicSubscriptionAuth: (model) => this.modelController.maybeWarnAboutAnthropicSubscriptionAuth(model),
342
+ });
319
343
  this.taskPanel.onNudge = (role) => this.teamFocus.showNudgeInput(role);
320
344
  this.taskPanel.onAttach = (role) => this.teamFocus.showAttach(role);
321
345
  this.taskPanel.onExitFocus = () => this.teamFocus.exitFocus();
@@ -563,7 +587,7 @@ export class InteractiveMode {
563
587
  this.ui.requestRender();
564
588
  });
565
589
  // Initialize available provider count for footer display
566
- await this.updateAvailableProviderCount();
590
+ await this.modelController.updateAvailableProviderCount();
567
591
  }
568
592
  /**
569
593
  * Update terminal title with session name and cwd.
@@ -614,7 +638,7 @@ export class InteractiveMode {
614
638
  if (modelFallbackMessage) {
615
639
  this.showWarning(modelFallbackMessage);
616
640
  }
617
- void this.maybeWarnAboutAnthropicSubscriptionAuth();
641
+ void this.modelController.maybeWarnAboutAnthropicSubscriptionAuth();
618
642
  // Process initial messages
619
643
  if (initialMessage) {
620
644
  try {
@@ -733,7 +757,7 @@ export class InteractiveMode {
733
757
  this.editor.setText(result.editorText);
734
758
  }
735
759
  this.showStatus("Navigated to selected point");
736
- void this.flushCompactionQueue({ willRetry: false });
760
+ void this.messageQueue.flushCompactionQueue({ willRetry: false });
737
761
  return { cancelled: false };
738
762
  },
739
763
  switchSession: async (sessionPath, options) => {
@@ -782,7 +806,7 @@ export class InteractiveMode {
782
806
  this.applyRuntimeSettings();
783
807
  await this.bindCurrentSessionExtensions();
784
808
  this.subscribeToAgent();
785
- await this.updateAvailableProviderCount();
809
+ await this.modelController.updateAvailableProviderCount();
786
810
  this.updateEditorBorderColor();
787
811
  this.updateTerminalTitle();
788
812
  }
@@ -796,7 +820,7 @@ export class InteractiveMode {
796
820
  renderCurrentSessionState() {
797
821
  this.chatContainer.clear();
798
822
  this.pendingMessagesContainer.clear();
799
- this.compactionQueuedMessages = [];
823
+ this.messageQueue.resetCompactionQueue();
800
824
  this.streamingComponent = undefined;
801
825
  this.streamingMessage = undefined;
802
826
  this.pendingTools.clear();
@@ -1116,7 +1140,7 @@ export class InteractiveMode {
1116
1140
  // so they work correctly regardless of which editor is active
1117
1141
  this.defaultEditor.onEscape = () => {
1118
1142
  if (this.session.isStreaming) {
1119
- this.restoreQueuedMessagesToEditor({ abort: true });
1143
+ this.messageQueue.restoreQueuedMessagesToEditor({ abort: true });
1120
1144
  }
1121
1145
  else if (this.session.isBashRunning) {
1122
1146
  this.session.abortBash();
@@ -1151,11 +1175,11 @@ export class InteractiveMode {
1151
1175
  this.defaultEditor.onCtrlD = () => this.handleCtrlD();
1152
1176
  this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
1153
1177
  this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
1154
- this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
1155
- this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
1178
+ this.defaultEditor.onAction("app.model.cycleForward", () => this.modelController.cycleModel("forward"));
1179
+ this.defaultEditor.onAction("app.model.cycleBackward", () => this.modelController.cycleModel("backward"));
1156
1180
  // Global debug handler on TUI (works regardless of focus)
1157
1181
  this.ui.onDebug = () => this.commandExecutor.handleDebug();
1158
- this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
1182
+ this.defaultEditor.onAction("app.model.select", () => this.modelController.showModelSelector());
1159
1183
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1160
1184
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1161
1185
  this.defaultEditor.onAction("app.tasks.cycleView", () => {
@@ -1231,7 +1255,7 @@ export class InteractiveMode {
1231
1255
  "/scoped-models": {
1232
1256
  run: async () => {
1233
1257
  clearEditor();
1234
- await this.showModelsSelector();
1258
+ await this.modelController.showModelsSelector();
1235
1259
  },
1236
1260
  },
1237
1261
  "/model": {
@@ -1313,13 +1337,13 @@ export class InteractiveMode {
1313
1337
  },
1314
1338
  "/login": {
1315
1339
  run: () => {
1316
- this.showOAuthSelector("login");
1340
+ this.loginController.showOAuthSelector("login");
1317
1341
  clearEditor();
1318
1342
  },
1319
1343
  },
1320
1344
  "/logout": {
1321
1345
  run: () => {
1322
- this.showOAuthSelector("logout");
1346
+ this.loginController.showOAuthSelector("logout");
1323
1347
  clearEditor();
1324
1348
  },
1325
1349
  },
@@ -1398,7 +1422,7 @@ export class InteractiveMode {
1398
1422
  return;
1399
1423
  }
1400
1424
  this.editor.addToHistory?.(text);
1401
- await this.handleBashCommand(command, isExcluded);
1425
+ await this.bashExecution.handleBashCommand(command, isExcluded);
1402
1426
  this.isBashMode = false;
1403
1427
  this.updateEditorBorderColor();
1404
1428
  return;
@@ -1406,13 +1430,13 @@ export class InteractiveMode {
1406
1430
  }
1407
1431
  // Queue input during compaction (extension commands execute immediately)
1408
1432
  if (this.session.isCompacting) {
1409
- if (this.isExtensionCommand(text)) {
1433
+ if (this.messageQueue.isExtensionCommand(text)) {
1410
1434
  this.editor.addToHistory?.(text);
1411
1435
  this.editor.setText("");
1412
1436
  await this.session.prompt(text);
1413
1437
  }
1414
1438
  else {
1415
- this.queueCompactionMessage(text, "steer");
1439
+ this.messageQueue.queueCompactionMessage(text, "steer");
1416
1440
  }
1417
1441
  return;
1418
1442
  }
@@ -1422,13 +1446,13 @@ export class InteractiveMode {
1422
1446
  this.editor.addToHistory?.(text);
1423
1447
  this.editor.setText("");
1424
1448
  await this.session.prompt(text, { streamingBehavior: "steer" });
1425
- this.updatePendingMessagesDisplay();
1449
+ this.messageQueue.updatePendingMessagesDisplay();
1426
1450
  this.ui.requestRender();
1427
1451
  return;
1428
1452
  }
1429
1453
  // Normal message submission
1430
1454
  // First, move any pending bash components to chat
1431
- this.flushPendingBashComponents();
1455
+ this.bashExecution.flushPendingBashComponents();
1432
1456
  if (this.onInputCallback) {
1433
1457
  this.onInputCallback(text);
1434
1458
  }
@@ -1473,7 +1497,7 @@ export class InteractiveMode {
1473
1497
  this.ui.requestRender();
1474
1498
  break;
1475
1499
  case "queue_update":
1476
- this.updatePendingMessagesDisplay();
1500
+ this.messageQueue.updatePendingMessagesDisplay();
1477
1501
  this.ui.requestRender();
1478
1502
  break;
1479
1503
  case "session_info_changed":
@@ -1499,7 +1523,7 @@ export class InteractiveMode {
1499
1523
  // message can arrive while a subagent is still running.
1500
1524
  taskStore.reset();
1501
1525
  this.addMessageToChat(event.message);
1502
- this.updatePendingMessagesDisplay();
1526
+ this.messageQueue.updatePendingMessagesDisplay();
1503
1527
  this.ui.requestRender();
1504
1528
  }
1505
1529
  else if (event.message.role === "assistant") {
@@ -1680,7 +1704,7 @@ export class InteractiveMode {
1680
1704
  this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
1681
1705
  }
1682
1706
  }
1683
- void this.flushCompactionQueue({ willRetry: event.willRetry });
1707
+ void this.messageQueue.flushCompactionQueue({ willRetry: event.willRetry });
1684
1708
  this.ui.requestRender();
1685
1709
  break;
1686
1710
  }
@@ -2055,13 +2079,13 @@ export class InteractiveMode {
2055
2079
  return;
2056
2080
  // Queue input during compaction (extension commands execute immediately)
2057
2081
  if (this.session.isCompacting) {
2058
- if (this.isExtensionCommand(text)) {
2082
+ if (this.messageQueue.isExtensionCommand(text)) {
2059
2083
  this.editor.addToHistory?.(text);
2060
2084
  this.editor.setText("");
2061
2085
  await this.session.prompt(text);
2062
2086
  }
2063
2087
  else {
2064
- this.queueCompactionMessage(text, "followUp");
2088
+ this.messageQueue.queueCompactionMessage(text, "followUp");
2065
2089
  }
2066
2090
  return;
2067
2091
  }
@@ -2071,7 +2095,7 @@ export class InteractiveMode {
2071
2095
  this.editor.addToHistory?.(text);
2072
2096
  this.editor.setText("");
2073
2097
  await this.session.prompt(text, { streamingBehavior: "followUp" });
2074
- this.updatePendingMessagesDisplay();
2098
+ this.messageQueue.updatePendingMessagesDisplay();
2075
2099
  this.ui.requestRender();
2076
2100
  }
2077
2101
  // If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
@@ -2081,7 +2105,7 @@ export class InteractiveMode {
2081
2105
  }
2082
2106
  }
2083
2107
  handleDequeue() {
2084
- const restored = this.restoreQueuedMessagesToEditor();
2108
+ const restored = this.messageQueue.restoreQueuedMessagesToEditor();
2085
2109
  if (restored === 0) {
2086
2110
  this.showStatus("No queued messages to restore");
2087
2111
  }
@@ -2121,25 +2145,6 @@ export class InteractiveMode {
2121
2145
  this.showStatus(`Thinking level: ${newLevel}`);
2122
2146
  }
2123
2147
  }
2124
- async cycleModel(direction) {
2125
- try {
2126
- const result = await this.session.cycleModel(direction);
2127
- if (result === undefined) {
2128
- const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
2129
- this.showStatus(msg);
2130
- }
2131
- else {
2132
- this.footer.invalidate();
2133
- this.updateEditorBorderColor();
2134
- const thinkingStr = result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
2135
- this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
2136
- void this.maybeWarnAboutAnthropicSubscriptionAuth(result.model);
2137
- }
2138
- }
2139
- catch (error) {
2140
- this.showError(error instanceof Error ? error.message : String(error));
2141
- }
2142
- }
2143
2148
  toggleToolOutputExpansion() {
2144
2149
  this.setToolsExpanded(!this.toolOutputExpanded);
2145
2150
  }
@@ -2252,170 +2257,6 @@ export class InteractiveMode {
2252
2257
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2253
2258
  this.ui.requestRender();
2254
2259
  }
2255
- /**
2256
- * Get all queued messages (read-only).
2257
- * Combines session queue and compaction queue.
2258
- */
2259
- getAllQueuedMessages() {
2260
- return {
2261
- steering: [
2262
- ...this.session.getSteeringMessages(),
2263
- ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
2264
- ],
2265
- followUp: [
2266
- ...this.session.getFollowUpMessages(),
2267
- ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
2268
- ],
2269
- };
2270
- }
2271
- /**
2272
- * Clear all queued messages and return their contents.
2273
- * Clears both session queue and compaction queue.
2274
- */
2275
- clearAllQueues() {
2276
- const { steering, followUp } = this.session.clearQueue();
2277
- const compactionSteering = this.compactionQueuedMessages
2278
- .filter((msg) => msg.mode === "steer")
2279
- .map((msg) => msg.text);
2280
- const compactionFollowUp = this.compactionQueuedMessages
2281
- .filter((msg) => msg.mode === "followUp")
2282
- .map((msg) => msg.text);
2283
- this.compactionQueuedMessages = [];
2284
- return {
2285
- steering: [...steering, ...compactionSteering],
2286
- followUp: [...followUp, ...compactionFollowUp],
2287
- };
2288
- }
2289
- updatePendingMessagesDisplay() {
2290
- this.pendingMessagesContainer.clear();
2291
- const { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();
2292
- if (steeringMessages.length > 0 || followUpMessages.length > 0) {
2293
- this.pendingMessagesContainer.addChild(new Spacer(1));
2294
- for (const message of steeringMessages) {
2295
- const text = theme.fg("dim", `Steering: ${message}`);
2296
- this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
2297
- }
2298
- for (const message of followUpMessages) {
2299
- const text = theme.fg("dim", `Follow-up: ${message}`);
2300
- this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
2301
- }
2302
- const dequeueHint = this.getAppKeyDisplay("app.message.dequeue");
2303
- const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
2304
- this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
2305
- }
2306
- }
2307
- restoreQueuedMessagesToEditor(options) {
2308
- const { steering, followUp } = this.clearAllQueues();
2309
- const allQueued = [...steering, ...followUp];
2310
- if (allQueued.length === 0) {
2311
- this.updatePendingMessagesDisplay();
2312
- if (options?.abort) {
2313
- this.agent.abort();
2314
- }
2315
- return 0;
2316
- }
2317
- const queuedText = allQueued.join("\n\n");
2318
- const currentText = options?.currentText ?? this.editor.getText();
2319
- const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
2320
- this.editor.setText(combinedText);
2321
- this.updatePendingMessagesDisplay();
2322
- if (options?.abort) {
2323
- this.agent.abort();
2324
- }
2325
- return allQueued.length;
2326
- }
2327
- queueCompactionMessage(text, mode) {
2328
- this.compactionQueuedMessages.push({ text, mode });
2329
- this.editor.addToHistory?.(text);
2330
- this.editor.setText("");
2331
- this.updatePendingMessagesDisplay();
2332
- this.showStatus("Queued message for after compaction");
2333
- }
2334
- isExtensionCommand(text) {
2335
- if (!text.startsWith("/"))
2336
- return false;
2337
- const extensionRunner = this.session.extensionRunner;
2338
- const spaceIndex = text.indexOf(" ");
2339
- const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
2340
- return !!extensionRunner.getCommand(commandName);
2341
- }
2342
- async flushCompactionQueue(options) {
2343
- if (this.compactionQueuedMessages.length === 0) {
2344
- return;
2345
- }
2346
- const queuedMessages = [...this.compactionQueuedMessages];
2347
- this.compactionQueuedMessages = [];
2348
- this.updatePendingMessagesDisplay();
2349
- const restoreQueue = (error) => {
2350
- this.session.clearQueue();
2351
- this.compactionQueuedMessages = queuedMessages;
2352
- this.updatePendingMessagesDisplay();
2353
- this.showError(`Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${error instanceof Error ? error.message : String(error)}`);
2354
- };
2355
- try {
2356
- if (options?.willRetry) {
2357
- // When retry is pending, queue messages for the retry turn
2358
- for (const message of queuedMessages) {
2359
- if (this.isExtensionCommand(message.text)) {
2360
- await this.session.prompt(message.text);
2361
- }
2362
- else if (message.mode === "followUp") {
2363
- await this.session.followUp(message.text);
2364
- }
2365
- else {
2366
- await this.session.steer(message.text);
2367
- }
2368
- }
2369
- this.updatePendingMessagesDisplay();
2370
- return;
2371
- }
2372
- // Find first non-extension-command message to use as prompt
2373
- const firstPromptIndex = queuedMessages.findIndex((message) => !this.isExtensionCommand(message.text));
2374
- if (firstPromptIndex === -1) {
2375
- // All extension commands - execute them all
2376
- for (const message of queuedMessages) {
2377
- await this.session.prompt(message.text);
2378
- }
2379
- return;
2380
- }
2381
- // Execute any extension commands before the first prompt
2382
- const preCommands = queuedMessages.slice(0, firstPromptIndex);
2383
- const firstPrompt = queuedMessages[firstPromptIndex];
2384
- const rest = queuedMessages.slice(firstPromptIndex + 1);
2385
- for (const message of preCommands) {
2386
- await this.session.prompt(message.text);
2387
- }
2388
- // Send first prompt (starts streaming)
2389
- const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
2390
- restoreQueue(error);
2391
- });
2392
- // Queue remaining messages
2393
- for (const message of rest) {
2394
- if (this.isExtensionCommand(message.text)) {
2395
- await this.session.prompt(message.text);
2396
- }
2397
- else if (message.mode === "followUp") {
2398
- await this.session.followUp(message.text);
2399
- }
2400
- else {
2401
- await this.session.steer(message.text);
2402
- }
2403
- }
2404
- this.updatePendingMessagesDisplay();
2405
- void promptPromise;
2406
- }
2407
- catch (error) {
2408
- restoreQueue(error);
2409
- }
2410
- }
2411
- /** Move pending bash components from pending area to chat */
2412
- flushPendingBashComponents() {
2413
- for (const component of this.pendingBashComponents) {
2414
- this.pendingMessagesContainer.removeChild(component);
2415
- this.chatContainer.addChild(component);
2416
- }
2417
- this.pendingBashComponents = [];
2418
- }
2419
2260
  // =========================================================================
2420
2261
  // Selectors
2421
2262
  // =========================================================================
@@ -2586,144 +2427,6 @@ export class InteractiveMode {
2586
2427
  return { component: selector, focus: selector.getSettingsList() };
2587
2428
  });
2588
2429
  }
2589
- async findExactModelMatch(searchTerm) {
2590
- const models = await this.getModelCandidates();
2591
- return findExactModelReferenceMatch(searchTerm, models);
2592
- }
2593
- async getModelCandidates() {
2594
- if (this.session.scopedModels.length > 0) {
2595
- return this.session.scopedModels.map((scoped) => scoped.model);
2596
- }
2597
- this.session.modelRegistry.refresh();
2598
- try {
2599
- return await this.session.modelRegistry.getAvailable();
2600
- }
2601
- catch {
2602
- return [];
2603
- }
2604
- }
2605
- /** Update the footer's available provider count from current model candidates */
2606
- async updateAvailableProviderCount() {
2607
- const models = await this.getModelCandidates();
2608
- const uniqueProviders = new Set(models.map((m) => m.provider));
2609
- this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
2610
- }
2611
- async maybeWarnAboutAnthropicSubscriptionAuth(model = this.session.model) {
2612
- if (this.settingsManager.getWarnings().anthropicExtraUsage === false) {
2613
- return;
2614
- }
2615
- if (this.anthropicSubscriptionWarningShown) {
2616
- return;
2617
- }
2618
- if (!model || model.provider !== "anthropic") {
2619
- return;
2620
- }
2621
- const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
2622
- if (storedCredential?.type === "oauth") {
2623
- this.anthropicSubscriptionWarningShown = true;
2624
- this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
2625
- return;
2626
- }
2627
- try {
2628
- const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
2629
- if (!isAnthropicSubscriptionAuthKey(apiKey)) {
2630
- return;
2631
- }
2632
- this.anthropicSubscriptionWarningShown = true;
2633
- this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
2634
- }
2635
- catch {
2636
- // Ignore auth lookup failures for warning-only checks.
2637
- }
2638
- }
2639
- showModelSelector(initialSearchInput) {
2640
- this.showSelector((done) => {
2641
- const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
2642
- try {
2643
- await this.session.setModel(model);
2644
- this.footer.invalidate();
2645
- this.updateEditorBorderColor();
2646
- done();
2647
- this.showStatus(`Model: ${model.id}`);
2648
- void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
2649
- }
2650
- catch (error) {
2651
- done();
2652
- this.showError(error instanceof Error ? error.message : String(error));
2653
- }
2654
- }, () => {
2655
- done();
2656
- this.ui.requestRender();
2657
- }, initialSearchInput);
2658
- return { component: selector, focus: selector };
2659
- });
2660
- }
2661
- async showModelsSelector() {
2662
- // Get all available models
2663
- this.session.modelRegistry.refresh();
2664
- const allModels = this.session.modelRegistry.getAvailable();
2665
- if (allModels.length === 0) {
2666
- this.showStatus("No models available");
2667
- return;
2668
- }
2669
- // Check if session has scoped models (from previous session-only changes or CLI --models)
2670
- const sessionScopedModels = this.session.scopedModels;
2671
- const hasSessionScope = sessionScopedModels.length > 0;
2672
- // Build enabled model IDs from session state or settings
2673
- let currentEnabledIds = null;
2674
- if (hasSessionScope) {
2675
- // Use current session's scoped models
2676
- currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
2677
- }
2678
- else {
2679
- // Fall back to settings
2680
- const patterns = this.settingsManager.getEnabledModels();
2681
- if (patterns !== undefined && patterns.length > 0) {
2682
- const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
2683
- currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
2684
- }
2685
- }
2686
- // Helper to update session's scoped models (session-only, no persist)
2687
- const updateSessionModels = async (enabledIds) => {
2688
- currentEnabledIds = enabledIds === null ? null : [...enabledIds];
2689
- if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
2690
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
2691
- this.session.setScopedModels(newScopedModels.map((sm) => ({
2692
- model: sm.model,
2693
- thinkingLevel: sm.thinkingLevel,
2694
- })));
2695
- }
2696
- else {
2697
- // All enabled or none enabled = no filter
2698
- this.session.setScopedModels([]);
2699
- }
2700
- await this.updateAvailableProviderCount();
2701
- this.ui.requestRender();
2702
- };
2703
- this.showSelector((done) => {
2704
- const selector = new ScopedModelsSelectorComponent({
2705
- allModels,
2706
- enabledModelIds: currentEnabledIds,
2707
- }, {
2708
- onChange: async (enabledIds) => {
2709
- await updateSessionModels(enabledIds);
2710
- },
2711
- onPersist: (enabledIds) => {
2712
- // Persist to settings
2713
- const newPatterns = enabledIds === null || enabledIds.length === allModels.length
2714
- ? undefined // All enabled = clear filter
2715
- : enabledIds;
2716
- this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
2717
- this.showStatus("Model selection saved to settings");
2718
- },
2719
- onCancel: () => {
2720
- done();
2721
- this.ui.requestRender();
2722
- },
2723
- });
2724
- return { component: selector, focus: selector };
2725
- });
2726
- }
2727
2430
  showUserMessageSelector() {
2728
2431
  const userMessages = this.session.getUserMessagesForForking();
2729
2432
  if (userMessages.length === 0) {
@@ -2836,7 +2539,7 @@ export class InteractiveMode {
2836
2539
  this.editor.setText(result.editorText);
2837
2540
  }
2838
2541
  this.showStatus("Navigated to selected point");
2839
- void this.flushCompactionQueue({ willRetry: false });
2542
+ void this.messageQueue.flushCompactionQueue({ willRetry: false });
2840
2543
  }
2841
2544
  catch (error) {
2842
2545
  this.showError(error instanceof Error ? error.message : String(error));
@@ -2920,326 +2623,6 @@ export class InteractiveMode {
2920
2623
  return this.handleFatalRuntimeError("Failed to resume session", error);
2921
2624
  }
2922
2625
  }
2923
- getLoginProviderOptions(authType) {
2924
- const authStorage = this.session.modelRegistry.authStorage;
2925
- const oauthProviders = authStorage.getOAuthProviders();
2926
- const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
2927
- const options = oauthProviders.map((provider) => ({
2928
- id: provider.id,
2929
- name: provider.name,
2930
- authType: "oauth",
2931
- }));
2932
- const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
2933
- for (const providerId of modelProviders) {
2934
- if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
2935
- continue;
2936
- }
2937
- options.push({
2938
- id: providerId,
2939
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
2940
- authType: "api_key",
2941
- });
2942
- }
2943
- const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
2944
- return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
2945
- }
2946
- getLogoutProviderOptions() {
2947
- const authStorage = this.session.modelRegistry.authStorage;
2948
- const options = [];
2949
- for (const providerId of authStorage.list()) {
2950
- const credential = authStorage.get(providerId);
2951
- if (!credential) {
2952
- continue;
2953
- }
2954
- options.push({
2955
- id: providerId,
2956
- name: this.session.modelRegistry.getProviderDisplayName(providerId),
2957
- authType: credential.type,
2958
- });
2959
- }
2960
- return options.sort((a, b) => a.name.localeCompare(b.name));
2961
- }
2962
- showLoginAuthTypeSelector() {
2963
- const subscriptionLabel = "Use a subscription";
2964
- const apiKeyLabel = "Use an API key";
2965
- this.showSelector((done) => {
2966
- const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
2967
- done();
2968
- const authType = option === subscriptionLabel ? "oauth" : "api_key";
2969
- this.showLoginProviderSelector(authType);
2970
- }, () => {
2971
- done();
2972
- this.ui.requestRender();
2973
- });
2974
- return { component: selector, focus: selector };
2975
- });
2976
- }
2977
- showLoginProviderSelector(authType) {
2978
- const providerOptions = this.getLoginProviderOptions(authType);
2979
- if (providerOptions.length === 0) {
2980
- this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
2981
- return;
2982
- }
2983
- this.showSelector((done) => {
2984
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
2985
- done();
2986
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
2987
- if (!providerOption) {
2988
- return;
2989
- }
2990
- if (providerOption.authType === "oauth") {
2991
- await this.showLoginDialog(providerOption.id, providerOption.name);
2992
- }
2993
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
2994
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
2995
- }
2996
- else {
2997
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
2998
- }
2999
- }, () => {
3000
- done();
3001
- this.showLoginAuthTypeSelector();
3002
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
3003
- return { component: selector, focus: selector };
3004
- });
3005
- }
3006
- async showOAuthSelector(mode) {
3007
- if (mode === "login") {
3008
- this.showLoginAuthTypeSelector();
3009
- return;
3010
- }
3011
- const providerOptions = this.getLogoutProviderOptions();
3012
- if (providerOptions.length === 0) {
3013
- this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
3014
- return;
3015
- }
3016
- this.showSelector((done) => {
3017
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
3018
- done();
3019
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
3020
- if (!providerOption) {
3021
- return;
3022
- }
3023
- try {
3024
- this.session.modelRegistry.authStorage.logout(providerOption.id);
3025
- this.session.modelRegistry.refresh();
3026
- await this.updateAvailableProviderCount();
3027
- const message = providerOption.authType === "oauth"
3028
- ? `Logged out of ${providerOption.name}`
3029
- : `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
3030
- this.showStatus(message);
3031
- }
3032
- catch (error) {
3033
- this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
3034
- }
3035
- }, () => {
3036
- done();
3037
- this.ui.requestRender();
3038
- });
3039
- return { component: selector, focus: selector };
3040
- });
3041
- }
3042
- async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
3043
- this.session.modelRegistry.refresh();
3044
- const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
3045
- let selectedModel;
3046
- let selectionError;
3047
- if (isUnknownModel(previousModel)) {
3048
- const availableModels = this.session.modelRegistry.getAvailable();
3049
- const providerModels = availableModels.filter((model) => model.provider === providerId);
3050
- if (!hasDefaultModelProvider(providerId)) {
3051
- selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
3052
- }
3053
- else if (providerModels.length === 0) {
3054
- selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
3055
- }
3056
- else {
3057
- const defaultModelId = defaultModelPerProvider[providerId];
3058
- selectedModel = providerModels.find((model) => model.id === defaultModelId);
3059
- if (!selectedModel) {
3060
- selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
3061
- }
3062
- else {
3063
- try {
3064
- await this.session.setModel(selectedModel);
3065
- }
3066
- catch (error) {
3067
- selectedModel = undefined;
3068
- const errorMessage = error instanceof Error ? error.message : String(error);
3069
- selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
3070
- }
3071
- }
3072
- }
3073
- }
3074
- await this.updateAvailableProviderCount();
3075
- this.footer.invalidate();
3076
- this.updateEditorBorderColor();
3077
- if (selectedModel) {
3078
- this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
3079
- void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
3080
- }
3081
- else {
3082
- this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
3083
- if (selectionError) {
3084
- this.showError(selectionError);
3085
- }
3086
- else {
3087
- void this.maybeWarnAboutAnthropicSubscriptionAuth();
3088
- }
3089
- }
3090
- }
3091
- showBedrockSetupDialog(providerId, providerName) {
3092
- const restoreEditor = () => {
3093
- this.editorContainer.clear();
3094
- this.editorContainer.addChild(this.editor);
3095
- this.ui.setFocus(this.editor);
3096
- this.ui.requestRender();
3097
- };
3098
- const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
3099
- dialog.showInfo([
3100
- theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
3101
- theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
3102
- theme.fg("muted", "See:"),
3103
- theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
3104
- ]);
3105
- this.editorContainer.clear();
3106
- this.editorContainer.addChild(dialog);
3107
- this.ui.setFocus(dialog);
3108
- this.ui.requestRender();
3109
- }
3110
- async showApiKeyLoginDialog(providerId, providerName) {
3111
- const previousModel = this.session.model;
3112
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3113
- // Completion handled below
3114
- }, providerName);
3115
- this.editorContainer.clear();
3116
- this.editorContainer.addChild(dialog);
3117
- this.ui.setFocus(dialog);
3118
- this.ui.requestRender();
3119
- const restoreEditor = () => {
3120
- this.editorContainer.clear();
3121
- this.editorContainer.addChild(this.editor);
3122
- this.ui.setFocus(this.editor);
3123
- this.ui.requestRender();
3124
- };
3125
- try {
3126
- const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
3127
- if (!apiKey) {
3128
- throw new Error("API key cannot be empty.");
3129
- }
3130
- this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
3131
- restoreEditor();
3132
- await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
3133
- }
3134
- catch (error) {
3135
- restoreEditor();
3136
- const errorMsg = error instanceof Error ? error.message : String(error);
3137
- if (errorMsg !== "Login cancelled") {
3138
- this.showError(`Failed to save API key for ${providerName}: ${errorMsg}`);
3139
- }
3140
- }
3141
- }
3142
- showOAuthLoginSelect(dialog, prompt) {
3143
- return new Promise((resolve) => {
3144
- const restoreDialog = () => {
3145
- this.editorContainer.clear();
3146
- this.editorContainer.addChild(dialog);
3147
- this.ui.setFocus(dialog);
3148
- this.ui.requestRender();
3149
- };
3150
- const labels = prompt.options.map((option) => option.label);
3151
- const selector = new ExtensionSelectorComponent(prompt.message, labels, (optionLabel) => {
3152
- restoreDialog();
3153
- resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
3154
- }, () => {
3155
- restoreDialog();
3156
- resolve(undefined);
3157
- });
3158
- this.editorContainer.clear();
3159
- this.editorContainer.addChild(selector);
3160
- this.ui.setFocus(selector);
3161
- this.ui.requestRender();
3162
- });
3163
- }
3164
- async showLoginDialog(providerId, providerName) {
3165
- const providerInfo = this.session.modelRegistry.authStorage
3166
- .getOAuthProviders()
3167
- .find((provider) => provider.id === providerId);
3168
- const previousModel = this.session.model;
3169
- // Providers that use callback servers (can paste redirect URL)
3170
- const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
3171
- // Create login dialog component
3172
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
3173
- // Completion handled below
3174
- }, providerName);
3175
- // Show dialog in editor container
3176
- this.editorContainer.clear();
3177
- this.editorContainer.addChild(dialog);
3178
- this.ui.setFocus(dialog);
3179
- this.ui.requestRender();
3180
- // Promise for manual code input (racing with callback server)
3181
- let manualCodeResolve;
3182
- let manualCodeReject;
3183
- const manualCodePromise = new Promise((resolve, reject) => {
3184
- manualCodeResolve = resolve;
3185
- manualCodeReject = reject;
3186
- });
3187
- // Restore editor helper
3188
- const restoreEditor = () => {
3189
- this.editorContainer.clear();
3190
- this.editorContainer.addChild(this.editor);
3191
- this.ui.setFocus(this.editor);
3192
- this.ui.requestRender();
3193
- };
3194
- try {
3195
- await this.session.modelRegistry.authStorage.login(providerId, {
3196
- onAuth: (info) => {
3197
- dialog.showAuth(info.url, info.instructions);
3198
- if (usesCallbackServer) {
3199
- // Show input for manual paste, racing with callback
3200
- dialog
3201
- .showManualInput("Paste redirect URL below, or complete login in browser:")
3202
- .then((value) => {
3203
- if (value && manualCodeResolve) {
3204
- manualCodeResolve(value);
3205
- manualCodeResolve = undefined;
3206
- }
3207
- })
3208
- .catch(() => {
3209
- if (manualCodeReject) {
3210
- manualCodeReject(new Error("Login cancelled"));
3211
- manualCodeReject = undefined;
3212
- }
3213
- });
3214
- }
3215
- else if (providerId === "github-copilot") {
3216
- // GitHub Copilot polls after onAuth
3217
- dialog.showWaiting("Waiting for browser authentication...");
3218
- }
3219
- // For Anthropic: onPrompt is called immediately after
3220
- },
3221
- onPrompt: async (prompt) => {
3222
- return dialog.showPrompt(prompt.message, prompt.placeholder);
3223
- },
3224
- onProgress: (message) => {
3225
- dialog.showProgress(message);
3226
- },
3227
- onSelect: (prompt) => this.showOAuthLoginSelect(dialog, prompt),
3228
- onManualCodeInput: () => manualCodePromise,
3229
- signal: dialog.signal,
3230
- });
3231
- // Success
3232
- restoreEditor();
3233
- await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
3234
- }
3235
- catch (error) {
3236
- restoreEditor();
3237
- const errorMsg = error instanceof Error ? error.message : String(error);
3238
- if (errorMsg !== "Login cancelled") {
3239
- this.showError(`Failed to login to ${providerName}: ${errorMsg}`);
3240
- }
3241
- }
3242
- }
3243
2626
  // =========================================================================
3244
2627
  // Command handlers
3245
2628
  // =========================================================================
@@ -3316,77 +2699,6 @@ export class InteractiveMode {
3316
2699
  this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
3317
2700
  }
3318
2701
  }
3319
- /**
3320
- * Get capitalized display string for an app keybinding action.
3321
- */
3322
- getAppKeyDisplay(action) {
3323
- return keyDisplayText(action);
3324
- }
3325
- async handleBashCommand(command, excludeFromContext = false) {
3326
- const extensionRunner = this.session.extensionRunner;
3327
- // Emit user_bash event to let extensions intercept
3328
- const eventResult = await extensionRunner.emitUserBash({
3329
- type: "user_bash",
3330
- command,
3331
- excludeFromContext,
3332
- cwd: this.sessionManager.getCwd(),
3333
- });
3334
- // If extension returned a full result, use it directly
3335
- if (eventResult?.result) {
3336
- const result = eventResult.result;
3337
- // Create UI component for display
3338
- this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
3339
- if (this.session.isStreaming) {
3340
- this.pendingMessagesContainer.addChild(this.bashComponent);
3341
- this.pendingBashComponents.push(this.bashComponent);
3342
- }
3343
- else {
3344
- this.chatContainer.addChild(this.bashComponent);
3345
- }
3346
- // Show output and complete
3347
- if (result.output) {
3348
- this.bashComponent.appendOutput(result.output);
3349
- }
3350
- this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
3351
- // Record the result in session
3352
- this.session.recordBashResult(command, result, { excludeFromContext });
3353
- this.bashComponent = undefined;
3354
- this.ui.requestRender();
3355
- return;
3356
- }
3357
- // Normal execution path (possibly with custom operations)
3358
- const isDeferred = this.session.isStreaming;
3359
- this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
3360
- if (isDeferred) {
3361
- // Show in pending area when agent is streaming
3362
- this.pendingMessagesContainer.addChild(this.bashComponent);
3363
- this.pendingBashComponents.push(this.bashComponent);
3364
- }
3365
- else {
3366
- // Show in chat immediately when agent is idle
3367
- this.chatContainer.addChild(this.bashComponent);
3368
- }
3369
- this.ui.requestRender();
3370
- try {
3371
- const result = await this.session.executeBash(command, (chunk) => {
3372
- if (this.bashComponent) {
3373
- this.bashComponent.appendOutput(chunk);
3374
- this.ui.requestRender();
3375
- }
3376
- }, { excludeFromContext, operations: eventResult?.operations });
3377
- if (this.bashComponent) {
3378
- this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true, content: result.output } : undefined, result.fullOutputPath);
3379
- }
3380
- }
3381
- catch (error) {
3382
- if (this.bashComponent) {
3383
- this.bashComponent.setComplete(undefined, false);
3384
- }
3385
- this.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
3386
- }
3387
- this.bashComponent = undefined;
3388
- this.ui.requestRender();
3389
- }
3390
2702
  async handleCompactCommand(customInstructions) {
3391
2703
  // The session is the single source of truth for whether compaction is
3392
2704
  // possible (e.g. "Already compacted", "Nothing to compact (session too