gsd-pi 2.39.0 → 2.40.0-dev.4a93031

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 (133) hide show
  1. package/dist/resource-loader.js +66 -2
  2. package/dist/resources/extensions/async-jobs/index.js +10 -0
  3. package/dist/resources/extensions/get-secrets-from-user.js +1 -1
  4. package/dist/resources/extensions/gsd/auto-dashboard.js +7 -0
  5. package/dist/resources/extensions/gsd/auto-loop.js +761 -673
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +10 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
  8. package/dist/resources/extensions/gsd/auto-start.js +6 -1
  9. package/dist/resources/extensions/gsd/auto.js +6 -4
  10. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +126 -0
  11. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +233 -0
  12. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +59 -0
  13. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +38 -0
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +156 -0
  15. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +46 -0
  16. package/dist/resources/extensions/gsd/bootstrap/system-context.js +300 -0
  17. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +38 -0
  18. package/dist/resources/extensions/gsd/commands/catalog.js +278 -0
  19. package/dist/resources/extensions/gsd/commands/context.js +84 -0
  20. package/dist/resources/extensions/gsd/commands/dispatcher.js +21 -0
  21. package/dist/resources/extensions/gsd/commands/handlers/auto.js +72 -0
  22. package/dist/resources/extensions/gsd/commands/handlers/core.js +246 -0
  23. package/dist/resources/extensions/gsd/commands/handlers/ops.js +166 -0
  24. package/dist/resources/extensions/gsd/commands/handlers/parallel.js +94 -0
  25. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +102 -0
  26. package/dist/resources/extensions/gsd/commands/index.js +11 -0
  27. package/dist/resources/extensions/gsd/commands-handlers.js +1 -1
  28. package/dist/resources/extensions/gsd/commands.js +8 -1190
  29. package/dist/resources/extensions/gsd/dashboard-overlay.js +9 -0
  30. package/dist/resources/extensions/gsd/doctor-proactive.js +80 -10
  31. package/dist/resources/extensions/gsd/doctor.js +32 -2
  32. package/dist/resources/extensions/gsd/export-html.js +46 -0
  33. package/dist/resources/extensions/gsd/files.js +1 -1
  34. package/dist/resources/extensions/gsd/health-widget.js +1 -1
  35. package/dist/resources/extensions/gsd/index.js +4 -1115
  36. package/dist/resources/extensions/gsd/progress-score.js +20 -1
  37. package/dist/resources/extensions/gsd/prompts/forensics.md +121 -46
  38. package/dist/resources/extensions/gsd/visualizer-data.js +26 -1
  39. package/dist/resources/extensions/gsd/visualizer-views.js +52 -0
  40. package/dist/welcome-screen.d.ts +3 -2
  41. package/dist/welcome-screen.js +66 -22
  42. package/package.json +1 -1
  43. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
  44. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/agent-session.js +107 -24
  46. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/skill-tool.test.d.ts +2 -0
  48. package/packages/pi-coding-agent/dist/core/skill-tool.test.d.ts.map +1 -0
  49. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +70 -0
  50. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -0
  51. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/skills.js +2 -1
  53. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +17 -0
  55. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -0
  56. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +244 -0
  57. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts +3 -0
  59. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -0
  60. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +58 -0
  61. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -0
  62. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +12 -0
  63. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -0
  64. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +54 -0
  65. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -0
  66. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts +6 -0
  67. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +63 -0
  69. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -0
  70. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +38 -0
  71. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -0
  72. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js +2 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -0
  74. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -1
  75. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -457
  77. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  78. package/packages/pi-coding-agent/package.json +1 -1
  79. package/packages/pi-coding-agent/src/core/agent-session.ts +122 -23
  80. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +89 -0
  81. package/packages/pi-coding-agent/src/core/skills.ts +2 -1
  82. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +302 -0
  83. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +59 -0
  84. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +68 -0
  85. package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +71 -0
  86. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +37 -0
  87. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +18 -510
  88. package/pkg/package.json +1 -1
  89. package/src/resources/extensions/async-jobs/index.ts +11 -0
  90. package/src/resources/extensions/get-secrets-from-user.ts +1 -1
  91. package/src/resources/extensions/gsd/auto-dashboard.ts +10 -0
  92. package/src/resources/extensions/gsd/auto-loop.ts +1075 -921
  93. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -2
  94. package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
  95. package/src/resources/extensions/gsd/auto-start.ts +6 -1
  96. package/src/resources/extensions/gsd/auto.ts +13 -10
  97. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +142 -0
  98. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +238 -0
  99. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +90 -0
  100. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +46 -0
  101. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +167 -0
  102. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +55 -0
  103. package/src/resources/extensions/gsd/bootstrap/system-context.ts +340 -0
  104. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +51 -0
  105. package/src/resources/extensions/gsd/commands/catalog.ts +301 -0
  106. package/src/resources/extensions/gsd/commands/context.ts +101 -0
  107. package/src/resources/extensions/gsd/commands/dispatcher.ts +32 -0
  108. package/src/resources/extensions/gsd/commands/handlers/auto.ts +74 -0
  109. package/src/resources/extensions/gsd/commands/handlers/core.ts +274 -0
  110. package/src/resources/extensions/gsd/commands/handlers/ops.ts +169 -0
  111. package/src/resources/extensions/gsd/commands/handlers/parallel.ts +118 -0
  112. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +109 -0
  113. package/src/resources/extensions/gsd/commands/index.ts +14 -0
  114. package/src/resources/extensions/gsd/commands-handlers.ts +1 -1
  115. package/src/resources/extensions/gsd/commands.ts +10 -1329
  116. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  117. package/src/resources/extensions/gsd/doctor-proactive.ts +106 -10
  118. package/src/resources/extensions/gsd/doctor.ts +47 -3
  119. package/src/resources/extensions/gsd/export-html.ts +51 -0
  120. package/src/resources/extensions/gsd/files.ts +1 -1
  121. package/src/resources/extensions/gsd/health-widget.ts +2 -1
  122. package/src/resources/extensions/gsd/index.ts +12 -1314
  123. package/src/resources/extensions/gsd/progress-score.ts +23 -0
  124. package/src/resources/extensions/gsd/prompts/forensics.md +121 -46
  125. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +13 -9
  126. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +3 -3
  127. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +16 -16
  128. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +4 -4
  129. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +10 -10
  130. package/src/resources/extensions/gsd/visualizer-data.ts +51 -1
  131. package/src/resources/extensions/gsd/visualizer-views.ts +58 -0
  132. /package/dist/resources/extensions/{env-utils.js → gsd/env-utils.js} +0 -0
  133. /package/src/resources/extensions/{env-utils.ts → gsd/env-utils.ts} +0 -0
@@ -89,6 +89,15 @@ import { TreeSelectorComponent } from "./components/tree-selector.js";
89
89
  import { UserMessageComponent } from "./components/user-message.js";
90
90
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
91
91
  import { type SlashCommandContext, dispatchSlashCommand, getAppKeyDisplay } from "./slash-command-handlers.js";
92
+ import { handleAgentEvent } from "./controllers/chat-controller.js";
93
+ import { createExtensionUIContext as buildExtensionUIContext } from "./controllers/extension-ui-controller.js";
94
+ import { setupEditorSubmitHandler as setupEditorSubmitHandlerController } from "./controllers/input-controller.js";
95
+ import {
96
+ findExactModelMatch as findExactModelMatchController,
97
+ getModelCandidates as getModelCandidatesController,
98
+ handleModelCommand as handleModelCommandController,
99
+ updateAvailableProviderCount as updateAvailableProviderCountController,
100
+ } from "./controllers/model-controller.js";
92
101
  import {
93
102
  getAvailableThemes,
94
103
  getAvailableThemesWithPaths,
@@ -1175,12 +1184,10 @@ export class InteractiveMode {
1175
1184
  }
1176
1185
 
1177
1186
  /**
1178
- * Get a registered tool definition by name (for custom rendering).
1187
+ * Get a tool definition by name (for custom rendering).
1179
1188
  */
1180
1189
  private getRegisteredToolDefinition(toolName: string) {
1181
- const tools = this.session.extensionRunner?.getAllRegisteredTools() ?? [];
1182
- const registeredTool = tools.find((t) => t.definition.name === toolName);
1183
- return registeredTool?.definition;
1190
+ return this.session.getRenderableToolDefinition(toolName);
1184
1191
  }
1185
1192
 
1186
1193
  /**
@@ -1486,60 +1493,7 @@ export class InteractiveMode {
1486
1493
  * Create the ExtensionUIContext for extensions.
1487
1494
  */
1488
1495
  private createExtensionUIContext(): ExtensionUIContext {
1489
- return {
1490
- select: (title, options, opts) => this.showExtensionSelector(title, options, opts),
1491
- confirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),
1492
- input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
1493
- notify: (message, type) => this.showExtensionNotify(message, type),
1494
- onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
1495
- setStatus: (key, text) => this.setExtensionStatus(key, text),
1496
- setWorkingMessage: (message) => {
1497
- if (this.loadingAnimation) {
1498
- if (message) {
1499
- this.loadingAnimation.setMessage(message);
1500
- } else {
1501
- this.loadingAnimation.setMessage(
1502
- `${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`,
1503
- );
1504
- }
1505
- } else {
1506
- // Queue message for when loadingAnimation is created (handles agent_start race)
1507
- this.pendingWorkingMessage = message;
1508
- }
1509
- },
1510
- setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
1511
- setFooter: (factory) => this.setExtensionFooter(factory),
1512
- setHeader: (factory) => this.setExtensionHeader(factory),
1513
- setTitle: (title) => this.ui.terminal.setTitle(title),
1514
- custom: (factory, options) => this.showExtensionCustom(factory, options),
1515
- pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
1516
- setEditorText: (text) => this.editor.setText(text),
1517
- getEditorText: () => this.editor.getText(),
1518
- editor: (title, prefill) => this.showExtensionEditor(title, prefill),
1519
- setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1520
- get theme() {
1521
- return theme;
1522
- },
1523
- getAllThemes: () => getAvailableThemesWithPaths(),
1524
- getTheme: (name) => getThemeByName(name),
1525
- setTheme: (themeOrName) => {
1526
- if (themeOrName instanceof Theme) {
1527
- setThemeInstance(themeOrName);
1528
- this.ui.requestRender();
1529
- return { success: true };
1530
- }
1531
- const result = setTheme(themeOrName, true);
1532
- if (result.success) {
1533
- if (this.settingsManager.getTheme() !== themeOrName) {
1534
- this.settingsManager.setTheme(themeOrName);
1535
- }
1536
- this.ui.requestRender();
1537
- }
1538
- return result;
1539
- },
1540
- getToolsExpanded: () => this.toolOutputExpanded,
1541
- setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
1542
- };
1496
+ return buildExtensionUIContext(this);
1543
1497
  }
1544
1498
 
1545
1499
  /**
@@ -2017,69 +1971,7 @@ export class InteractiveMode {
2017
1971
  }
2018
1972
 
2019
1973
  private setupEditorSubmitHandler(): void {
2020
- this.defaultEditor.onSubmit = async (text: string) => {
2021
- text = text.trim();
2022
- if (!text) return;
2023
-
2024
- // Handle slash commands
2025
- if (text.startsWith("/")) {
2026
- const handled = await dispatchSlashCommand(text, this.getSlashCommandContext());
2027
- if (handled) {
2028
- this.editor.setText("");
2029
- return;
2030
- }
2031
- }
2032
-
2033
- // Handle bash command (! for normal, !! for excluded from context)
2034
- if (text.startsWith("!")) {
2035
- const isExcluded = text.startsWith("!!");
2036
- const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
2037
- if (command) {
2038
- if (this.session.isBashRunning) {
2039
- this.showWarning("A bash command is already running. Press Esc to cancel it first.");
2040
- this.editor.setText(text);
2041
- return;
2042
- }
2043
- this.editor.addToHistory?.(text);
2044
- await this.handleBashCommand(command, isExcluded);
2045
- this.isBashMode = false;
2046
- this.updateEditorBorderColor();
2047
- return;
2048
- }
2049
- }
2050
-
2051
- // Queue input during compaction (extension commands execute immediately)
2052
- if (this.session.isCompacting) {
2053
- if (this.isExtensionCommand(text)) {
2054
- this.editor.addToHistory?.(text);
2055
- this.editor.setText("");
2056
- await this.session.prompt(text);
2057
- } else {
2058
- this.queueCompactionMessage(text, "steer");
2059
- }
2060
- return;
2061
- }
2062
-
2063
- // If streaming, use prompt() with steer behavior
2064
- // This handles extension commands (execute immediately), prompt template expansion, and queueing
2065
- if (this.session.isStreaming) {
2066
- this.editor.addToHistory?.(text);
2067
- this.editor.setText("");
2068
- await this.session.prompt(text, { streamingBehavior: "steer" });
2069
- this.updatePendingMessagesDisplay();
2070
- this.ui.requestRender();
2071
- return;
2072
- }
2073
-
2074
- // Normal message submission
2075
- // First, move any pending bash components to chat
2076
- this.flushPendingBashComponents();
2077
-
2078
- if (this.onInputCallback) {
2079
- this.onInputCallback(text);
2080
- }
2081
- this.editor.addToHistory?.(text);
2082
- };
1974
+ setupEditorSubmitHandlerController(this as any);
2083
1975
  }
2084
1976
 
2085
1977
  private subscribeToAgent(): void {
@@ -2089,338 +1981,7 @@ export class InteractiveMode {
2089
1981
  }
2090
1982
 
2091
1983
  private async handleEvent(event: AgentSessionEvent): Promise<void> {
2092
- if (!this.isInitialized) {
2093
- await this.init();
2094
- }
2095
-
2096
- this.footer.invalidate();
2097
-
2098
- switch (event.type) {
2099
- case "agent_start":
2100
- // Restore main escape handler if retry handler is still active
2101
- // (retry success event fires later, but we need main handler now)
2102
- if (this.retryEscapeHandler) {
2103
- this.defaultEditor.onEscape = this.retryEscapeHandler;
2104
- this.retryEscapeHandler = undefined;
2105
- }
2106
- if (this.retryLoader) {
2107
- this.retryLoader.stop();
2108
- this.retryLoader = undefined;
2109
- }
2110
- if (this.loadingAnimation) {
2111
- this.loadingAnimation.stop();
2112
- }
2113
- this.statusContainer.clear();
2114
- this.loadingAnimation = new Loader(
2115
- this.ui,
2116
- (spinner) => theme.fg("accent", spinner),
2117
- (text) => theme.fg("muted", text),
2118
- this.defaultWorkingMessage,
2119
- );
2120
- this.statusContainer.addChild(this.loadingAnimation);
2121
- // Apply any pending working message queued before loader existed
2122
- if (this.pendingWorkingMessage !== undefined) {
2123
- if (this.pendingWorkingMessage) {
2124
- this.loadingAnimation.setMessage(this.pendingWorkingMessage);
2125
- }
2126
- this.pendingWorkingMessage = undefined;
2127
- }
2128
- this.ui.requestRender();
2129
- break;
2130
-
2131
- case "message_start":
2132
- if (event.message.role === "custom") {
2133
- this.addMessageToChat(event.message);
2134
- this.ui.requestRender();
2135
- } else if (event.message.role === "user") {
2136
- this.addMessageToChat(event.message);
2137
- this.updatePendingMessagesDisplay();
2138
- this.ui.requestRender();
2139
- } else if (event.message.role === "assistant") {
2140
- this.streamingComponent = new AssistantMessageComponent(
2141
- undefined,
2142
- this.hideThinkingBlock,
2143
- this.getMarkdownThemeWithSettings(),
2144
- );
2145
- this.streamingMessage = event.message;
2146
- this.chatContainer.addChild(this.streamingComponent);
2147
- this.streamingComponent.updateContent(this.streamingMessage);
2148
- this.ui.requestRender();
2149
- }
2150
- break;
2151
-
2152
- case "message_update":
2153
- if (this.streamingComponent && event.message.role === "assistant") {
2154
- this.streamingMessage = event.message;
2155
- this.streamingComponent.updateContent(this.streamingMessage);
2156
-
2157
- for (const content of this.streamingMessage.content) {
2158
- if (content.type === "toolCall") {
2159
- if (!this.pendingTools.has(content.id)) {
2160
- const component = new ToolExecutionComponent(
2161
- content.name,
2162
- content.arguments,
2163
- {
2164
- showImages: this.settingsManager.getShowImages(),
2165
- },
2166
- this.getRegisteredToolDefinition(content.name),
2167
- this.ui,
2168
- );
2169
- component.setExpanded(this.toolOutputExpanded);
2170
- this.chatContainer.addChild(component);
2171
- this.pendingTools.set(content.id, component);
2172
- } else {
2173
- const component = this.pendingTools.get(content.id);
2174
- if (component) {
2175
- component.updateArgs(content.arguments);
2176
- }
2177
- }
2178
- } else if (content.type === "serverToolUse") {
2179
- // Server-side tool (e.g., native web search) — show as pending tool execution
2180
- if (!this.pendingTools.has(content.id)) {
2181
- const component = new ToolExecutionComponent(
2182
- content.name,
2183
- content.input ?? {},
2184
- {
2185
- showImages: this.settingsManager.getShowImages(),
2186
- },
2187
- undefined,
2188
- this.ui,
2189
- );
2190
- component.setExpanded(this.toolOutputExpanded);
2191
- this.chatContainer.addChild(component);
2192
- this.pendingTools.set(content.id, component);
2193
- }
2194
- } else if (content.type === "webSearchResult") {
2195
- // Server-side tool result — resolve the pending server tool execution
2196
- const component = this.pendingTools.get(content.toolUseId);
2197
- if (component) {
2198
- const searchContent = content.content;
2199
- const isError = searchContent && typeof searchContent === "object" && "type" in (searchContent as any) && (searchContent as any).type === "web_search_tool_result_error";
2200
- const resultText = this.formatWebSearchResult(searchContent);
2201
- component.updateResult({
2202
- content: [{ type: "text", text: resultText }],
2203
- isError: !!isError,
2204
- });
2205
- this.pendingTools.delete(content.toolUseId);
2206
- }
2207
- }
2208
- }
2209
- this.ui.requestRender();
2210
- }
2211
- break;
2212
-
2213
- case "message_end":
2214
- if (event.message.role === "user") break;
2215
- if (this.streamingComponent && event.message.role === "assistant") {
2216
- this.streamingMessage = event.message;
2217
- let errorMessage: string | undefined;
2218
- if (this.streamingMessage.stopReason === "aborted") {
2219
- const retryAttempt = this.session.retryAttempt;
2220
- errorMessage =
2221
- retryAttempt > 0
2222
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
2223
- : "Operation aborted";
2224
- this.streamingMessage.errorMessage = errorMessage;
2225
- }
2226
- this.streamingComponent.updateContent(this.streamingMessage);
2227
-
2228
- if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2229
- if (!errorMessage) {
2230
- errorMessage = this.streamingMessage.errorMessage || "Error";
2231
- }
2232
- for (const [, component] of this.pendingTools.entries()) {
2233
- component.updateResult({
2234
- content: [{ type: "text", text: errorMessage }],
2235
- isError: true,
2236
- });
2237
- }
2238
- this.pendingTools.clear();
2239
- } else {
2240
- // Args are now complete - trigger diff computation for edit tools
2241
- for (const [, component] of this.pendingTools.entries()) {
2242
- component.setArgsComplete();
2243
- }
2244
- }
2245
- this.streamingComponent = undefined;
2246
- this.streamingMessage = undefined;
2247
- this.footer.invalidate();
2248
- }
2249
- this.ui.requestRender();
2250
- break;
2251
-
2252
- case "tool_execution_start": {
2253
- if (!this.pendingTools.has(event.toolCallId)) {
2254
- const component = new ToolExecutionComponent(
2255
- event.toolName,
2256
- event.args,
2257
- {
2258
- showImages: this.settingsManager.getShowImages(),
2259
- },
2260
- this.getRegisteredToolDefinition(event.toolName),
2261
- this.ui,
2262
- );
2263
- component.setExpanded(this.toolOutputExpanded);
2264
- this.chatContainer.addChild(component);
2265
- this.pendingTools.set(event.toolCallId, component);
2266
- this.ui.requestRender();
2267
- }
2268
- break;
2269
- }
2270
-
2271
- case "tool_execution_update": {
2272
- const component = this.pendingTools.get(event.toolCallId);
2273
- if (component) {
2274
- component.updateResult({ ...event.partialResult, isError: false }, true);
2275
- this.ui.requestRender();
2276
- }
2277
- break;
2278
- }
2279
-
2280
- case "tool_execution_end": {
2281
- const component = this.pendingTools.get(event.toolCallId);
2282
- if (component) {
2283
- component.updateResult({ ...event.result, isError: event.isError });
2284
- this.pendingTools.delete(event.toolCallId);
2285
- this.ui.requestRender();
2286
- }
2287
- break;
2288
- }
2289
-
2290
- case "agent_end":
2291
- if (this.loadingAnimation) {
2292
- this.loadingAnimation.stop();
2293
- this.loadingAnimation = undefined;
2294
- this.statusContainer.clear();
2295
- }
2296
- if (this.streamingComponent) {
2297
- this.chatContainer.removeChild(this.streamingComponent);
2298
- this.streamingComponent = undefined;
2299
- this.streamingMessage = undefined;
2300
- }
2301
- this.pendingTools.clear();
2302
-
2303
- await this.checkShutdownRequested();
2304
-
2305
- this.ui.requestRender();
2306
- break;
2307
-
2308
- case "auto_compaction_start": {
2309
- // Keep editor active; submissions are queued during compaction.
2310
- // Set up escape to abort auto-compaction
2311
- this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
2312
- this.defaultEditor.onEscape = () => {
2313
- this.session.abortCompaction();
2314
- };
2315
- // Show compacting indicator with reason
2316
- this.statusContainer.clear();
2317
- const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
2318
- this.autoCompactionLoader = new Loader(
2319
- this.ui,
2320
- (spinner) => theme.fg("accent", spinner),
2321
- (text) => theme.fg("muted", text),
2322
- `${reasonText}Auto-compacting... (${appKey(this.keybindings, "interrupt")} to cancel)`,
2323
- );
2324
- this.statusContainer.addChild(this.autoCompactionLoader);
2325
- this.ui.requestRender();
2326
- break;
2327
- }
2328
-
2329
- case "auto_compaction_end": {
2330
- // Restore escape handler
2331
- if (this.autoCompactionEscapeHandler) {
2332
- this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
2333
- this.autoCompactionEscapeHandler = undefined;
2334
- }
2335
- // Stop loader
2336
- if (this.autoCompactionLoader) {
2337
- this.autoCompactionLoader.stop();
2338
- this.autoCompactionLoader = undefined;
2339
- this.statusContainer.clear();
2340
- }
2341
- // Handle result
2342
- if (event.aborted) {
2343
- this.showStatus("Auto-compaction cancelled");
2344
- } else if (event.result) {
2345
- // Rebuild chat to show compacted state
2346
- this.chatContainer.clear();
2347
- this.rebuildChatFromMessages();
2348
- // Add compaction component at bottom so user sees it without scrolling
2349
- this.addMessageToChat({
2350
- role: "compactionSummary",
2351
- tokensBefore: event.result.tokensBefore,
2352
- summary: event.result.summary,
2353
- timestamp: Date.now(),
2354
- });
2355
- this.footer.invalidate();
2356
- } else if (event.errorMessage) {
2357
- // Compaction failed (e.g., quota exceeded, API error)
2358
- this.chatContainer.addChild(new Spacer(1));
2359
- this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
2360
- }
2361
- void this.flushCompactionQueue({ willRetry: event.willRetry });
2362
- this.ui.requestRender();
2363
- break;
2364
- }
2365
-
2366
- case "auto_retry_start": {
2367
- // Set up escape to abort retry
2368
- this.retryEscapeHandler = this.defaultEditor.onEscape;
2369
- this.defaultEditor.onEscape = () => {
2370
- this.session.abortRetry();
2371
- };
2372
- // Show retry indicator
2373
- this.statusContainer.clear();
2374
- const delaySeconds = Math.round(event.delayMs / 1000);
2375
- this.retryLoader = new Loader(
2376
- this.ui,
2377
- (spinner) => theme.fg("warning", spinner),
2378
- (text) => theme.fg("muted", text),
2379
- `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${appKey(this.keybindings, "interrupt")} to cancel)`,
2380
- );
2381
- this.statusContainer.addChild(this.retryLoader);
2382
- this.ui.requestRender();
2383
- break;
2384
- }
2385
-
2386
- case "auto_retry_end": {
2387
- // Restore escape handler
2388
- if (this.retryEscapeHandler) {
2389
- this.defaultEditor.onEscape = this.retryEscapeHandler;
2390
- this.retryEscapeHandler = undefined;
2391
- }
2392
- // Stop loader
2393
- if (this.retryLoader) {
2394
- this.retryLoader.stop();
2395
- this.retryLoader = undefined;
2396
- this.statusContainer.clear();
2397
- }
2398
- // Show error only on final failure (success shows normal response)
2399
- if (!event.success) {
2400
- this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
2401
- }
2402
- this.ui.requestRender();
2403
- break;
2404
- }
2405
-
2406
- case "fallback_provider_switch": {
2407
- this.showStatus(`Switched from ${event.from} → ${event.to} (${event.reason})`);
2408
- this.ui.requestRender();
2409
- break;
2410
- }
2411
-
2412
- case "fallback_provider_restored": {
2413
- this.showStatus(`Restored to ${event.provider}`);
2414
- this.ui.requestRender();
2415
- break;
2416
- }
2417
-
2418
- case "fallback_chain_exhausted": {
2419
- this.showError(event.reason);
2420
- this.ui.requestRender();
2421
- break;
2422
- }
2423
- }
1984
+ await handleAgentEvent(this as any, event);
2424
1985
  }
2425
1986
 
2426
1987
  /** Extract text content from a user message */
@@ -3299,73 +2860,20 @@ export class InteractiveMode {
3299
2860
  }
3300
2861
 
3301
2862
  private async handleModelCommand(searchTerm?: string): Promise<void> {
3302
- if (!searchTerm) {
3303
- this.showModelSelector();
3304
- return;
3305
- }
3306
-
3307
- const model = await this.findExactModelMatch(searchTerm);
3308
- if (model) {
3309
- try {
3310
- await this.session.setModel(model);
3311
- this.footer.invalidate();
3312
- this.updateEditorBorderColor();
3313
- this.showStatus(`Model: ${model.id}`);
3314
- this.checkDaxnutsEasterEgg(model);
3315
- } catch (error) {
3316
- this.showError(error instanceof Error ? error.message : String(error));
3317
- }
3318
- return;
3319
- }
3320
-
3321
- this.showModelSelector(searchTerm);
2863
+ await handleModelCommandController(this, searchTerm);
3322
2864
  }
3323
2865
 
3324
2866
  private async findExactModelMatch(searchTerm: string): Promise<Model<any> | undefined> {
3325
- const term = searchTerm.trim();
3326
- if (!term) return undefined;
3327
-
3328
- let targetProvider: string | undefined;
3329
- let targetModelId = "";
3330
-
3331
- if (term.includes("/")) {
3332
- const parts = term.split("/", 2);
3333
- targetProvider = parts[0]?.trim().toLowerCase();
3334
- targetModelId = parts[1]?.trim().toLowerCase() ?? "";
3335
- } else {
3336
- targetModelId = term.toLowerCase();
3337
- }
3338
-
3339
- if (!targetModelId) return undefined;
3340
-
3341
- const models = await this.getModelCandidates();
3342
- const exactMatches = models.filter((item) => {
3343
- const idMatch = item.id.toLowerCase() === targetModelId;
3344
- const providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider;
3345
- return idMatch && providerMatch;
3346
- });
3347
-
3348
- return exactMatches.length === 1 ? exactMatches[0] : undefined;
2867
+ return findExactModelMatchController(this, searchTerm);
3349
2868
  }
3350
2869
 
3351
2870
  private async getModelCandidates(): Promise<Model<any>[]> {
3352
- if (this.session.scopedModels.length > 0) {
3353
- return this.session.scopedModels.map((scoped) => scoped.model);
3354
- }
3355
-
3356
- this.session.modelRegistry.refresh();
3357
- try {
3358
- return await this.session.modelRegistry.getAvailable();
3359
- } catch {
3360
- return [];
3361
- }
2871
+ return getModelCandidatesController(this);
3362
2872
  }
3363
2873
 
3364
2874
  /** Update the footer's available provider count from current model candidates */
3365
2875
  private async updateAvailableProviderCount(): Promise<void> {
3366
- const models = await this.getModelCandidates();
3367
- const uniqueProviders = new Set(models.map((m) => m.provider));
3368
- this.footerDataProvider.setAvailableProviderCount(uniqueProviders.size);
2876
+ await updateAvailableProviderCountController(this);
3369
2877
  }
3370
2878
 
3371
2879
  private showModelSelector(initialSearchInput?: string): void {
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.39.0",
3
+ "version": "2.40.0",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -78,6 +78,17 @@ export default function AsyncJobs(pi: ExtensionAPI) {
78
78
  });
79
79
  });
80
80
 
81
+ pi.on("session_before_switch", async () => {
82
+ if (manager) {
83
+ // Cancel all running background jobs — their results are no longer
84
+ // relevant to the new session and would produce wasteful follow-up
85
+ // notifications that trigger empty LLM turns (#1642).
86
+ for (const job of manager.getRunningJobs()) {
87
+ manager.cancel(job.id);
88
+ }
89
+ }
90
+ });
91
+
81
92
  pi.on("session_shutdown", async () => {
82
93
  if (manager) {
83
94
  manager.shutdown();
@@ -70,7 +70,7 @@ async function writeEnvKey(filePath: string, key: string, value: string): Promis
70
70
  // Re-export from env-utils.ts so existing consumers still work.
71
71
  // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
72
72
  // into modules that only need env-checking (e.g. files.ts during reports).
73
- import { checkExistingEnvKeys } from "./env-utils.js";
73
+ import { checkExistingEnvKeys } from "./gsd/env-utils.js";
74
74
  export { checkExistingEnvKeys };
75
75
 
76
76
  /**
@@ -390,6 +390,8 @@ export interface WidgetStateAccessors {
390
390
  getCmdCtx(): ExtensionCommandContext | null;
391
391
  getBasePath(): string;
392
392
  isVerbose(): boolean;
393
+ /** True while newSession() is in-flight — render must not access session state. */
394
+ isSessionSwitching(): boolean;
393
395
  }
394
396
 
395
397
  export function updateProgressWidget(
@@ -460,6 +462,14 @@ export function updateProgressWidget(
460
462
  render(width: number): string[] {
461
463
  if (cachedLines && cachedWidth === width) return cachedLines;
462
464
 
465
+ // While newSession() is in-flight, session state is mid-mutation.
466
+ // Accessing cmdCtx.sessionManager or cmdCtx.getContextUsage() can
467
+ // block the render loop and freeze the TUI. Return the last cached
468
+ // frame (or an empty frame on first render) until the switch settles.
469
+ if (accessors.isSessionSwitching()) {
470
+ return cachedLines ?? [];
471
+ }
472
+
463
473
  const ui = makeUI(theme, width);
464
474
  const lines: string[] = [];
465
475
  const pad = INDENT.base;