@openeryc/pi-coding-agent 0.75.54 → 0.75.56

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 (74) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +4 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/agent-session.d.ts.map +1 -1
  6. package/dist/core/agent-session.js +11 -26
  7. package/dist/core/agent-session.js.map +1 -1
  8. package/dist/core/fetch-openai-models.d.ts +25 -0
  9. package/dist/core/fetch-openai-models.d.ts.map +1 -0
  10. package/dist/core/fetch-openai-models.js +107 -0
  11. package/dist/core/fetch-openai-models.js.map +1 -0
  12. package/dist/core/mcp/client.d.ts +16 -7
  13. package/dist/core/mcp/client.d.ts.map +1 -1
  14. package/dist/core/mcp/client.js +135 -107
  15. package/dist/core/mcp/client.js.map +1 -1
  16. package/dist/core/mcp/manager.d.ts +8 -1
  17. package/dist/core/mcp/manager.d.ts.map +1 -1
  18. package/dist/core/mcp/manager.js +56 -33
  19. package/dist/core/mcp/manager.js.map +1 -1
  20. package/dist/core/mcp/types.d.ts +0 -13
  21. package/dist/core/mcp/types.d.ts.map +1 -1
  22. package/dist/core/mcp/types.js.map +1 -1
  23. package/dist/core/model-registry.d.ts +112 -0
  24. package/dist/core/model-registry.d.ts.map +1 -1
  25. package/dist/core/model-registry.js +99 -2
  26. package/dist/core/model-registry.js.map +1 -1
  27. package/dist/core/package-manager.d.ts.map +1 -1
  28. package/dist/core/package-manager.js +3 -1
  29. package/dist/core/package-manager.js.map +1 -1
  30. package/dist/core/session-manager.d.ts.map +1 -1
  31. package/dist/core/session-manager.js +1 -1
  32. package/dist/core/session-manager.js.map +1 -1
  33. package/dist/core/settings-manager.d.ts +4 -0
  34. package/dist/core/settings-manager.d.ts.map +1 -1
  35. package/dist/core/settings-manager.js +12 -0
  36. package/dist/core/settings-manager.js.map +1 -1
  37. package/dist/core/slash-commands.d.ts.map +1 -1
  38. package/dist/core/slash-commands.js +2 -2
  39. package/dist/core/slash-commands.js.map +1 -1
  40. package/dist/modes/interactive/components/index.d.ts +1 -1
  41. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  42. package/dist/modes/interactive/components/index.js +1 -1
  43. package/dist/modes/interactive/components/index.js.map +1 -1
  44. package/dist/modes/interactive/components/model-hub.d.ts +115 -0
  45. package/dist/modes/interactive/components/model-hub.d.ts.map +1 -0
  46. package/dist/modes/interactive/components/model-hub.js +753 -0
  47. package/dist/modes/interactive/components/model-hub.js.map +1 -0
  48. package/dist/modes/interactive/components/model-selector.d.ts +2 -44
  49. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  50. package/dist/modes/interactive/components/model-selector.js +2 -275
  51. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  52. package/dist/modes/interactive/interactive-mode.d.ts +22 -2
  53. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  54. package/dist/modes/interactive/interactive-mode.js +666 -557
  55. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  56. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  57. package/dist/modes/rpc/rpc-client.js +5 -1
  58. package/dist/modes/rpc/rpc-client.js.map +1 -1
  59. package/dist/utils/sleep.d.ts.map +1 -1
  60. package/dist/utils/sleep.js +7 -3
  61. package/dist/utils/sleep.js.map +1 -1
  62. package/dist/utils/tools-manager.d.ts.map +1 -1
  63. package/dist/utils/tools-manager.js.map +1 -1
  64. package/docs/models.md +20 -0
  65. package/docs/providers.md +13 -0
  66. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  67. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  68. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  69. package/examples/extensions/sandbox/package-lock.json +2 -2
  70. package/examples/extensions/sandbox/package.json +1 -1
  71. package/examples/extensions/with-deps/package-lock.json +2 -2
  72. package/examples/extensions/with-deps/package.json +1 -1
  73. package/npm-shrinkwrap.json +12 -12
  74. package/package.json +4 -4
@@ -12,6 +12,7 @@ import { spawn, spawnSync } from "child_process";
12
12
  import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getSelfUpdateCommand, getShareViewerUrl, PACKAGE_NAME, VERSION, } from "../../config.js";
13
13
  import { parseSkillBlock } from "../../core/agent-session.js";
14
14
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
15
+ import { fetchOpenAICompatibleModels, slugifyProviderId } from "../../core/fetch-openai-models.js";
15
16
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
16
17
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
17
18
  import { KeybindingsManager } from "../../core/keybindings.js";
@@ -52,9 +53,8 @@ import { FooterComponent } from "./components/footer.js";
52
53
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
53
54
  import { LoginDialogComponent } from "./components/login-dialog.js";
54
55
  import { McpSelectorComponent } from "./components/mcp-selector.js";
55
- import { ModelSelectorComponent } from "./components/model-selector.js";
56
+ import { ModelHubComponent } from "./components/model-hub.js";
56
57
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
57
- import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
58
58
  import { SessionSelectorComponent } from "./components/session-selector.js";
59
59
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
60
60
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
@@ -153,6 +153,8 @@ export class InteractiveMode {
153
153
  hideThinkingBlock = false;
154
154
  // Skill commands: command name -> skill file path
155
155
  skillCommands = new Map();
156
+ /** Slash command dispatch map */
157
+ commands = new Map();
156
158
  // Agent subscription unsubscribe function
157
159
  unsubscribe;
158
160
  signalCleanupHandlers = [];
@@ -478,6 +480,7 @@ export class InteractiveMode {
478
480
  this.ui.setFocus(this.editor);
479
481
  this.setupKeyHandlers();
480
482
  this.setupEditorSubmitHandler();
483
+ this.initCommands();
481
484
  // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
482
485
  this.ui.start();
483
486
  this.isInitialized = true;
@@ -528,23 +531,29 @@ export class InteractiveMode {
528
531
  await this.goalRunnerContinuationLoop(resumedGoal);
529
532
  }
530
533
  // Start version check asynchronously
531
- checkForNewPiVersion(this.version).then((newRelease) => {
534
+ checkForNewPiVersion(this.version)
535
+ .then((newRelease) => {
532
536
  if (newRelease) {
533
537
  this.showNewVersionNotification(newRelease);
534
538
  }
535
- });
539
+ })
540
+ .catch(() => { });
536
541
  // Start package update check asynchronously
537
- this.checkForPackageUpdates().then((updates) => {
542
+ this.checkForPackageUpdates()
543
+ .then((updates) => {
538
544
  if (updates.length > 0) {
539
545
  this.showPackageUpdateNotification(updates);
540
546
  }
541
- });
547
+ })
548
+ .catch(() => { });
542
549
  // Check tmux keyboard setup asynchronously
543
- this.checkTmuxKeyboardSetup().then((warning) => {
550
+ this.checkTmuxKeyboardSetup()
551
+ .then((warning) => {
544
552
  if (warning) {
545
553
  this.showWarning(warning);
546
554
  }
547
- });
555
+ })
556
+ .catch(() => { });
548
557
  // Show startup warnings
549
558
  const { migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages } = this.options;
550
559
  if (migratedProviders && migratedProviders.length > 0) {
@@ -1957,7 +1966,7 @@ export class InteractiveMode {
1957
1966
  this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
1958
1967
  // Global debug handler on TUI (works regardless of focus)
1959
1968
  this.ui.onDebug = () => this.handleDebugCommand();
1960
- this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
1969
+ this.defaultEditor.onAction("app.model.select", () => this.showModelHub("select"));
1961
1970
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1962
1971
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1963
1972
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
@@ -1999,6 +2008,60 @@ export class InteractiveMode {
1999
2008
  // Silently ignore clipboard errors (may not have permission, etc.)
2000
2009
  }
2001
2010
  }
2011
+ initCommands() {
2012
+ const noArg = (fn) => async (_text) => {
2013
+ this.editor.setText("");
2014
+ await fn();
2015
+ };
2016
+ this.commands.set("/settings", noArg(() => this.showSettingsSelector()));
2017
+ this.commands.set("/scoped-models", noArg(() => this.showModelHub("cycle")));
2018
+ this.commands.set("/model", async (text) => {
2019
+ const s = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
2020
+ this.editor.setText("");
2021
+ await this.handleModelCommand(s);
2022
+ });
2023
+ this.commands.set("/export", async (text) => {
2024
+ await this.handleExportCommand(text);
2025
+ this.editor.setText("");
2026
+ });
2027
+ this.commands.set("/import", async (text) => {
2028
+ await this.handleImportCommand(text);
2029
+ this.editor.setText("");
2030
+ });
2031
+ this.commands.set("/share", noArg(() => this.handleShareCommand()));
2032
+ this.commands.set("/copy", noArg(() => this.handleCopyCommand()));
2033
+ this.commands.set("/name", async (text) => {
2034
+ this.handleNameCommand(text);
2035
+ this.editor.setText("");
2036
+ });
2037
+ this.commands.set("/session", noArg(() => this.handleSessionCommand()));
2038
+ this.commands.set("/mcp", noArg(() => this.showMcpSelector()));
2039
+ this.commands.set("/skills", noArg(() => this.showSkillsSelector()));
2040
+ this.commands.set("/usage", noArg(() => this.handleUsageCommand()));
2041
+ this.commands.set("/memory", noArg(() => this.handleMemoryCommand()));
2042
+ this.commands.set("/changelog", noArg(() => this.handleChangelogCommand()));
2043
+ this.commands.set("/hotkeys", noArg(() => this.handleHotkeysCommand()));
2044
+ this.commands.set("/fork", noArg(() => this.showUserMessageSelector()));
2045
+ this.commands.set("/clone", noArg(() => this.handleCloneCommand()));
2046
+ this.commands.set("/tree", noArg(() => this.showTreeSelector()));
2047
+ this.commands.set("/login", noArg(() => this.showOAuthSelector("login")));
2048
+ this.commands.set("/logout", noArg(() => this.showOAuthSelector("logout")));
2049
+ this.commands.set("/new", noArg(() => this.handleClearCommand()));
2050
+ this.commands.set("/compact", async (text) => {
2051
+ const ci = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
2052
+ this.editor.setText("");
2053
+ await this.handleCompactCommand(ci);
2054
+ });
2055
+ this.commands.set("/cleanup", noArg(() => this.handleCleanupCommand()));
2056
+ this.commands.set("/reload", noArg(() => this.handleReloadCommand()));
2057
+ this.commands.set("/debug", noArg(() => this.handleDebugCommand()));
2058
+ this.commands.set("/arminsayshi", noArg(() => this.handleArminSaysHi()));
2059
+ this.commands.set("/dementedelves", noArg(() => this.handleDementedDelves()));
2060
+ this.commands.set("/daxnuts", noArg(() => this.handleDaxnuts()));
2061
+ this.commands.set("/resume", noArg(() => this.showSessionSelector()));
2062
+ this.commands.set("/update", noArg(() => this.handleUpdateCommand()));
2063
+ this.commands.set("/quit", noArg(() => this.shutdown()));
2064
+ }
2002
2065
  setupEditorSubmitHandler() {
2003
2066
  this.defaultEditor.onSubmit = async (text) => {
2004
2067
  text = text.trim();
@@ -2006,171 +2069,22 @@ export class InteractiveMode {
2006
2069
  return;
2007
2070
  // Add to history before handling (so up/down can recall commands too)
2008
2071
  this.editor.addToHistory?.(text);
2009
- // Handle commands
2010
- if (text === "/settings") {
2011
- this.showSettingsSelector();
2012
- this.editor.setText("");
2013
- return;
2014
- }
2015
- if (text === "/scoped-models") {
2016
- this.editor.setText("");
2017
- await this.showModelsSelector();
2018
- return;
2019
- }
2020
- if (text === "/model" || text.startsWith("/model ")) {
2021
- const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
2022
- this.editor.setText("");
2023
- await this.handleModelCommand(searchTerm);
2024
- return;
2025
- }
2026
- if (text === "/export" || text.startsWith("/export ")) {
2027
- await this.handleExportCommand(text);
2028
- this.editor.setText("");
2029
- return;
2030
- }
2031
- if (text === "/import" || text.startsWith("/import ")) {
2032
- await this.handleImportCommand(text);
2033
- this.editor.setText("");
2034
- return;
2035
- }
2036
- if (text === "/share") {
2037
- await this.handleShareCommand();
2038
- this.editor.setText("");
2039
- return;
2040
- }
2041
- if (text === "/copy") {
2042
- await this.handleCopyCommand();
2043
- this.editor.setText("");
2044
- return;
2045
- }
2046
- if (text === "/name" || text.startsWith("/name ")) {
2047
- this.handleNameCommand(text);
2048
- this.editor.setText("");
2049
- return;
2050
- }
2051
- if (text === "/goal" || text.startsWith("/goal ")) {
2072
+ // Handle commands (with special case for /goal which continues to submit)
2073
+ if (text.startsWith("/goal")) {
2052
2074
  const goal = text.replace(/^\/goal\s*/, "").trim();
2053
2075
  this.handleGoalCommand(text);
2054
- if (goal)
2055
- text = goal; // Submit goal text as the user message, not /goal prefix
2056
- // Don't return — let the normal submit flow run so the agent starts working
2057
- }
2058
- if (text === "/session") {
2059
- this.handleSessionCommand();
2060
- this.editor.setText("");
2061
- return;
2062
- }
2063
- if (text === "/mcp") {
2064
- this.editor.setText("");
2065
- this.showMcpSelector();
2066
- return;
2067
- }
2068
- if (text.startsWith("/mcp ")) {
2069
- const name = text.slice(5).trim();
2070
- this.editor.setText("");
2071
- this.showMcpSelector(name);
2072
- return;
2073
- }
2074
- if (text === "/skills") {
2075
- this.editor.setText("");
2076
- this.showSkillsSelector();
2077
- return;
2078
- }
2079
- if (text === "/usage") {
2080
- this.editor.setText("");
2081
- await this.handleUsageCommand();
2082
- return;
2083
- }
2084
- if (text === "/memory") {
2085
- this.handleMemoryCommand();
2086
- this.editor.setText("");
2087
- return;
2088
- }
2089
- if (text === "/changelog") {
2090
- this.handleChangelogCommand();
2091
- this.editor.setText("");
2092
- return;
2093
- }
2094
- if (text === "/hotkeys") {
2095
- this.handleHotkeysCommand();
2096
- this.editor.setText("");
2097
- return;
2098
- }
2099
- if (text === "/fork") {
2100
- this.showUserMessageSelector();
2101
- this.editor.setText("");
2102
- return;
2103
- }
2104
- if (text === "/clone") {
2105
- this.editor.setText("");
2106
- await this.handleCloneCommand();
2107
- return;
2108
- }
2109
- if (text === "/tree") {
2110
- this.showTreeSelector();
2111
- this.editor.setText("");
2112
- return;
2113
- }
2114
- if (text === "/login") {
2115
- this.showOAuthSelector("login");
2116
- this.editor.setText("");
2117
- return;
2118
- }
2119
- if (text === "/logout") {
2120
- this.showOAuthSelector("logout");
2121
- this.editor.setText("");
2122
- return;
2123
- }
2124
- if (text === "/new") {
2125
2076
  this.editor.setText("");
2126
- await this.handleClearCommand();
2127
- return;
2128
- }
2129
- if (text === "/compact" || text.startsWith("/compact ")) {
2130
- const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
2131
- this.editor.setText("");
2132
- await this.handleCompactCommand(customInstructions);
2133
- return;
2134
- }
2135
- if (text === "/cleanup") {
2136
- this.editor.setText("");
2137
- await this.handleCleanupCommand();
2138
- return;
2139
- }
2140
- if (text === "/reload") {
2141
- this.editor.setText("");
2142
- await this.handleReloadCommand();
2143
- return;
2144
- }
2145
- if (text === "/debug") {
2146
- this.handleDebugCommand();
2147
- this.editor.setText("");
2148
- return;
2149
- }
2150
- if (text === "/arminsayshi") {
2151
- this.handleArminSaysHi();
2152
- this.editor.setText("");
2153
- return;
2154
- }
2155
- if (text === "/dementedelves") {
2156
- this.handleDementedDelves();
2157
- this.editor.setText("");
2158
- return;
2159
- }
2160
- if (text === "/resume") {
2161
- this.showSessionSelector();
2162
- this.editor.setText("");
2163
- return;
2164
- }
2165
- if (text === "/update") {
2166
- this.editor.setText("");
2167
- await this.handleUpdateCommand();
2168
- return;
2077
+ if (goal)
2078
+ text = goal;
2169
2079
  }
2170
- if (text === "/quit") {
2171
- this.editor.setText("");
2172
- await this.shutdown();
2173
- return;
2080
+ else {
2081
+ const spaceIdx = text.indexOf(" ");
2082
+ const cmd = spaceIdx === -1 ? text : text.slice(0, spaceIdx);
2083
+ const handler = this.commands.get(cmd);
2084
+ if (handler) {
2085
+ await handler(text);
2086
+ return;
2087
+ }
2174
2088
  }
2175
2089
  // Handle bash command (! for normal, !! for excluded from context)
2176
2090
  if (text.startsWith("!")) {
@@ -2232,280 +2146,299 @@ export class InteractiveMode {
2232
2146
  this.footer.invalidate();
2233
2147
  switch (event.type) {
2234
2148
  case "agent_start":
2235
- this.pendingTools.clear();
2236
- if (this.settingsManager.getShowTerminalProgress()) {
2237
- this.ui.terminal.setProgress(true);
2238
- }
2239
- // Restore main escape handler if retry handler is still active
2240
- // (retry success event fires later, but we need main handler now)
2241
- if (this.retryEscapeHandler) {
2242
- this.defaultEditor.onEscape = this.retryEscapeHandler;
2243
- this.retryEscapeHandler = undefined;
2244
- }
2245
- if (this.retryCountdown) {
2246
- this.retryCountdown.dispose();
2247
- this.retryCountdown = undefined;
2248
- }
2249
- if (this.retryLoader) {
2250
- this.retryLoader.stop();
2251
- this.retryLoader = undefined;
2252
- }
2253
- this.stopWorkingLoader();
2254
- if (this.workingVisible) {
2255
- this.loadingAnimation = this.createWorkingLoader();
2256
- this.statusContainer.addChild(this.loadingAnimation);
2257
- }
2258
- this.ui.requestRender();
2149
+ this._handleAgentStart();
2259
2150
  break;
2260
2151
  case "queue_update":
2261
- this.updatePendingMessagesDisplay();
2262
- this.ui.requestRender();
2152
+ this._handleQueueUpdate();
2263
2153
  break;
2264
2154
  case "session_info_changed":
2265
- this.updateTerminalTitle();
2266
- this.footer.invalidate();
2267
- this.ui.requestRender();
2155
+ this._handleSessionInfoChanged();
2268
2156
  break;
2269
2157
  case "thinking_level_changed":
2270
- this.footer.invalidate();
2271
- this.updateEditorBorderColor();
2158
+ this._handleThinkingLevelChanged();
2272
2159
  break;
2273
2160
  case "message_start":
2274
- if (event.message.role === "custom") {
2275
- this.addMessageToChat(event.message);
2276
- this.ui.requestRender();
2277
- }
2278
- else if (event.message.role === "user") {
2279
- this.addMessageToChat(event.message);
2280
- this.updatePendingMessagesDisplay();
2281
- this.ui.requestRender();
2282
- }
2283
- else if (event.message.role === "assistant") {
2284
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2285
- this.streamingMessage = event.message;
2286
- this.chatContainer.addChild(this.streamingComponent);
2287
- this.streamingComponent.updateContent(this.streamingMessage);
2288
- this.ui.requestRender();
2289
- }
2161
+ this._handleMessageStart(event);
2290
2162
  break;
2291
2163
  case "message_update":
2292
- if (this.streamingComponent && event.message.role === "assistant") {
2293
- this.streamingMessage = event.message;
2294
- this.streamingComponent.updateContent(this.streamingMessage);
2295
- for (const content of this.streamingMessage.content) {
2296
- if (content.type === "toolCall") {
2297
- if (!this.pendingTools.has(content.id)) {
2298
- const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
2299
- showImages: this.settingsManager.getShowImages(),
2300
- imageWidthCells: this.settingsManager.getImageWidthCells(),
2301
- }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2302
- component.setExpanded(this.toolOutputExpanded);
2303
- this.chatContainer.addChild(component);
2304
- this.pendingTools.set(content.id, component);
2305
- }
2306
- else {
2307
- const component = this.pendingTools.get(content.id);
2308
- if (component) {
2309
- component.updateArgs(content.arguments);
2310
- }
2311
- }
2312
- }
2313
- }
2314
- this.ui.requestRender();
2315
- }
2164
+ this._handleMessageUpdate(event);
2316
2165
  break;
2317
2166
  case "message_end":
2318
- if (event.message.role === "user")
2319
- break;
2320
- if (this.streamingComponent && event.message.role === "assistant") {
2321
- this.streamingMessage = event.message;
2322
- let errorMessage;
2323
- if (this.streamingMessage.stopReason === "aborted") {
2324
- const retryAttempt = this.session.retryAttempt;
2325
- errorMessage =
2326
- retryAttempt > 0
2327
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
2328
- : "Operation aborted";
2329
- this.streamingMessage.errorMessage = errorMessage;
2330
- }
2331
- this.streamingComponent.updateContent(this.streamingMessage);
2332
- if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2333
- if (!errorMessage) {
2334
- errorMessage = this.streamingMessage.errorMessage || "Error";
2335
- }
2336
- for (const [, component] of this.pendingTools.entries()) {
2337
- component.updateResult({
2338
- content: [{ type: "text", text: errorMessage }],
2339
- isError: true,
2340
- });
2341
- }
2342
- this.pendingTools.clear();
2343
- }
2344
- else {
2345
- // Args are now complete - trigger diff computation for edit tools
2346
- for (const [, component] of this.pendingTools.entries()) {
2347
- component.setArgsComplete();
2348
- }
2349
- }
2350
- this.streamingComponent = undefined;
2351
- this.streamingMessage = undefined;
2352
- this.footer.invalidate();
2353
- }
2354
- this.ui.requestRender();
2167
+ this._handleMessageEnd(event);
2355
2168
  break;
2356
- case "tool_execution_start": {
2357
- let component = this.pendingTools.get(event.toolCallId);
2358
- if (!component) {
2359
- component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
2360
- showImages: this.settingsManager.getShowImages(),
2361
- imageWidthCells: this.settingsManager.getImageWidthCells(),
2362
- }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
2363
- component.setExpanded(this.toolOutputExpanded);
2364
- this.chatContainer.addChild(component);
2365
- this.pendingTools.set(event.toolCallId, component);
2366
- }
2367
- component.markExecutionStarted();
2368
- this.ui.requestRender();
2169
+ case "tool_execution_start":
2170
+ this._handleToolExecutionStart(event);
2369
2171
  break;
2370
- }
2371
- case "tool_execution_update": {
2372
- const component = this.pendingTools.get(event.toolCallId);
2373
- if (component) {
2374
- component.updateResult({ ...event.partialResult, isError: false }, true);
2375
- this.ui.requestRender();
2376
- }
2172
+ case "tool_execution_update":
2173
+ this._handleToolExecutionUpdate(event);
2377
2174
  break;
2378
- }
2379
- case "tool_execution_end": {
2380
- const component = this.pendingTools.get(event.toolCallId);
2381
- if (component) {
2382
- component.updateResult({ ...event.result, isError: event.isError });
2383
- this.pendingTools.delete(event.toolCallId);
2384
- this.ui.requestRender();
2385
- }
2175
+ case "tool_execution_end":
2176
+ this._handleToolExecutionEnd(event);
2386
2177
  break;
2387
- }
2388
2178
  case "agent_end":
2389
- if (this.settingsManager.getShowTerminalProgress()) {
2390
- this.ui.terminal.setProgress(false);
2391
- }
2392
- if (this.loadingAnimation) {
2393
- this.loadingAnimation.stop();
2394
- this.loadingAnimation = undefined;
2395
- this.statusContainer.clear();
2396
- }
2397
- if (this.streamingComponent) {
2398
- this.chatContainer.removeChild(this.streamingComponent);
2399
- this.streamingComponent = undefined;
2400
- this.streamingMessage = undefined;
2401
- }
2402
- this.pendingTools.clear();
2403
- await this.checkShutdownRequested();
2404
- this.ui.requestRender();
2179
+ await this._handleAgentEnd();
2405
2180
  break;
2406
- case "compaction_start": {
2407
- if (this.settingsManager.getShowTerminalProgress()) {
2408
- this.ui.terminal.setProgress(true);
2409
- }
2410
- // Keep editor active; submissions are queued during compaction.
2411
- this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
2412
- this.defaultEditor.onEscape = () => {
2413
- this.session.abortCompaction();
2414
- };
2415
- this.statusContainer.clear();
2416
- const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
2417
- const label = event.reason === "manual"
2418
- ? `Compacting context... ${cancelHint}`
2419
- : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
2420
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
2421
- this.statusContainer.addChild(this.autoCompactionLoader);
2422
- this.ui.requestRender();
2181
+ case "compaction_start":
2182
+ this._handleCompactionStart(event);
2423
2183
  break;
2424
- }
2425
- case "compaction_end": {
2426
- if (this.settingsManager.getShowTerminalProgress()) {
2427
- this.ui.terminal.setProgress(false);
2428
- }
2429
- if (this.autoCompactionEscapeHandler) {
2430
- this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
2431
- this.autoCompactionEscapeHandler = undefined;
2432
- }
2433
- if (this.autoCompactionLoader) {
2434
- this.autoCompactionLoader.stop();
2435
- this.autoCompactionLoader = undefined;
2436
- this.statusContainer.clear();
2437
- }
2438
- if (event.aborted) {
2439
- if (event.reason === "manual") {
2440
- this.showError("Compaction cancelled");
2441
- }
2442
- else {
2443
- this.showStatus("Auto-compaction cancelled");
2444
- }
2445
- }
2446
- else if (event.result) {
2447
- this.chatContainer.clear();
2448
- this.rebuildChatFromMessages();
2449
- this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
2450
- this.footer.invalidate();
2451
- }
2452
- else if (event.errorMessage) {
2453
- if (event.reason === "manual") {
2454
- this.showError(event.errorMessage);
2455
- }
2456
- else {
2457
- this.chatContainer.addChild(new Spacer(1));
2458
- this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
2459
- }
2460
- }
2461
- void this.flushCompactionQueue({ willRetry: event.willRetry });
2462
- this.ui.requestRender();
2184
+ case "compaction_end":
2185
+ this._handleCompactionEnd(event);
2463
2186
  break;
2464
- }
2465
- case "auto_retry_start": {
2466
- // Set up escape to abort retry
2467
- this.retryEscapeHandler = this.defaultEditor.onEscape;
2468
- this.defaultEditor.onEscape = () => {
2469
- this.session.abortRetry();
2470
- };
2471
- // Show retry indicator
2472
- this.statusContainer.clear();
2473
- this.retryCountdown?.dispose();
2474
- const retryMessage = (seconds) => `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`;
2475
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
2476
- this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => {
2477
- this.retryLoader?.setMessage(retryMessage(seconds));
2478
- }, () => {
2479
- this.retryCountdown = undefined;
2480
- });
2481
- this.statusContainer.addChild(this.retryLoader);
2482
- this.ui.requestRender();
2187
+ case "auto_retry_start":
2188
+ this._handleAutoRetryStart(event);
2483
2189
  break;
2484
- }
2485
- case "auto_retry_end": {
2486
- // Restore escape handler
2487
- if (this.retryEscapeHandler) {
2488
- this.defaultEditor.onEscape = this.retryEscapeHandler;
2489
- this.retryEscapeHandler = undefined;
2490
- }
2491
- if (this.retryCountdown) {
2492
- this.retryCountdown.dispose();
2493
- this.retryCountdown = undefined;
2494
- }
2495
- // Stop loader
2496
- if (this.retryLoader) {
2497
- this.retryLoader.stop();
2498
- this.retryLoader = undefined;
2499
- this.statusContainer.clear();
2500
- }
2501
- // Show error only on final failure (success shows normal response)
2502
- if (!event.success) {
2503
- this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
2504
- }
2505
- this.ui.requestRender();
2190
+ case "auto_retry_end":
2191
+ this._handleAutoRetryEnd(event);
2506
2192
  break;
2193
+ }
2194
+ }
2195
+ _handleAgentStart() {
2196
+ this.pendingTools.clear();
2197
+ if (this.settingsManager.getShowTerminalProgress()) {
2198
+ this.ui.terminal.setProgress(true);
2199
+ }
2200
+ if (this.retryEscapeHandler) {
2201
+ this.defaultEditor.onEscape = this.retryEscapeHandler;
2202
+ this.retryEscapeHandler = undefined;
2203
+ }
2204
+ if (this.retryCountdown) {
2205
+ this.retryCountdown.dispose();
2206
+ this.retryCountdown = undefined;
2207
+ }
2208
+ if (this.retryLoader) {
2209
+ this.retryLoader.stop();
2210
+ this.retryLoader = undefined;
2211
+ }
2212
+ this.stopWorkingLoader();
2213
+ if (this.workingVisible) {
2214
+ this.loadingAnimation = this.createWorkingLoader();
2215
+ this.statusContainer.addChild(this.loadingAnimation);
2216
+ }
2217
+ this.ui.requestRender();
2218
+ }
2219
+ _handleQueueUpdate() {
2220
+ this.updatePendingMessagesDisplay();
2221
+ this.ui.requestRender();
2222
+ }
2223
+ _handleSessionInfoChanged() {
2224
+ this.updateTerminalTitle();
2225
+ this.footer.invalidate();
2226
+ this.ui.requestRender();
2227
+ }
2228
+ _handleThinkingLevelChanged() {
2229
+ this.footer.invalidate();
2230
+ this.updateEditorBorderColor();
2231
+ }
2232
+ _handleMessageStart(event) {
2233
+ if (event.message.role === "custom") {
2234
+ this.addMessageToChat(event.message);
2235
+ this.ui.requestRender();
2236
+ }
2237
+ else if (event.message.role === "user") {
2238
+ this.addMessageToChat(event.message);
2239
+ this.updatePendingMessagesDisplay();
2240
+ this.ui.requestRender();
2241
+ }
2242
+ else if (event.message.role === "assistant") {
2243
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2244
+ this.streamingMessage = event.message;
2245
+ this.chatContainer.addChild(this.streamingComponent);
2246
+ this.streamingComponent.updateContent(this.streamingMessage);
2247
+ this.ui.requestRender();
2248
+ }
2249
+ }
2250
+ _handleMessageUpdate(event) {
2251
+ if (!this.streamingComponent || event.message.role !== "assistant")
2252
+ return;
2253
+ this.streamingMessage = event.message;
2254
+ this.streamingComponent.updateContent(this.streamingMessage);
2255
+ for (const content of this.streamingMessage.content) {
2256
+ if (content.type !== "toolCall")
2257
+ continue;
2258
+ if (!this.pendingTools.has(content.id)) {
2259
+ const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
2260
+ showImages: this.settingsManager.getShowImages(),
2261
+ imageWidthCells: this.settingsManager.getImageWidthCells(),
2262
+ }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2263
+ component.setExpanded(this.toolOutputExpanded);
2264
+ this.chatContainer.addChild(component);
2265
+ this.pendingTools.set(content.id, component);
2266
+ }
2267
+ else {
2268
+ this.pendingTools.get(content.id)?.updateArgs(content.arguments);
2269
+ }
2270
+ }
2271
+ this.ui.requestRender();
2272
+ }
2273
+ _handleMessageEnd(event) {
2274
+ if (event.message.role === "user")
2275
+ return;
2276
+ if (!this.streamingComponent || event.message.role !== "assistant")
2277
+ return;
2278
+ this.streamingMessage = event.message;
2279
+ let errorMessage;
2280
+ if (this.streamingMessage.stopReason === "aborted") {
2281
+ const retryAttempt = this.session.retryAttempt;
2282
+ errorMessage =
2283
+ retryAttempt > 0
2284
+ ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
2285
+ : "Operation aborted";
2286
+ this.streamingMessage.errorMessage = errorMessage;
2287
+ }
2288
+ this.streamingComponent.updateContent(this.streamingMessage);
2289
+ if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2290
+ const msg = errorMessage || this.streamingMessage.errorMessage || "Error";
2291
+ for (const [, component] of this.pendingTools.entries()) {
2292
+ component.updateResult({ content: [{ type: "text", text: msg }], isError: true });
2293
+ }
2294
+ this.pendingTools.clear();
2295
+ }
2296
+ else {
2297
+ for (const [, component] of this.pendingTools.entries()) {
2298
+ component.setArgsComplete();
2299
+ }
2300
+ }
2301
+ this.streamingComponent = undefined;
2302
+ this.streamingMessage = undefined;
2303
+ this.footer.invalidate();
2304
+ this.ui.requestRender();
2305
+ }
2306
+ _handleToolExecutionStart(event) {
2307
+ let component = this.pendingTools.get(event.toolCallId);
2308
+ if (!component) {
2309
+ component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
2310
+ showImages: this.settingsManager.getShowImages(),
2311
+ imageWidthCells: this.settingsManager.getImageWidthCells(),
2312
+ }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
2313
+ component.setExpanded(this.toolOutputExpanded);
2314
+ this.chatContainer.addChild(component);
2315
+ this.pendingTools.set(event.toolCallId, component);
2316
+ }
2317
+ component.markExecutionStarted();
2318
+ this.ui.requestRender();
2319
+ }
2320
+ _handleToolExecutionUpdate(event) {
2321
+ const component = this.pendingTools.get(event.toolCallId);
2322
+ if (component) {
2323
+ component.updateResult({ ...event.partialResult, isError: false }, true);
2324
+ this.ui.requestRender();
2325
+ }
2326
+ }
2327
+ _handleToolExecutionEnd(event) {
2328
+ const component = this.pendingTools.get(event.toolCallId);
2329
+ if (component) {
2330
+ component.updateResult({ ...event.result, isError: event.isError });
2331
+ this.pendingTools.delete(event.toolCallId);
2332
+ this.ui.requestRender();
2333
+ }
2334
+ }
2335
+ async _handleAgentEnd() {
2336
+ if (this.settingsManager.getShowTerminalProgress()) {
2337
+ this.ui.terminal.setProgress(false);
2338
+ }
2339
+ if (this.loadingAnimation) {
2340
+ this.loadingAnimation.stop();
2341
+ this.loadingAnimation = undefined;
2342
+ this.statusContainer.clear();
2343
+ }
2344
+ if (this.streamingComponent) {
2345
+ this.chatContainer.removeChild(this.streamingComponent);
2346
+ this.streamingComponent = undefined;
2347
+ this.streamingMessage = undefined;
2348
+ }
2349
+ this.pendingTools.clear();
2350
+ await this.checkShutdownRequested();
2351
+ this.ui.requestRender();
2352
+ }
2353
+ _handleCompactionStart(event) {
2354
+ if (this.settingsManager.getShowTerminalProgress()) {
2355
+ this.ui.terminal.setProgress(true);
2356
+ }
2357
+ this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
2358
+ this.defaultEditor.onEscape = () => {
2359
+ this.session.abortCompaction();
2360
+ };
2361
+ this.statusContainer.clear();
2362
+ const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
2363
+ const label = event.reason === "manual"
2364
+ ? `Compacting context... ${cancelHint}`
2365
+ : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
2366
+ this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
2367
+ this.statusContainer.addChild(this.autoCompactionLoader);
2368
+ this.ui.requestRender();
2369
+ }
2370
+ _handleCompactionEnd(event) {
2371
+ if (this.settingsManager.getShowTerminalProgress()) {
2372
+ this.ui.terminal.setProgress(false);
2373
+ }
2374
+ if (this.autoCompactionEscapeHandler) {
2375
+ this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
2376
+ this.autoCompactionEscapeHandler = undefined;
2377
+ }
2378
+ if (this.autoCompactionLoader) {
2379
+ this.autoCompactionLoader.stop();
2380
+ this.autoCompactionLoader = undefined;
2381
+ this.statusContainer.clear();
2382
+ }
2383
+ if (event.aborted) {
2384
+ if (event.reason === "manual") {
2385
+ this.showError("Compaction cancelled");
2386
+ }
2387
+ else {
2388
+ this.showStatus("Auto-compaction cancelled");
2389
+ }
2390
+ }
2391
+ else if (event.result) {
2392
+ this.chatContainer.clear();
2393
+ this.rebuildChatFromMessages();
2394
+ this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
2395
+ this.footer.invalidate();
2396
+ }
2397
+ else if (event.errorMessage) {
2398
+ if (event.reason === "manual") {
2399
+ this.showError(event.errorMessage);
2400
+ }
2401
+ else {
2402
+ this.chatContainer.addChild(new Spacer(1));
2403
+ this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
2507
2404
  }
2508
2405
  }
2406
+ void this.flushCompactionQueue({ willRetry: event.willRetry });
2407
+ this.ui.requestRender();
2408
+ }
2409
+ _handleAutoRetryStart(event) {
2410
+ this.retryEscapeHandler = this.defaultEditor.onEscape;
2411
+ this.defaultEditor.onEscape = () => {
2412
+ this.session.abortRetry();
2413
+ };
2414
+ this.statusContainer.clear();
2415
+ this.retryCountdown?.dispose();
2416
+ const retryMessage = (seconds) => `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`;
2417
+ this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
2418
+ this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => this.retryLoader?.setMessage(retryMessage(seconds)), () => {
2419
+ this.retryCountdown = undefined;
2420
+ });
2421
+ this.statusContainer.addChild(this.retryLoader);
2422
+ this.ui.requestRender();
2423
+ }
2424
+ _handleAutoRetryEnd(event) {
2425
+ if (this.retryEscapeHandler) {
2426
+ this.defaultEditor.onEscape = this.retryEscapeHandler;
2427
+ this.retryEscapeHandler = undefined;
2428
+ }
2429
+ if (this.retryCountdown) {
2430
+ this.retryCountdown.dispose();
2431
+ this.retryCountdown = undefined;
2432
+ }
2433
+ if (this.retryLoader) {
2434
+ this.retryLoader.stop();
2435
+ this.retryLoader = undefined;
2436
+ this.statusContainer.clear();
2437
+ }
2438
+ if (!event.success) {
2439
+ this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
2440
+ }
2441
+ this.ui.requestRender();
2509
2442
  }
2510
2443
  /** Extract text content from a user message */
2511
2444
  getUserMessageText(message) {
@@ -3415,7 +3348,7 @@ export class InteractiveMode {
3415
3348
  }
3416
3349
  async handleModelCommand(searchTerm) {
3417
3350
  if (!searchTerm) {
3418
- this.showModelSelector();
3351
+ this.showModelHub("select");
3419
3352
  return;
3420
3353
  }
3421
3354
  const model = await this.findExactModelMatch(searchTerm);
@@ -3433,7 +3366,7 @@ export class InteractiveMode {
3433
3366
  }
3434
3367
  return;
3435
3368
  }
3436
- this.showModelSelector(searchTerm);
3369
+ this.showModelHub("select", searchTerm);
3437
3370
  }
3438
3371
  async findExactModelMatch(searchTerm) {
3439
3372
  const models = await this.getModelCandidates();
@@ -3485,93 +3418,100 @@ export class InteractiveMode {
3485
3418
  // Ignore auth lookup failures for warning-only checks.
3486
3419
  }
3487
3420
  }
3488
- showModelSelector(initialSearchInput) {
3489
- this.showSelector((done) => {
3490
- const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
3491
- try {
3492
- await this.session.setModel(model);
3493
- this.footer.invalidate();
3494
- this.updateEditorBorderColor();
3495
- done();
3496
- this.showStatus(`Model: ${model.id}`);
3497
- void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
3498
- this.checkDaxnutsEasterEgg(model);
3499
- }
3500
- catch (error) {
3501
- done();
3502
- this.showError(error instanceof Error ? error.message : String(error));
3503
- }
3504
- }, () => {
3505
- done();
3506
- this.ui.requestRender();
3507
- }, initialSearchInput);
3508
- return { component: selector, focus: selector };
3509
- });
3421
+ async resolveEnabledModelIds() {
3422
+ const sessionScopedModels = this.session.scopedModels;
3423
+ if (sessionScopedModels.length > 0) {
3424
+ return sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3425
+ }
3426
+ const patterns = this.settingsManager.getEnabledModels();
3427
+ if (patterns !== undefined && patterns.length > 0) {
3428
+ const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3429
+ return scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3430
+ }
3431
+ return null;
3510
3432
  }
3511
- async showModelsSelector() {
3512
- // Get all available models
3513
- this.session.modelRegistry.refresh();
3433
+ async applyCycleEnabledIds(enabledIds) {
3514
3434
  const allModels = this.session.modelRegistry.getAvailable();
3515
- if (allModels.length === 0) {
3516
- this.showStatus("No models available");
3517
- return;
3518
- }
3519
- // Check if session has scoped models (from previous session-only changes or CLI --models)
3520
- const sessionScopedModels = this.session.scopedModels;
3521
- const hasSessionScope = sessionScopedModels.length > 0;
3522
- // Build enabled model IDs from session state or settings
3523
- let currentEnabledIds = null;
3524
- if (hasSessionScope) {
3525
- // Use current session's scoped models
3526
- currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3435
+ if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3436
+ const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3437
+ this.session.setScopedModels(newScopedModels.map((sm) => ({
3438
+ model: sm.model,
3439
+ thinkingLevel: sm.thinkingLevel,
3440
+ })));
3527
3441
  }
3528
3442
  else {
3529
- // Fall back to settings
3530
- const patterns = this.settingsManager.getEnabledModels();
3531
- if (patterns !== undefined && patterns.length > 0) {
3532
- const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
3533
- currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
3534
- }
3535
- }
3536
- // Helper to update session's scoped models (session-only, no persist)
3537
- const updateSessionModels = async (enabledIds) => {
3538
- currentEnabledIds = enabledIds === null ? null : [...enabledIds];
3539
- if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
3540
- const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
3541
- this.session.setScopedModels(newScopedModels.map((sm) => ({
3542
- model: sm.model,
3543
- thinkingLevel: sm.thinkingLevel,
3544
- })));
3545
- }
3546
- else {
3547
- // All enabled or none enabled = no filter
3548
- this.session.setScopedModels([]);
3549
- }
3550
- await this.updateAvailableProviderCount();
3551
- this.ui.requestRender();
3552
- };
3553
- this.showSelector((done) => {
3554
- const selector = new ScopedModelsSelectorComponent({
3555
- allModels,
3556
- enabledModelIds: currentEnabledIds,
3557
- }, {
3558
- onChange: async (enabledIds) => {
3559
- await updateSessionModels(enabledIds);
3560
- },
3561
- onPersist: (enabledIds) => {
3562
- // Persist to settings
3563
- const newPatterns = enabledIds === null || enabledIds.length === allModels.length
3564
- ? undefined // All enabled = clear filter
3565
- : enabledIds;
3566
- this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3567
- this.showStatus("Model selection saved to settings");
3568
- },
3569
- onCancel: () => {
3570
- done();
3571
- this.ui.requestRender();
3572
- },
3443
+ this.session.setScopedModels([]);
3444
+ }
3445
+ await this.updateAvailableProviderCount();
3446
+ this.ui.requestRender();
3447
+ }
3448
+ showModelHub(initialTab = "select", initialSearchInput) {
3449
+ void this.resolveEnabledModelIds().then((enabledModelIds) => {
3450
+ this.showSelector((done) => {
3451
+ const hub = new ModelHubComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, enabledModelIds, {
3452
+ onSelectModel: async (model) => {
3453
+ try {
3454
+ await this.session.setModel(model);
3455
+ this.footer.invalidate();
3456
+ this.updateEditorBorderColor();
3457
+ done();
3458
+ this.showStatus(`Model: ${model.id}`);
3459
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
3460
+ this.checkDaxnutsEasterEgg(model);
3461
+ }
3462
+ catch (error) {
3463
+ done();
3464
+ this.showError(error instanceof Error ? error.message : String(error));
3465
+ }
3466
+ },
3467
+ onCancel: () => {
3468
+ done();
3469
+ this.ui.requestRender();
3470
+ },
3471
+ onCycleChange: async (ids) => {
3472
+ await this.applyCycleEnabledIds(ids);
3473
+ },
3474
+ onCyclePersist: async (ids) => {
3475
+ const allModels = this.session.modelRegistry.getAvailable();
3476
+ const newPatterns = ids === null || ids.length === allModels.length ? undefined : ids;
3477
+ this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
3478
+ this.showStatus("Model selection saved to settings");
3479
+ },
3480
+ onManageAdd: async () => {
3481
+ done();
3482
+ await this.showCustomProviderWizard({ mode: "add", target: "models_json" });
3483
+ },
3484
+ onManageEdit: async (source, providerId) => {
3485
+ done();
3486
+ await this.showCustomProviderWizard({ mode: "edit", target: source, providerId });
3487
+ },
3488
+ onManageRemove: async (source, providerId) => {
3489
+ done();
3490
+ const confirmed = await this.showExtensionConfirm("Remove provider", `Remove "${providerId}" from ${source === "models_json" ? "models.json" : "auth.json"}?`);
3491
+ if (!confirmed) {
3492
+ this.showModelHub("manage");
3493
+ return;
3494
+ }
3495
+ try {
3496
+ if (source === "models_json") {
3497
+ this.session.modelRegistry.removeProviderFromModelsJson(providerId);
3498
+ }
3499
+ else {
3500
+ this.session.modelRegistry.authStorage.remove(providerId);
3501
+ this.session.modelRegistry.refresh();
3502
+ }
3503
+ await this.updateAvailableProviderCount();
3504
+ this.showStatus(`Removed provider ${providerId}`);
3505
+ this.showModelHub("manage");
3506
+ }
3507
+ catch (error) {
3508
+ this.showError(error instanceof Error ? error.message : String(error));
3509
+ this.showModelHub("manage");
3510
+ }
3511
+ },
3512
+ }, { initialTab, initialSearchInput });
3513
+ return { component: hub, focus: hub };
3573
3514
  });
3574
- return { component: selector, focus: selector };
3575
3515
  });
3576
3516
  }
3577
3517
  showUserMessageSelector() {
@@ -3932,17 +3872,18 @@ export class InteractiveMode {
3932
3872
  if (isUnknownModel(previousModel)) {
3933
3873
  const availableModels = this.session.modelRegistry.getAvailable();
3934
3874
  const providerModels = availableModels.filter((model) => model.provider === providerId);
3935
- if (!hasDefaultModelProvider(providerId)) {
3936
- selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
3937
- }
3938
- else if (providerModels.length === 0) {
3875
+ if (providerModels.length === 0) {
3939
3876
  selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
3940
3877
  }
3941
3878
  else {
3942
- const defaultModelId = defaultModelPerProvider[providerId];
3943
- selectedModel = providerModels.find((model) => model.id === defaultModelId);
3879
+ const defaultModelId = hasDefaultModelProvider(providerId)
3880
+ ? defaultModelPerProvider[providerId]
3881
+ : undefined;
3882
+ selectedModel =
3883
+ (defaultModelId ? providerModels.find((model) => model.id === defaultModelId) : undefined) ??
3884
+ providerModels[0];
3944
3885
  if (!selectedModel) {
3945
- selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
3886
+ selectionError = `${actionLabel}, but no model could be selected. Use /model to select a model.`;
3946
3887
  }
3947
3888
  else {
3948
3889
  try {
@@ -3951,7 +3892,7 @@ export class InteractiveMode {
3951
3892
  catch (error) {
3952
3893
  selectedModel = undefined;
3953
3894
  const errorMessage = error instanceof Error ? error.message : String(error);
3954
- selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
3895
+ selectionError = `${actionLabel}, but selecting model failed: ${errorMessage}. Use /model to select a model.`;
3955
3896
  }
3956
3897
  }
3957
3898
  }
@@ -4026,12 +3967,60 @@ export class InteractiveMode {
4026
3967
  }
4027
3968
  }
4028
3969
  async showOpenAICompatibleLoginDialog() {
3970
+ await this.showCustomProviderWizard({ mode: "add", target: "openai_compatible" });
3971
+ }
3972
+ /**
3973
+ * Interactive wizard to add/edit a custom OpenAI-compatible provider.
3974
+ * - target models_json: writes ~/.pi/agent/models.json (Manage tab default)
3975
+ * - target openai_compatible: writes auth.json ( /login path )
3976
+ */
3977
+ async showCustomProviderWizard(options) {
4029
3978
  const previousModel = this.session.model;
4030
- const providerId = `openai-compatible-${crypto.randomUUID().slice(0, 8)}`;
4031
- const providerName = "OpenAI Compatible";
4032
- const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4033
- // Completion handled below
4034
- }, providerName, "Configure OpenAI Compatible Provider");
3979
+ const isEdit = options.mode === "edit" && !!options.providerId;
3980
+ let existing;
3981
+ if (isEdit && options.providerId) {
3982
+ if (options.target === "openai_compatible") {
3983
+ const all = this.session.modelRegistry.authStorage.getOpenAICompatibleProviders();
3984
+ const info = all[options.providerId];
3985
+ if (!info) {
3986
+ this.showError(`Provider "${options.providerId}" not found in auth.json`);
3987
+ this.showModelHub("manage");
3988
+ return;
3989
+ }
3990
+ existing = {
3991
+ providerId: options.providerId,
3992
+ baseUrl: info.baseUrl,
3993
+ apiKey: info.key,
3994
+ models: info.models,
3995
+ };
3996
+ }
3997
+ else {
3998
+ const listed = this.session.modelRegistry
3999
+ .listModelsJsonProviders()
4000
+ .find((p) => p.name === options.providerId);
4001
+ if (!listed) {
4002
+ this.showError(`Provider "${options.providerId}" not found in models.json`);
4003
+ this.showModelHub("manage");
4004
+ return;
4005
+ }
4006
+ // models.json apiKey is opaque (env/command/literal); do not surface the raw value
4007
+ existing = {
4008
+ providerId: options.providerId,
4009
+ baseUrl: listed.baseUrl ?? "",
4010
+ apiKey: "",
4011
+ models: listed.modelIds.map((id) => ({ id, name: id })),
4012
+ };
4013
+ }
4014
+ }
4015
+ const dialogProviderId = existing?.providerId ?? "custom-provider";
4016
+ const title = options.target === "models_json"
4017
+ ? isEdit
4018
+ ? `Edit models.json provider (${existing?.providerId})`
4019
+ : "Add custom provider (models.json)"
4020
+ : isEdit
4021
+ ? `Edit OpenAI Compatible (${existing?.providerId})`
4022
+ : "Configure OpenAI Compatible Provider";
4023
+ const dialog = new LoginDialogComponent(this.ui, dialogProviderId, (_success, _message) => { }, existing?.providerId ?? "Custom Provider", title);
4035
4024
  this.editorContainer.clear();
4036
4025
  this.editorContainer.addChild(dialog);
4037
4026
  this.ui.setFocus(dialog);
@@ -4043,45 +4032,165 @@ export class InteractiveMode {
4043
4032
  this.ui.requestRender();
4044
4033
  };
4045
4034
  try {
4046
- const baseUrl = (await dialog.showPrompt("Enter base URL:", "http://localhost:11434/v1")).trim();
4035
+ let providerId = existing?.providerId ?? "";
4036
+ if (!isEdit) {
4037
+ const rawName = (await dialog.showPrompt("Provider name/id (e.g. ollama, agnes):", existing?.providerId ?? "ollama")).trim();
4038
+ providerId = slugifyProviderId(rawName);
4039
+ if (!providerId) {
4040
+ throw new Error("Provider name cannot be empty.");
4041
+ }
4042
+ if (BUILT_IN_MODEL_PROVIDERS.has(providerId) && options.target === "openai_compatible") {
4043
+ throw new Error(`"${providerId}" is a built-in provider. Use a different name, or add it via Manage → models.json override.`);
4044
+ }
4045
+ }
4046
+ const baseUrl = (await dialog.showPrompt("Enter base URL:", existing?.baseUrl || "http://localhost:11434/v1")).trim();
4047
4047
  if (!baseUrl) {
4048
4048
  throw new Error("Base URL cannot be empty.");
4049
4049
  }
4050
- const apiKey = (await dialog.showPrompt("Enter API key (or leave blank for local endpoints):")).trim();
4051
- const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", "llama3.1")).trim();
4052
- if (!modelNames) {
4053
- throw new Error("At least one model name is required.");
4054
- }
4055
- const models = modelNames
4056
- .split(",")
4057
- .map((s) => s.trim())
4058
- .filter((s) => s.length > 0)
4059
- .map((id) => ({
4060
- id,
4061
- name: id,
4062
- }));
4063
- const effectiveApiKey = apiKey || "not-needed";
4064
- // Immediately register the provider so models are available
4065
- this.session.modelRegistry.configureOpenAICompatibleProvider(providerId, {
4066
- baseUrl,
4067
- apiKey: effectiveApiKey,
4068
- models,
4069
- });
4070
- // Persist to auth.json so it survives restarts
4071
- this.session.modelRegistry.authStorage.set(providerId, {
4072
- type: "openai_compatible",
4073
- key: effectiveApiKey,
4074
- baseUrl,
4075
- models,
4076
- });
4077
- restoreEditor();
4078
- await this.completeProviderAuthentication(providerId, `${baseUrl}`, "api_key", previousModel);
4050
+ const apiKeyPrompt = options.target === "models_json"
4051
+ ? "API key (literal, env var name, or !command; blank = not-needed):"
4052
+ : "Enter API key (or leave blank for local endpoints):";
4053
+ const apiKey = (await dialog.showPrompt(apiKeyPrompt, existing?.apiKey === "not-needed" ? "" : (existing?.apiKey ?? ""))).trim();
4054
+ // On models.json edit, blank keeps the existing apiKey field (not rewritten).
4055
+ const effectiveApiKey = apiKey
4056
+ ? apiKey
4057
+ : isEdit && options.target === "models_json"
4058
+ ? undefined
4059
+ : "not-needed";
4060
+ dialog.showInfo([theme.fg("muted", "Discovering models from /models ...")]);
4061
+ this.ui.requestRender();
4062
+ const discovered = await fetchOpenAICompatibleModels(baseUrl, effectiveApiKey);
4063
+ let models;
4064
+ if (discovered.ok && discovered.models.length > 0) {
4065
+ // Offer: use all discovered, or hand-edit
4066
+ const useAllLabel = `Use all ${discovered.models.length} discovered models`;
4067
+ const manualLabel = "Enter model ids manually";
4068
+ const preview = discovered.models
4069
+ .slice(0, 8)
4070
+ .map((m) => m.id)
4071
+ .join(", ");
4072
+ const more = discovered.models.length > 8 ? ` (+${discovered.models.length - 8} more)` : "";
4073
+ dialog.showInfo([
4074
+ theme.fg("success", `Found ${discovered.models.length} models`),
4075
+ theme.fg("muted", `${preview}${more}`),
4076
+ ]);
4077
+ this.ui.requestRender();
4078
+ const choice = await new Promise((resolve) => {
4079
+ const restoreDialog = () => {
4080
+ this.editorContainer.clear();
4081
+ this.editorContainer.addChild(dialog);
4082
+ this.ui.setFocus(dialog);
4083
+ this.ui.requestRender();
4084
+ };
4085
+ const selector = new ExtensionSelectorComponent("Discovered models", [useAllLabel, manualLabel], (option) => {
4086
+ restoreDialog();
4087
+ resolve(option);
4088
+ }, () => {
4089
+ restoreDialog();
4090
+ resolve(undefined);
4091
+ });
4092
+ this.editorContainer.clear();
4093
+ this.editorContainer.addChild(selector);
4094
+ this.ui.setFocus(selector);
4095
+ this.ui.requestRender();
4096
+ });
4097
+ if (!choice) {
4098
+ throw new Error("Login cancelled");
4099
+ }
4100
+ if (choice === useAllLabel) {
4101
+ models = discovered.models.map((m) => ({ id: m.id, name: m.name }));
4102
+ }
4103
+ else {
4104
+ const defaultIds = existing?.models.map((m) => m.id).join(", ") ||
4105
+ discovered.models
4106
+ .slice(0, 5)
4107
+ .map((m) => m.id)
4108
+ .join(", ");
4109
+ const modelNames = (await dialog.showPrompt("Model id(s), comma-separated:", defaultIds)).trim();
4110
+ if (!modelNames)
4111
+ throw new Error("At least one model name is required.");
4112
+ models = modelNames
4113
+ .split(",")
4114
+ .map((s) => s.trim())
4115
+ .filter((s) => s.length > 0)
4116
+ .map((id) => ({ id, name: id }));
4117
+ }
4118
+ }
4119
+ else {
4120
+ dialog.showInfo([
4121
+ theme.fg("warning", `Auto-discover failed: ${discovered.ok ? "no models" : discovered.error}`),
4122
+ theme.fg("muted", "Falling back to manual model entry."),
4123
+ ]);
4124
+ this.ui.requestRender();
4125
+ const defaultIds = existing?.models.map((m) => m.id).join(", ") || "llama3.1";
4126
+ const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", defaultIds)).trim();
4127
+ if (!modelNames) {
4128
+ throw new Error("At least one model name is required.");
4129
+ }
4130
+ models = modelNames
4131
+ .split(",")
4132
+ .map((s) => s.trim())
4133
+ .filter((s) => s.length > 0)
4134
+ .map((id) => ({ id, name: id }));
4135
+ }
4136
+ if (options.target === "openai_compatible") {
4137
+ const storedKey = effectiveApiKey ?? "not-needed";
4138
+ // Replace previous id if renaming isn't supported (edit keeps id)
4139
+ if (isEdit && existing && existing.providerId !== providerId) {
4140
+ this.session.modelRegistry.authStorage.remove(existing.providerId);
4141
+ }
4142
+ this.session.modelRegistry.configureOpenAICompatibleProvider(providerId, {
4143
+ baseUrl,
4144
+ apiKey: storedKey,
4145
+ models,
4146
+ });
4147
+ this.session.modelRegistry.authStorage.set(providerId, {
4148
+ type: "openai_compatible",
4149
+ key: storedKey,
4150
+ baseUrl,
4151
+ models,
4152
+ });
4153
+ // refresh so getAvailable sees it
4154
+ this.session.modelRegistry.refresh();
4155
+ restoreEditor();
4156
+ await this.completeProviderAuthentication(providerId, baseUrl, "api_key", previousModel);
4157
+ }
4158
+ else {
4159
+ this.session.modelRegistry.upsertProviderInModelsJson(providerId, {
4160
+ baseUrl,
4161
+ ...(effectiveApiKey !== undefined ? { apiKey: effectiveApiKey } : {}),
4162
+ api: "openai-completions",
4163
+ models: models.map((m) => ({
4164
+ id: m.id,
4165
+ name: m.name,
4166
+ reasoning: false,
4167
+ input: ["text"],
4168
+ })),
4169
+ });
4170
+ restoreEditor();
4171
+ await this.updateAvailableProviderCount();
4172
+ // Prefer selecting first model if no real model yet
4173
+ if (isUnknownModel(previousModel)) {
4174
+ const available = this.session.modelRegistry.getAvailable().filter((m) => m.provider === providerId);
4175
+ if (available[0]) {
4176
+ try {
4177
+ await this.session.setModel(available[0]);
4178
+ this.footer.invalidate();
4179
+ this.updateEditorBorderColor();
4180
+ }
4181
+ catch {
4182
+ // ignore
4183
+ }
4184
+ }
4185
+ }
4186
+ this.showStatus(`Saved provider "${providerId}" to models.json (${models.length} model${models.length === 1 ? "" : "s"}). Comments/trailing commas in that file are not preserved.`);
4187
+ }
4079
4188
  }
4080
4189
  catch (error) {
4081
4190
  restoreEditor();
4082
4191
  const errorMsg = error instanceof Error ? error.message : String(error);
4083
4192
  if (errorMsg !== "Login cancelled") {
4084
- this.showError(`Failed to configure OpenAI Compatible provider: ${errorMsg}`);
4193
+ this.showError(`Failed to configure provider: ${errorMsg}`);
4085
4194
  }
4086
4195
  }
4087
4196
  }
@@ -4612,7 +4721,7 @@ export class InteractiveMode {
4612
4721
  this.chatContainer.addChild(new Text(info, 1, 0));
4613
4722
  this.ui.requestRender();
4614
4723
  }
4615
- showMcpSelector(highlightServer) {
4724
+ async showMcpSelector(highlightServer) {
4616
4725
  const buildItems = () => {
4617
4726
  // Fetch fresh config each time so toggle reflects immediately
4618
4727
  const configured = this.settingsManager.getMcpServers();
@@ -4635,22 +4744,22 @@ export class InteractiveMode {
4635
4744
  };
4636
4745
  });
4637
4746
  };
4638
- const items = buildItems();
4639
- if (items.length === 0) {
4640
- this.chatContainer.addChild(new Spacer(1));
4641
- this.chatContainer.addChild(new Text(`${theme.bold("MCP Servers")}\n\n${theme.fg("dim", "No MCP servers configured. Add 'mcpServers' to settings.json.")}`, 1, 0));
4642
- this.ui.requestRender();
4643
- return;
4644
- }
4645
4747
  if (highlightServer) {
4646
4748
  const configured = this.settingsManager.getMcpServers();
4647
4749
  const server = configured?.[highlightServer];
4648
4750
  if (server) {
4649
4751
  const currentEnabled = server.enabled !== false;
4650
4752
  this.settingsManager.setMcpServerEnabled(highlightServer, !currentEnabled);
4651
- this.session.reloadMcp(highlightServer);
4753
+ await this.session.reloadMcp(highlightServer);
4652
4754
  }
4653
4755
  }
4756
+ const items = buildItems();
4757
+ if (items.length === 0) {
4758
+ this.chatContainer.addChild(new Spacer(1));
4759
+ this.chatContainer.addChild(new Text(`${theme.bold("MCP Servers")}\n\n${theme.fg("dim", "No MCP servers configured. Add 'mcpServers' to settings.json.")}`, 1, 0));
4760
+ this.ui.requestRender();
4761
+ return;
4762
+ }
4654
4763
  this.showSelector((done) => {
4655
4764
  const selector = new McpSelectorComponent("MCP Servers", items, async (name) => {
4656
4765
  const cur = this.settingsManager.getMcpServers();