@gajae-code/coding-agent 0.5.1 → 0.5.3

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 (165) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +6 -0
  4. package/dist/types/cli/setup-cli.d.ts +8 -1
  5. package/dist/types/commands/setup.d.ts +7 -0
  6. package/dist/types/config/file-lock.d.ts +24 -2
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/models-config-schema.d.ts +5 -0
  9. package/dist/types/config/settings-schema.d.ts +62 -0
  10. package/dist/types/dap/client.d.ts +2 -1
  11. package/dist/types/edit/read-file.d.ts +6 -0
  12. package/dist/types/eval/js/context-manager.d.ts +3 -0
  13. package/dist/types/eval/js/executor.d.ts +1 -0
  14. package/dist/types/exec/bash-executor.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  19. package/dist/types/lsp/types.d.ts +2 -0
  20. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +2 -0
  22. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  23. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  24. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  25. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  26. package/dist/types/modes/interactive-mode.d.ts +1 -1
  27. package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
  28. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  29. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  30. package/dist/types/modes/theme/theme.d.ts +1 -0
  31. package/dist/types/modes/types.d.ts +1 -1
  32. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  33. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  34. package/dist/types/runtime-mcp/types.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +17 -1
  36. package/dist/types/session/artifacts.d.ts +4 -1
  37. package/dist/types/session/history-storage.d.ts +2 -2
  38. package/dist/types/session/session-manager.d.ts +10 -1
  39. package/dist/types/session/streaming-output.d.ts +5 -0
  40. package/dist/types/setup/credential-import.d.ts +79 -0
  41. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  42. package/dist/types/task/executor.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +1 -1
  44. package/dist/types/tools/bash.d.ts +1 -0
  45. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  46. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  47. package/dist/types/tools/subagent-render.d.ts +7 -1
  48. package/dist/types/tools/subagent.d.ts +21 -0
  49. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  50. package/dist/types/web/search/index.d.ts +4 -4
  51. package/dist/types/web/search/provider.d.ts +16 -20
  52. package/dist/types/web/search/providers/base.d.ts +2 -1
  53. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  54. package/dist/types/web/search/types.d.ts +14 -2
  55. package/package.json +7 -7
  56. package/scripts/build-binary.ts +7 -0
  57. package/src/async/job-manager.ts +153 -39
  58. package/src/cli/args.ts +2 -0
  59. package/src/cli/fast-help.ts +2 -0
  60. package/src/cli/setup-cli.ts +138 -3
  61. package/src/commands/setup.ts +5 -1
  62. package/src/commands/ultragoal.ts +3 -1
  63. package/src/config/file-lock-gc.ts +14 -2
  64. package/src/config/file-lock.ts +63 -13
  65. package/src/config/model-profile-activation.ts +15 -3
  66. package/src/config/model-profiles.ts +15 -15
  67. package/src/config/model-registry.ts +21 -1
  68. package/src/config/models-config-schema.ts +1 -0
  69. package/src/config/settings-schema.ts +62 -0
  70. package/src/dap/client.ts +105 -64
  71. package/src/dap/session.ts +44 -7
  72. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  73. package/src/edit/read-file.ts +19 -1
  74. package/src/eval/js/context-manager.ts +228 -65
  75. package/src/eval/js/executor.ts +2 -0
  76. package/src/eval/js/index.ts +1 -0
  77. package/src/eval/js/worker-core.ts +10 -6
  78. package/src/eval/py/executor.ts +68 -19
  79. package/src/eval/py/kernel.ts +46 -22
  80. package/src/eval/py/runner.py +68 -14
  81. package/src/exec/bash-executor.ts +49 -13
  82. package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
  83. package/src/gjc-runtime/launch-tmux.ts +3 -4
  84. package/src/gjc-runtime/ralplan-runtime.ts +174 -12
  85. package/src/gjc-runtime/state-runtime.ts +2 -1
  86. package/src/gjc-runtime/state-writer.ts +254 -7
  87. package/src/gjc-runtime/tmux-gc.ts +88 -38
  88. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  89. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  90. package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
  91. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  92. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  93. package/src/harness-control-plane/owner.ts +3 -2
  94. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  95. package/src/hooks/skill-state.ts +121 -2
  96. package/src/internal-urls/artifact-protocol.ts +10 -1
  97. package/src/internal-urls/docs-index.generated.ts +14 -10
  98. package/src/lsp/client.ts +64 -26
  99. package/src/lsp/defaults.json +1 -0
  100. package/src/lsp/index.ts +2 -1
  101. package/src/lsp/lspmux.ts +33 -9
  102. package/src/lsp/types.ts +2 -0
  103. package/src/main.ts +14 -4
  104. package/src/modes/acp/acp-agent.ts +4 -2
  105. package/src/modes/bridge/bridge-mode.ts +23 -1
  106. package/src/modes/components/assistant-message.ts +10 -2
  107. package/src/modes/components/bash-execution.ts +5 -1
  108. package/src/modes/components/eval-execution.ts +5 -1
  109. package/src/modes/components/history-search.ts +5 -2
  110. package/src/modes/components/model-selector.ts +60 -2
  111. package/src/modes/components/oauth-selector.ts +5 -0
  112. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  113. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  114. package/src/modes/components/skill-message.ts +24 -16
  115. package/src/modes/components/tool-execution.ts +6 -0
  116. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  117. package/src/modes/controllers/input-controller.ts +5 -0
  118. package/src/modes/controllers/selector-controller.ts +86 -2
  119. package/src/modes/interactive-mode.ts +11 -1
  120. package/src/modes/rpc/rpc-mode.ts +132 -18
  121. package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
  122. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  123. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  124. package/src/modes/theme/defaults/claude-code.json +100 -0
  125. package/src/modes/theme/defaults/codex.json +100 -0
  126. package/src/modes/theme/defaults/index.ts +6 -0
  127. package/src/modes/theme/defaults/opencode.json +102 -0
  128. package/src/modes/theme/theme.ts +2 -2
  129. package/src/modes/types.ts +1 -1
  130. package/src/modes/utils/ui-helpers.ts +5 -2
  131. package/src/prompts/agents/executor.md +5 -2
  132. package/src/runtime/process-lifecycle.ts +400 -0
  133. package/src/runtime-mcp/manager.ts +164 -50
  134. package/src/runtime-mcp/transports/http.ts +12 -11
  135. package/src/runtime-mcp/transports/stdio.ts +64 -38
  136. package/src/runtime-mcp/types.ts +3 -0
  137. package/src/sdk.ts +39 -1
  138. package/src/session/agent-session.ts +190 -33
  139. package/src/session/artifacts.ts +17 -2
  140. package/src/session/blob-store.ts +36 -2
  141. package/src/session/history-storage.ts +32 -11
  142. package/src/session/session-manager.ts +99 -31
  143. package/src/session/streaming-output.ts +54 -3
  144. package/src/setup/credential-import.ts +429 -0
  145. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  146. package/src/slash-commands/builtin-registry.ts +30 -3
  147. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  148. package/src/task/executor.ts +7 -1
  149. package/src/task/render.ts +18 -7
  150. package/src/tools/archive-reader.ts +10 -1
  151. package/src/tools/ask.ts +4 -2
  152. package/src/tools/bash.ts +11 -4
  153. package/src/tools/browser/tab-supervisor.ts +22 -0
  154. package/src/tools/browser.ts +38 -4
  155. package/src/tools/cron.ts +1 -1
  156. package/src/tools/read.ts +11 -12
  157. package/src/tools/sqlite-reader.ts +19 -5
  158. package/src/tools/subagent-render.ts +119 -29
  159. package/src/tools/subagent.ts +147 -7
  160. package/src/tools/ultragoal-ask-guard.ts +39 -0
  161. package/src/web/search/index.ts +25 -25
  162. package/src/web/search/provider.ts +178 -87
  163. package/src/web/search/providers/base.ts +2 -1
  164. package/src/web/search/providers/openai-compatible.ts +151 -0
  165. package/src/web/search/types.ts +47 -22
@@ -131,6 +131,11 @@ export class MCPAddWizard extends Container {
131
131
  | null = null;
132
132
  #onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
133
133
  #onRenderCallback: (() => void) | null = null;
134
+ #disposed = false;
135
+ #transitionTimers = new Set<NodeJS.Timeout>();
136
+ #healthCheckSpinner?: NodeJS.Timeout;
137
+ #healthCheckTimeout?: NodeJS.Timeout;
138
+ #asyncGeneration = 0;
134
139
 
135
140
  constructor(
136
141
  onComplete: (name: string, config: MCPServerConfig, scope: Scope) => void,
@@ -178,6 +183,39 @@ export class MCPAddWizard extends Container {
178
183
  this.#renderStep();
179
184
  }
180
185
 
186
+ dispose(): void {
187
+ if (this.#disposed) return;
188
+ this.#disposed = true;
189
+ this.#asyncGeneration += 1;
190
+ for (const timer of this.#transitionTimers) {
191
+ clearTimeout(timer);
192
+ }
193
+ this.#transitionTimers.clear();
194
+ this.#clearHealthCheckTimers();
195
+ super.dispose();
196
+ }
197
+
198
+ #scheduleTransition(callback: () => void, delay: number): void {
199
+ if (this.#disposed) return;
200
+ const timer = setTimeout(() => {
201
+ this.#transitionTimers.delete(timer);
202
+ if (this.#disposed) return;
203
+ callback();
204
+ }, delay);
205
+ this.#transitionTimers.add(timer);
206
+ }
207
+
208
+ #clearHealthCheckTimers(): void {
209
+ if (this.#healthCheckSpinner) {
210
+ clearInterval(this.#healthCheckSpinner);
211
+ this.#healthCheckSpinner = undefined;
212
+ }
213
+ if (this.#healthCheckTimeout) {
214
+ clearTimeout(this.#healthCheckTimeout);
215
+ this.#healthCheckTimeout = undefined;
216
+ }
217
+ }
218
+
181
219
  #requestRender(): void {
182
220
  this.#onRenderCallback?.();
183
221
  }
@@ -941,6 +979,8 @@ export class MCPAddWizard extends Container {
941
979
  * Test connection and automatically detect if auth is needed.
942
980
  */
943
981
  async #testConnectionAndDetectAuth(): Promise<void> {
982
+ if (this.#disposed) return;
983
+ const generation = ++this.#asyncGeneration;
944
984
  const testConfig = this.#buildServerConfig();
945
985
 
946
986
  if (!this.#onTestConnectionCallback) {
@@ -954,6 +994,7 @@ export class MCPAddWizard extends Container {
954
994
  try {
955
995
  // Try to connect - timeout is handled by the transport layer (5 seconds)
956
996
  await this.#onTestConnectionCallback(testConfig);
997
+ if (this.#disposed || generation !== this.#asyncGeneration) return;
957
998
 
958
999
  // Success! No auth required
959
1000
  this.#contentContainer.clear();
@@ -962,7 +1003,7 @@ export class MCPAddWizard extends Container {
962
1003
  this.#contentContainer.addChild(new Text("No authentication required", 0, 0));
963
1004
  this.#contentContainer.addChild(new Spacer(1));
964
1005
 
965
- setTimeout(() => {
1006
+ this.#scheduleTransition(() => {
966
1007
  this.#state.authMethod = "none";
967
1008
  this.#currentStep = "scope";
968
1009
  this.#selectedIndex = 0;
@@ -978,10 +1019,12 @@ export class MCPAddWizard extends Container {
978
1019
  if (!oauth && this.#state.transport !== "stdio" && this.#state.url) {
979
1020
  try {
980
1021
  oauth = await discoverOAuthEndpoints(this.#state.url, authResult.authServerUrl);
1022
+ if (this.#disposed || generation !== this.#asyncGeneration) return;
981
1023
  } catch {
982
1024
  // Ignore discovery failures and fallback to manual auth.
983
1025
  }
984
1026
  }
1027
+ if (this.#disposed || generation !== this.#asyncGeneration) return;
985
1028
 
986
1029
  if (oauth) {
987
1030
  this.#state.oauthAuthUrl = oauth.authorizationUrl;
@@ -1019,7 +1062,7 @@ export class MCPAddWizard extends Container {
1019
1062
  this.#contentContainer.addChild(new Spacer(1));
1020
1063
  this.#contentContainer.addChild(new Text(theme.fg("muted", "Adding server anyway..."), 0, 0));
1021
1064
 
1022
- setTimeout(() => {
1065
+ this.#scheduleTransition(() => {
1023
1066
  this.#state.authMethod = "none";
1024
1067
  this.#currentStep = "scope";
1025
1068
  this.#selectedIndex = 0;
@@ -1107,6 +1150,8 @@ export class MCPAddWizard extends Container {
1107
1150
  }
1108
1151
 
1109
1152
  async #launchOAuthFlow(): Promise<void> {
1153
+ if (this.#disposed) return;
1154
+ const generation = ++this.#asyncGeneration;
1110
1155
  if (!this.#onOAuthCallback) {
1111
1156
  this.#contentContainer.clear();
1112
1157
  this.#contentContainer.addChild(new Text(theme.fg("error", "OAuth flow not available"), 0, 0));
@@ -1150,6 +1195,7 @@ export class MCPAddWizard extends Container {
1150
1195
  this.#state.oauthClientSecret,
1151
1196
  this.#state.oauthScopes,
1152
1197
  );
1198
+ if (this.#disposed || generation !== this.#asyncGeneration) return;
1153
1199
 
1154
1200
  // Store credential ID + any dynamically-registered client credentials,
1155
1201
  // so the final mcp.json entry persists everything needed for refresh.
@@ -1168,7 +1214,8 @@ export class MCPAddWizard extends Container {
1168
1214
  this.#contentContainer.addChild(healthText);
1169
1215
 
1170
1216
  let spinnerIndex = 0;
1171
- const spinner = setInterval(() => {
1217
+ this.#healthCheckSpinner = setInterval(() => {
1218
+ if (this.#disposed || generation !== this.#asyncGeneration) return;
1172
1219
  healthText.setText(
1173
1220
  theme.fg("muted", `${spinnerFrames[spinnerIndex % spinnerFrames.length]} Checking server connection...`),
1174
1221
  );
@@ -1181,7 +1228,7 @@ export class MCPAddWizard extends Container {
1181
1228
  if (this.#onTestConnectionCallback) {
1182
1229
  try {
1183
1230
  const { promise: timeoutPromise, reject: timeoutReject } = Promise.withResolvers<never>();
1184
- const timer = setTimeout(
1231
+ this.#healthCheckTimeout = setTimeout(
1185
1232
  () => timeoutReject(new Error("Health check timed out after 10 seconds")),
1186
1233
  10_000,
1187
1234
  );
@@ -1191,15 +1238,19 @@ export class MCPAddWizard extends Container {
1191
1238
  timeoutPromise,
1192
1239
  ]);
1193
1240
  } finally {
1194
- clearTimeout(timer);
1241
+ if (this.#healthCheckTimeout) {
1242
+ clearTimeout(this.#healthCheckTimeout);
1243
+ this.#healthCheckTimeout = undefined;
1244
+ }
1195
1245
  }
1196
1246
  } catch (error) {
1197
1247
  healthPassed = false;
1198
1248
  healthError = sanitize(error instanceof Error ? error.message : String(error));
1199
1249
  }
1200
1250
  }
1251
+ if (this.#disposed || generation !== this.#asyncGeneration) return;
1201
1252
 
1202
- clearInterval(spinner);
1253
+ this.#clearHealthCheckTimers();
1203
1254
  if (healthPassed) {
1204
1255
  healthText.setText(theme.fg("success", "✓ Health check passed"));
1205
1256
  } else {
@@ -1210,7 +1261,7 @@ export class MCPAddWizard extends Container {
1210
1261
  this.#requestRender();
1211
1262
 
1212
1263
  // Move to scope selection after short delay
1213
- setTimeout(
1264
+ this.#scheduleTransition(
1214
1265
  () => {
1215
1266
  this.#currentStep = "scope";
1216
1267
  this.#selectedIndex = 0;
@@ -1,6 +1,6 @@
1
1
  import type { TextContent } from "@gajae-code/ai";
2
2
  import type { Component } from "@gajae-code/tui";
3
- import { Box, Container, Markdown, Spacer, Text } from "@gajae-code/tui";
3
+ import { Box, Container, Markdown, Spacer, Text, truncateToWidth } from "@gajae-code/tui";
4
4
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
5
5
  import type { CustomMessage, SkillPromptDetails } from "../../session/messages";
6
6
 
@@ -39,29 +39,37 @@ export class SkillMessageComponent extends Container {
39
39
  this.addChild(this.#box);
40
40
  this.#box.clear();
41
41
 
42
- const label = theme.fg("customMessageLabel", theme.bold("[skill]"));
43
- this.#box.addChild(new Text(label, 0, 0));
44
- this.#box.addChild(new Spacer(1));
45
-
46
42
  const details = this.message.details;
43
+ const name = details?.name ?? "unknown";
47
44
  const args = details?.args?.trim();
48
- const infoLines = [
49
- `Skill: ${details?.name ?? "unknown"}`,
50
- args ? `Args: ${args}` : undefined,
51
- details?.path ? `Path: ${details.path}` : undefined,
52
- typeof details?.lineCount === "number" ? `Prompt: ${details.lineCount} lines` : undefined,
53
- ].filter((line): line is string => Boolean(line));
54
45
 
55
- this.#box.addChild(
56
- new Markdown(infoLines.join("\n"), 0, 0, getMarkdownTheme(), {
57
- color: (value: string) => theme.fg("customMessageText", value),
58
- }),
59
- );
46
+ // Single compact line: `[skill] <name>: <args>`. The summary is the
47
+ // args the user typed; with none, just `[skill] <name>`. Collapsed to
48
+ // one line path / line-count / full prompt body are debugging detail
49
+ // and only render once expanded.
50
+ const summary = args ? truncateToWidth(args.replace(/\s+/g, " "), 72) : undefined;
51
+ const header = `${theme.fg("customMessageLabel", theme.bold("[skill]"))} ${theme.fg("customMessageText", name)}`;
52
+ const headerText = summary ? `${header}${theme.fg("customMessageText", `: ${summary}`)}` : header;
53
+ this.#box.addChild(new Text(headerText, 0, 0));
60
54
 
61
55
  if (!this.#expanded) {
62
56
  return;
63
57
  }
64
58
 
59
+ const detailLines = [
60
+ details?.path ? `Path: ${details.path}` : undefined,
61
+ typeof details?.lineCount === "number" ? `Prompt: ${details.lineCount} lines` : undefined,
62
+ ].filter((line): line is string => Boolean(line));
63
+
64
+ if (detailLines.length > 0) {
65
+ this.#box.addChild(new Spacer(1));
66
+ this.#box.addChild(
67
+ new Markdown(detailLines.join("\n"), 0, 0, getMarkdownTheme(), {
68
+ color: (value: string) => theme.fg("customMessageText", value),
69
+ }),
70
+ );
71
+ }
72
+
65
73
  const text = this.#extractText();
66
74
  if (!text) {
67
75
  return;
@@ -375,6 +375,7 @@ export class ToolExecutionComponent extends Container {
375
375
  this.#renderState.spinnerFrame = this.#spinnerFrame;
376
376
  this.#ui.requestRender();
377
377
  }, 80);
378
+ this.#spinnerInterval?.unref?.();
378
379
  } else if (!needsSpinner && this.#spinnerInterval) {
379
380
  clearInterval(this.#spinnerInterval);
380
381
  this.#spinnerInterval = undefined;
@@ -394,6 +395,11 @@ export class ToolExecutionComponent extends Container {
394
395
  this.#editDiffAbort = undefined;
395
396
  }
396
397
 
398
+ override dispose(): void {
399
+ this.stopAnimation();
400
+ super.dispose();
401
+ }
402
+
397
403
  setExpanded(expanded: boolean): void {
398
404
  this.#expanded = expanded;
399
405
  this.#updateDisplay();
@@ -36,9 +36,20 @@ export class ExtensionUiController {
36
36
  #hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
37
37
  #hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
38
38
  #hookSelectorMouseReportingEnabled = false;
39
+ #activeHookCustomComponent?: Component & { dispose?(): void };
40
+ #activeHookCustomOverlay?: OverlayHandle;
39
41
 
40
42
  constructor(private ctx: InteractiveModeContext) {}
41
43
 
44
+ #clearActiveHookCustom(): void {
45
+ const component = this.#activeHookCustomComponent;
46
+ const overlay = this.#activeHookCustomOverlay;
47
+ this.#activeHookCustomComponent = undefined;
48
+ this.#activeHookCustomOverlay = undefined;
49
+ component?.dispose?.();
50
+ overlay?.hide();
51
+ }
52
+
42
53
  /**
43
54
  * Initialize the hook system with TUI-based UI context.
44
55
  */
@@ -301,7 +312,11 @@ export class ExtensionUiController {
301
312
  spacerWhenEmpty: boolean,
302
313
  leadingSpacer: boolean,
303
314
  ): void {
304
- container.clear();
315
+ // Detach (not dispose): hook widgets are persistent instances owned by the
316
+ // #hookWidgets* maps and re-added on every rebuild. Disposal happens only on
317
+ // explicit removal (#removeHookWidget) or clearHookWidgets(), so a rebuild must
318
+ // not tear down a still-live widget (e.g. an extension CancellableLoader timer).
319
+ container.detachAll();
305
320
 
306
321
  if (widgets.size === 0) {
307
322
  if (spacerWhenEmpty) {
@@ -665,6 +680,9 @@ export class ExtensionUiController {
665
680
  : undefined,
666
681
  },
667
682
  );
683
+ // Detach (not dispose) the reusable editor before mounting the transient hook UI, so the
684
+ // disposing clear() only tears down a prior transient — the editor is re-added intact on close.
685
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
668
686
  this.ctx.editorContainer.clear();
669
687
  this.ctx.editorContainer.addChild(this.ctx.hookSelector);
670
688
  this.ctx.ui.setFocus(this.ctx.hookSelector);
@@ -743,6 +761,9 @@ export class ExtensionUiController {
743
761
  tui: this.ctx.ui,
744
762
  },
745
763
  );
764
+ // Detach (not dispose) the reusable editor before mounting the transient hook UI, so the
765
+ // disposing clear() only tears down a prior transient — the editor is re-added intact on close.
766
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
746
767
  this.ctx.editorContainer.clear();
747
768
  this.ctx.editorContainer.addChild(this.ctx.hookInput);
748
769
  this.ctx.ui.setFocus(this.ctx.hookInput);
@@ -791,6 +812,9 @@ export class ExtensionUiController {
791
812
  editorOptions,
792
813
  );
793
814
 
815
+ // Detach (not dispose) the reusable editor before mounting the transient hook UI, so the
816
+ // disposing clear() only tears down a prior transient — the editor is re-added intact on close.
817
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
794
818
  this.ctx.editorContainer.clear();
795
819
  this.ctx.editorContainer.addChild(this.ctx.hookEditor);
796
820
  this.ctx.ui.setFocus(this.ctx.hookEditor);
@@ -840,15 +864,12 @@ export class ExtensionUiController {
840
864
 
841
865
  const { promise, resolve } = Promise.withResolvers<T>();
842
866
  let component: (Component & { dispose?(): void }) | undefined;
843
- let overlayHandle: OverlayHandle | undefined;
844
867
  let closed = false;
845
868
 
846
869
  const close = (result: T) => {
847
870
  if (closed) return;
848
871
  closed = true;
849
- component?.dispose?.();
850
- overlayHandle?.hide();
851
- overlayHandle = undefined;
872
+ this.#clearActiveHookCustom();
852
873
  if (!options?.overlay) {
853
874
  this.ctx.editorContainer.clear();
854
875
  this.ctx.editorContainer.addChild(this.ctx.editor);
@@ -859,14 +880,16 @@ export class ExtensionUiController {
859
880
  resolve(result);
860
881
  };
861
882
 
883
+ this.#clearActiveHookCustom();
862
884
  Promise.try(() => factory(this.ctx.ui, theme, keybindings, close)).then(c => {
863
885
  if (closed) {
864
886
  c.dispose?.();
865
887
  return;
866
888
  }
867
889
  component = c;
890
+ this.#activeHookCustomComponent = c;
868
891
  if (options?.overlay) {
869
- overlayHandle = this.ctx.ui.showOverlay(component, {
892
+ this.#activeHookCustomOverlay = this.ctx.ui.showOverlay(component, {
870
893
  anchor: "bottom-center",
871
894
  width: "100%",
872
895
  maxHeight: "100%",
@@ -874,6 +897,9 @@ export class ExtensionUiController {
874
897
  });
875
898
  return;
876
899
  }
900
+ // Detach (not dispose) the reusable editor before mounting the transient hook UI, so the
901
+ // disposing clear() only tears down a prior transient — the editor is re-added intact on close.
902
+ this.ctx.editorContainer.detachChild(this.ctx.editor);
877
903
  this.ctx.editorContainer.clear();
878
904
  this.ctx.editorContainer.addChild(component);
879
905
  this.ctx.ui.setFocus(component);
@@ -895,6 +921,7 @@ export class ExtensionUiController {
895
921
  }
896
922
 
897
923
  clearHookWidgets(): void {
924
+ this.#clearActiveHookCustom();
898
925
  for (const widget of this.#hookWidgetsAbove.values()) {
899
926
  widget.dispose?.();
900
927
  }
@@ -887,6 +887,11 @@ export class InputController {
887
887
  this.ctx.session.agent.hideThinkingSummary = this.ctx.hideThinkingBlock;
888
888
 
889
889
  // Rebuild chat from session messages
890
+ // Detach the live streaming component before the disposing clear() so the
891
+ // component we re-add below is not torn down (detach != dispose).
892
+ if (this.ctx.streamingComponent) {
893
+ this.ctx.chatContainer.detachChild(this.ctx.streamingComponent);
894
+ }
890
895
  this.ctx.chatContainer.clear();
891
896
  this.ctx.rebuildChatFromMessages();
892
897
 
@@ -32,13 +32,20 @@ import {
32
32
  import type { InteractiveModeContext } from "../../modes/types";
33
33
  import { type SessionInfo, SessionManager } from "../../session/session-manager";
34
34
  import { FileSessionStorage } from "../../session/session-storage";
35
+ import { discoverExternalCredentials, formatDiscoverySummary, importCredentials } from "../../setup/credential-import";
35
36
  import {
36
37
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
37
38
  MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
38
39
  MODEL_ONBOARDING_SETUP_COMMAND,
39
40
  } from "../../setup/model-onboarding-guidance";
40
41
  import { addApiCompatibleProvider, formatProviderSetupResult } from "../../setup/provider-onboarding";
41
- import { isSearchProviderPreference, setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
42
+ import {
43
+ isConfigurableSearchProviderId,
44
+ isSearchProviderPreference,
45
+ setPreferredImageProvider,
46
+ setPreferredSearchProvider,
47
+ setSearchFallbackProviders,
48
+ } from "../../tools";
42
49
  import { setSessionTerminalTitle } from "../../utils/title-generator";
43
50
  import { AgentDashboard } from "../components/agent-dashboard";
44
51
  import { AssistantMessageComponent } from "../components/assistant-message";
@@ -128,6 +135,8 @@ export class SelectorController {
128
135
  this.showCustomProviderWizard();
129
136
  } else if (action === "oauth-login") {
130
137
  void this.showOAuthSelector("login");
138
+ } else if (action === "import-credentials") {
139
+ void this.#handleCredentialImport();
131
140
  } else {
132
141
  this.ctx.showStatus(formatProviderOnboardingCommandGuide());
133
142
  }
@@ -141,6 +150,69 @@ export class SelectorController {
141
150
  });
142
151
  }
143
152
 
153
+ async #handleCredentialImport(): Promise<void> {
154
+ this.ctx.showStatus("Scanning for existing Claude Code / Codex CLI credentials…");
155
+ const result = await discoverExternalCredentials();
156
+ const summaryLines = formatDiscoverySummary(result);
157
+
158
+ if (result.importable.length === 0) {
159
+ this.ctx.chatContainer.addChild(new Spacer(1));
160
+ for (const line of summaryLines) {
161
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
162
+ }
163
+ this.ctx.chatContainer.addChild(
164
+ new Text(
165
+ theme.fg(
166
+ "warning",
167
+ "No importable Claude/Codex credentials found. Use /login or add a custom provider.",
168
+ ),
169
+ 1,
170
+ 0,
171
+ ),
172
+ );
173
+ this.ctx.ui.requestRender();
174
+ return;
175
+ }
176
+
177
+ const confirmed = await this.ctx.showHookConfirm(
178
+ `Import ${result.importable.length} credential(s)?`,
179
+ summaryLines.join("\n"),
180
+ );
181
+ if (!confirmed) {
182
+ this.ctx.showStatus("Credential import cancelled.");
183
+ return;
184
+ }
185
+
186
+ const summary = await importCredentials(result.importable, (provider, credential) =>
187
+ this.ctx.session.modelRegistry.authStorage.upsertCredential(provider, credential),
188
+ );
189
+ await this.ctx.session.modelRegistry.refresh();
190
+
191
+ this.ctx.chatContainer.addChild(new Spacer(1));
192
+ for (const credential of summary.imported) {
193
+ this.ctx.chatContainer.addChild(
194
+ new Text(
195
+ theme.fg("success", `${theme.status.success} Imported ${credential.provider} (${credential.source})`),
196
+ 1,
197
+ 0,
198
+ ),
199
+ );
200
+ }
201
+ for (const failure of summary.failed) {
202
+ this.ctx.chatContainer.addChild(
203
+ new Text(
204
+ theme.fg("error", `${theme.status.error} Failed ${failure.credential.provider}: ${failure.error}`),
205
+ 1,
206
+ 0,
207
+ ),
208
+ );
209
+ }
210
+ if (summary.imported.length > 0) {
211
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
212
+ }
213
+ this.ctx.ui.requestRender();
214
+ }
215
+
144
216
  showCustomProviderWizard(): void {
145
217
  this.showSelector(done => {
146
218
  let wizard: CustomProviderWizardComponent;
@@ -507,6 +579,13 @@ export class SelectorController {
507
579
  setPreferredSearchProvider(value);
508
580
  }
509
581
  break;
582
+ case "web_search.fallback":
583
+ if (Array.isArray(value)) {
584
+ setSearchFallbackProviders(
585
+ value.filter(item => typeof item === "string" && isConfigurableSearchProviderId(item)),
586
+ );
587
+ }
588
+ break;
510
589
  case "providers.image":
511
590
  if (
512
591
  value === "auto" ||
@@ -605,7 +684,12 @@ export class SelectorController {
605
684
  done();
606
685
  this.ctx.ui.requestRender();
607
686
  },
608
- { ...options, sessionId: this.ctx.session.sessionId },
687
+ {
688
+ ...options,
689
+ sessionId: this.ctx.session.sessionId,
690
+ isFastForProvider: provider => this.ctx.session.isFastForProvider(provider),
691
+ isFastForSubagentProvider: provider => this.ctx.session.isFastForSubagentProvider(provider),
692
+ },
609
693
  );
610
694
  return { component: selector, focus: selector };
611
695
  });
@@ -283,7 +283,7 @@ export class InteractiveMode implements InteractiveModeContext {
283
283
  }
284
284
  autoCompactionEscapeHandler?: () => void;
285
285
  retryEscapeHandler?: () => void;
286
- retryCountdownTimer?: ReturnType<typeof setInterval>;
286
+ retryCountdownTimer?: NodeJS.Timeout;
287
287
  unsubscribe?: () => void;
288
288
  onInputCallback?: (input: SubmittedUserInput) => void;
289
289
  optimisticUserMessageSignature: string | undefined = undefined;
@@ -709,6 +709,16 @@ export class InteractiveMode implements InteractiveModeContext {
709
709
  if (this.#pendingSubmittedInput) return;
710
710
  if (this.editor.getText().trim().length > 0) return;
711
711
  if ((this.pendingImages?.length ?? 0) > 0) return;
712
+ // Never fire an autonomous continuation prompt() while the session is
713
+ // busy. A wedged/orphaned subagent turn can leave isStreaming stuck true;
714
+ // firing prompt() here throws AgentBusyError, which submitInteractiveInput
715
+ // surfaces as a red "Error: Agent is already processing…" and then loops
716
+ // back to getUserInput(), re-arming this timer — an infinite error spam.
717
+ // Re-arm and only fire once the session returns to idle.
718
+ if (this.session.isStreaming || this.session.isCompacting) {
719
+ this.#scheduleGoalContinuation();
720
+ return;
721
+ }
712
722
  const latestState = this.session.getGoalModeState();
713
723
  if (!latestState?.enabled || latestState.goal.status !== "active") return;
714
724
  this.#goalContinuationTurnInFlight = true;