@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.
- package/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
{
|
|
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?:
|
|
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;
|