@gajae-code/coding-agent 0.15.2 → 0.15.4
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 +43 -1
- package/dist/types/config/autorouting-tier-map.d.ts +0 -9
- package/dist/types/config/file-lock.d.ts +7 -1
- package/dist/types/config/keybindings.d.ts +13 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +65 -1
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/action-registry.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +16 -1
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/ui-language.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/broker/session-index.d.ts +18 -0
- package/dist/types/sdk/bus/index.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/cli/session-cli.d.ts +1 -1
- package/dist/types/sdk/lifecycle/service.d.ts +32 -5
- package/dist/types/sdk/session.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/fallback-chain-controller.d.ts +8 -0
- package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
- package/dist/types/tools/ask.d.ts +9 -0
- package/dist/types/utils/clipboard.d.ts +9 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/autorouting-tier-map.ts +0 -9
- package/src/config/file-lock.ts +15 -1
- package/src/config/keybindings.ts +13 -3
- package/src/config/model-registry.ts +132 -21
- package/src/config/model-resolver.ts +2 -1
- package/src/config/models-config-schema.ts +2 -2
- package/src/config/settings-schema.ts +66 -1
- package/src/config/settings.ts +1 -1
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +235 -4
- package/src/coordinator-mcp/server.ts +402 -2
- package/src/dap/client.ts +19 -8
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/deep-interview-stage.ts +20 -1
- package/src/gjc-runtime/deep-interview-state.ts +3 -1
- package/src/gjc-runtime/ralplan-runtime.ts +6 -1
- package/src/gjc-runtime/session-state-lock.ts +11 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/client.ts +39 -4
- package/src/main.ts +42 -11
- package/src/modes/action-registry.ts +7 -0
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/components/iterm-pet-transport.ts +4 -2
- package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/tool-status-header.ts +149 -41
- package/src/modes/components/welcome.ts +8 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +141 -7
- package/src/modes/controllers/plan-mode-controller.ts +12 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
- package/src/modes/controllers/selector-controller.ts +11 -2
- package/src/modes/interactive-mode.ts +88 -1
- package/src/modes/shared/oauth-url-copy.ts +33 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/ui-language.ts +44 -3
- package/src/runtime-mcp/oauth-flow.ts +19 -8
- package/src/runtime-mcp/transports/stdio.ts +27 -13
- package/src/sdk/acp/adapter.ts +29 -2
- package/src/sdk/broker/broker.ts +26 -6
- package/src/sdk/broker/lifecycle-ledger.ts +28 -1
- package/src/sdk/broker/lifecycle.ts +664 -5
- package/src/sdk/broker/session-index.ts +246 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/index.ts +135 -38
- package/src/sdk/bus/natives-augment.d.ts +20 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +3 -0
- package/src/sdk/cli/session-cli.ts +34 -7
- package/src/sdk/lifecycle/service.ts +166 -11
- package/src/sdk/mcp/server.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/sdk/protocol/operation-registry.ts +2 -1
- package/src/sdk/session.ts +91 -7
- package/src/session/agent-session.ts +473 -66
- package/src/session/fallback-chain-controller.ts +17 -0
- package/src/setup/frictionless-onboarding.ts +4 -4
- package/src/slash-commands/aside-command.test.ts +366 -0
- package/src/slash-commands/builtin-registry.ts +19 -0
- package/src/slash-commands/helpers/aside.ts +299 -0
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +54 -6
- package/src/utils/clipboard.ts +37 -28
package/src/lsp/client.ts
CHANGED
|
@@ -23,6 +23,8 @@ const clients = new Map<string, LspClient>();
|
|
|
23
23
|
const killedClients = new WeakSet<LspClient>();
|
|
24
24
|
const clientLocks = new Map<string, Promise<LspClient>>();
|
|
25
25
|
const fileOperationLocks = new Map<string, Promise<void>>();
|
|
26
|
+
const initializingClients = new Set<LspClient>();
|
|
27
|
+
let shutdownGeneration = 0;
|
|
26
28
|
const transportClosedErrors = new WeakMap<LspClient, Error>();
|
|
27
29
|
const LSP_TRANSPORT_CLOSED_MESSAGE = "LSP transport closed";
|
|
28
30
|
let lspCleanupOwner: (() => void) | undefined;
|
|
@@ -280,7 +282,13 @@ async function writeMessage(
|
|
|
280
282
|
if (terminalError) throw terminalError;
|
|
281
283
|
|
|
282
284
|
try {
|
|
283
|
-
|
|
285
|
+
// FileSink.write() returns `number | Promise<number>`: under backpressure
|
|
286
|
+
// (e.g. a large didOpen/didChange payload) it returns a promise whose
|
|
287
|
+
// rejection — typically EPIPE when the server dies mid-write — would
|
|
288
|
+
// otherwise be discarded here and surface as an unhandled rejection that
|
|
289
|
+
// takes down the whole process. Await it so every failure mode reaches
|
|
290
|
+
// the transport-closed mapping below.
|
|
291
|
+
await client.proc.stdin.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n${content}`);
|
|
284
292
|
await client.proc.stdin.flush();
|
|
285
293
|
} catch (error) {
|
|
286
294
|
if (isKnownSinkPeerClosedError(error)) {
|
|
@@ -509,6 +517,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
509
517
|
// Create new client with lock
|
|
510
518
|
let clientPromise!: Promise<LspClient>;
|
|
511
519
|
clientPromise = (async () => {
|
|
520
|
+
const creationGeneration = shutdownGeneration;
|
|
512
521
|
const baseCommand = config.resolvedCommand ?? config.command;
|
|
513
522
|
const baseArgs = config.args ?? [];
|
|
514
523
|
|
|
@@ -516,6 +525,9 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
516
525
|
const { command, args, env } = isLspmuxSupported(baseCommand)
|
|
517
526
|
? await getLspmuxCommand(baseCommand, baseArgs, cwd)
|
|
518
527
|
: { command: baseCommand, args: baseArgs };
|
|
528
|
+
if (creationGeneration !== shutdownGeneration) {
|
|
529
|
+
throw new Error("LSP client shutdown");
|
|
530
|
+
}
|
|
519
531
|
|
|
520
532
|
const owner = spawnOwnedProcess([command, ...args], {
|
|
521
533
|
cwd,
|
|
@@ -561,7 +573,12 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
561
573
|
killedClients.add(client);
|
|
562
574
|
return originalKill(...args);
|
|
563
575
|
};
|
|
564
|
-
|
|
576
|
+
initializingClients.add(client);
|
|
577
|
+
if (creationGeneration !== shutdownGeneration) {
|
|
578
|
+
initializingClients.delete(client);
|
|
579
|
+
await shutdownClientInstance(client);
|
|
580
|
+
throw new Error("LSP client shutdown");
|
|
581
|
+
}
|
|
565
582
|
|
|
566
583
|
// Register crash recovery - remove client on process exit
|
|
567
584
|
proc.exited.then(async () => {
|
|
@@ -636,15 +653,26 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
636
653
|
ensureLspCleanup();
|
|
637
654
|
const terminalError = transportClosedErrors.get(client);
|
|
638
655
|
if (terminalError) throw terminalError;
|
|
656
|
+
if (creationGeneration !== shutdownGeneration) {
|
|
657
|
+
throw new Error("LSP client shutdown");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Publish to the cache only after the handshake completes: callers that
|
|
661
|
+
// hit the `clients` map must never observe a client whose initialize is
|
|
662
|
+
// still in flight. Concurrent callers instead share `clientLocks` and
|
|
663
|
+
// wait for initialization, so a server that dies mid-handshake rejects
|
|
664
|
+
// every waiter instead of handing them a transport that is about to
|
|
665
|
+
// break underneath them.
|
|
666
|
+
clients.set(key, client);
|
|
639
667
|
|
|
640
668
|
return client;
|
|
641
669
|
} catch (err) {
|
|
642
670
|
// Clean up on initialization failure
|
|
643
|
-
deleteCachedClient(key, client);
|
|
644
671
|
deleteClientLock(key, clientPromise);
|
|
645
672
|
await shutdownClientInstance(client);
|
|
646
673
|
throw err;
|
|
647
674
|
} finally {
|
|
675
|
+
initializingClients.delete(client);
|
|
648
676
|
deleteClientLock(key, clientPromise);
|
|
649
677
|
}
|
|
650
678
|
})();
|
|
@@ -999,11 +1027,18 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
999
1027
|
*/
|
|
1000
1028
|
export async function shutdownAll(): Promise<void> {
|
|
1001
1029
|
stopIdleChecker();
|
|
1030
|
+
shutdownGeneration += 1;
|
|
1031
|
+
const inFlightPromises = Array.from(clientLocks.values());
|
|
1032
|
+
const initializingToShutdown = Array.from(initializingClients);
|
|
1002
1033
|
clientLocks.clear();
|
|
1003
1034
|
fileOperationLocks.clear();
|
|
1004
1035
|
const clientsToShutdown = Array.from(clients.values());
|
|
1005
1036
|
clients.clear();
|
|
1006
|
-
await Promise.allSettled(
|
|
1037
|
+
await Promise.allSettled([
|
|
1038
|
+
...clientsToShutdown.map(client => shutdownClientInstance(client)),
|
|
1039
|
+
...initializingToShutdown.map(client => shutdownClientInstance(client)),
|
|
1040
|
+
...inFlightPromises,
|
|
1041
|
+
]);
|
|
1007
1042
|
}
|
|
1008
1043
|
|
|
1009
1044
|
/** Status of an LSP server */
|
package/src/main.ts
CHANGED
|
@@ -343,7 +343,11 @@ export interface InteractiveModeNotify {
|
|
|
343
343
|
export async function submitInteractiveInput(
|
|
344
344
|
mode: Pick<
|
|
345
345
|
InteractiveMode,
|
|
346
|
-
|
|
346
|
+
| "markPendingSubmissionStarted"
|
|
347
|
+
| "finishPendingSubmission"
|
|
348
|
+
| "showError"
|
|
349
|
+
| "checkShutdownRequested"
|
|
350
|
+
| "waitForAgentEnd"
|
|
347
351
|
>,
|
|
348
352
|
session: Pick<AgentSession, "prompt" | "promptCustomMessage">,
|
|
349
353
|
input: SubmittedUserInput,
|
|
@@ -357,15 +361,35 @@ export async function submitInteractiveInput(
|
|
|
357
361
|
if (!input.started && !mode.markPendingSubmissionStarted(input)) {
|
|
358
362
|
return;
|
|
359
363
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
364
|
+
const agentEnd = mode.waitForAgentEnd();
|
|
365
|
+
try {
|
|
366
|
+
const prompt = input.customType
|
|
367
|
+
? session.promptCustomMessage({
|
|
368
|
+
customType: input.customType,
|
|
369
|
+
content: input.text,
|
|
370
|
+
display: input.display ?? false,
|
|
371
|
+
attribution: "agent",
|
|
372
|
+
})
|
|
373
|
+
: session.prompt(input.text, { images: input.images });
|
|
374
|
+
const promptResult = prompt.then(
|
|
375
|
+
() => ({ type: "prompt" as const }),
|
|
376
|
+
(error: unknown) => ({ type: "error" as const, error }),
|
|
377
|
+
);
|
|
378
|
+
const result = await Promise.race([
|
|
379
|
+
promptResult,
|
|
380
|
+
agentEnd.promise.then(() => ({ type: "agent_end" as const })),
|
|
381
|
+
]);
|
|
382
|
+
if (result.type === "error") {
|
|
383
|
+
const errorMessage = result.error instanceof Error ? result.error.message : "Unknown error occurred";
|
|
384
|
+
mode.showError(errorMessage);
|
|
385
|
+
} else if (result.type === "agent_end") {
|
|
386
|
+
void prompt.catch((error: unknown) => {
|
|
387
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
388
|
+
mode.showError(errorMessage);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
} finally {
|
|
392
|
+
agentEnd.dispose();
|
|
369
393
|
}
|
|
370
394
|
} catch (error: unknown) {
|
|
371
395
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
@@ -1698,8 +1722,15 @@ export async function runRootCommand(
|
|
|
1698
1722
|
);
|
|
1699
1723
|
},
|
|
1700
1724
|
};
|
|
1725
|
+
let rootStartupCacheAdmissionAttempted = false;
|
|
1701
1726
|
sessionOptions.authStorage = authStorage;
|
|
1702
1727
|
sessionOptions.modelRegistry = modelRegistry;
|
|
1728
|
+
sessionOptions.modelRegistryStartupMutation = {
|
|
1729
|
+
owner: "cli-root",
|
|
1730
|
+
onAttempt: () => {
|
|
1731
|
+
rootStartupCacheAdmissionAttempted = true;
|
|
1732
|
+
},
|
|
1733
|
+
};
|
|
1703
1734
|
sessionOptions.hasUI = isInteractive;
|
|
1704
1735
|
sessionOptions.notificationHostModeSupported = isInteractive;
|
|
1705
1736
|
sessionOptions.sdkHostModeSupported = isInteractive;
|
|
@@ -1818,7 +1849,7 @@ export async function runRootCommand(
|
|
|
1818
1849
|
// discovery arms; running these concurrently contends for the event loop and stretches
|
|
1819
1850
|
// every parallel arm by ~30ms. Startup model profiles do their own foreground refresh
|
|
1820
1851
|
// before activation so project-scoped defaults can resolve freshly discovered models.
|
|
1821
|
-
if (!context?.skipPostCreateModelRefresh) {
|
|
1852
|
+
if (!context?.skipPostCreateModelRefresh && !rootStartupCacheAdmissionAttempted) {
|
|
1822
1853
|
modelRegistry.refreshInBackground();
|
|
1823
1854
|
}
|
|
1824
1855
|
return result;
|
|
@@ -48,6 +48,7 @@ export const APP_ACTION_METADATA: readonly ActionMetadata[] = [
|
|
|
48
48
|
action("app.clipboard.pasteText", "Paste text from configured clipboard", "Clipboard", ["composer"]),
|
|
49
49
|
action("app.clipboard.copyLine", "Copy line", "Clipboard", ["composer"]),
|
|
50
50
|
action("app.clipboard.copyPrompt", "Copy prompt", "Clipboard", ["composer"]),
|
|
51
|
+
action("app.clipboard.copyOAuthUrl", "Copy OAuth URL", "Clipboard", ["composer"]),
|
|
51
52
|
action("app.session.new", "New session", "Session", ["composer"]),
|
|
52
53
|
action("app.session.tree", "Session tree", "Session", ["composer"]),
|
|
53
54
|
action("app.session.fork", "Branch from message", "Session", ["composer"]),
|
|
@@ -71,6 +72,7 @@ export const APP_ACTION_METADATA: readonly ActionMetadata[] = [
|
|
|
71
72
|
action("app.transcript.nextTurn", "Next transcript turn", "Transcript", ["composer"]),
|
|
72
73
|
action("app.mode.cycle", "Cycle mode", "Mode", ["composer"]),
|
|
73
74
|
action("app.tasks.toggle", "Toggle tasks pane", "Jobs", ["composer"]),
|
|
75
|
+
action("app.todo.toggle", "Toggle todo expansion", "Tasks", ["composer"]),
|
|
74
76
|
action("app.queue.togglePane", "Toggle queue pane", "Messages", ["composer"]),
|
|
75
77
|
action("app.message.sendNow", "Send message now", "Messages", ["composer"]),
|
|
76
78
|
];
|
|
@@ -163,4 +165,9 @@ export class ActionRegistry<Context> {
|
|
|
163
165
|
if (group) this.#busyGroups.delete(group);
|
|
164
166
|
}
|
|
165
167
|
}
|
|
168
|
+
|
|
169
|
+
async executeFresh(id: AppKeybinding): Promise<boolean> {
|
|
170
|
+
this.#availability.delete(id);
|
|
171
|
+
return this.execute(id);
|
|
172
|
+
}
|
|
166
173
|
}
|
|
@@ -72,6 +72,7 @@ export interface HookSelectorOptions {
|
|
|
72
72
|
customInput?: {
|
|
73
73
|
optionLabel: string;
|
|
74
74
|
onSubmit: (text: string) => void;
|
|
75
|
+
allowEmpty?: boolean;
|
|
75
76
|
};
|
|
76
77
|
clarificationInput?: {
|
|
77
78
|
optionLabel: string;
|
|
@@ -390,7 +391,7 @@ export class HookSelectorComponent extends Container {
|
|
|
390
391
|
#outline: boolean;
|
|
391
392
|
#scrollTitleRows: number | undefined;
|
|
392
393
|
#inlineEditorMaxHeight: number | undefined;
|
|
393
|
-
#customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
|
|
394
|
+
#customInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
394
395
|
#clarificationInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
395
396
|
#activeInput: { onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
396
397
|
#inputArea: Container;
|
|
@@ -122,11 +122,13 @@ export function isItermCandidate(
|
|
|
122
122
|
): boolean {
|
|
123
123
|
if (!tty) return false;
|
|
124
124
|
const ci = env.CI?.trim().toLowerCase();
|
|
125
|
-
if (
|
|
125
|
+
if (env.CI !== undefined && ci !== "0" && ci !== "false") return false;
|
|
126
126
|
// These variables are untrusted hints, especially when forwarded over SSH.
|
|
127
127
|
// They may start the capability probe, but only an OSC 1337 reply containing
|
|
128
128
|
// the File capability can make the transport available.
|
|
129
|
-
|
|
129
|
+
const lcTerminal = env.LC_TERMINAL?.trim().toLowerCase();
|
|
130
|
+
const termProgram = env.TERM_PROGRAM?.trim().toLowerCase();
|
|
131
|
+
return lcTerminal === "iterm2" || termProgram === "iterm.app" || termProgram === "iterm2";
|
|
130
132
|
}
|
|
131
133
|
export function createNativePetTransport(o: {
|
|
132
134
|
ui: NativePetUi;
|
|
@@ -129,10 +129,17 @@ export class MCPAddWizard extends Container {
|
|
|
129
129
|
clientId: string,
|
|
130
130
|
clientSecret: string,
|
|
131
131
|
scopes: string,
|
|
132
|
+
signal: AbortSignal,
|
|
132
133
|
) => Promise<MCPAddWizardOAuthResult>)
|
|
133
134
|
| null = null;
|
|
134
135
|
#onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
|
|
135
136
|
#onRenderCallback: (() => void) | null = null;
|
|
137
|
+
#onCommandPaletteCallback: ((keyData: string) => boolean) | null = null;
|
|
138
|
+
#onOAuthCredentialCleanupCallback: ((credentialId: string) => Promise<void> | void) | null = null;
|
|
139
|
+
#onOAuthCredentialCleanupErrorCallback: ((credentialId: string, error: unknown) => void) | null = null;
|
|
140
|
+
#oauthAbortController: AbortController | undefined;
|
|
141
|
+
#oauthCredentialCleanupStarted = false;
|
|
142
|
+
#completed = false;
|
|
136
143
|
#disposed = false;
|
|
137
144
|
#transitionTimers = new Set<NodeJS.Timeout>();
|
|
138
145
|
#healthCheckSpinner?: NodeJS.Timeout;
|
|
@@ -149,10 +156,14 @@ export class MCPAddWizard extends Container {
|
|
|
149
156
|
clientId: string,
|
|
150
157
|
clientSecret: string,
|
|
151
158
|
scopes: string,
|
|
159
|
+
signal: AbortSignal,
|
|
152
160
|
) => Promise<MCPAddWizardOAuthResult>,
|
|
153
161
|
onTestConnection?: (config: MCPServerConfig) => Promise<void>,
|
|
154
162
|
onRender?: () => void,
|
|
163
|
+
onCommandPalette?: (keyData: string) => boolean,
|
|
155
164
|
initialName?: string,
|
|
165
|
+
onOAuthCredentialCleanup?: (credentialId: string) => Promise<void> | void,
|
|
166
|
+
onOAuthCredentialCleanupError?: (credentialId: string, error: unknown) => void,
|
|
156
167
|
) {
|
|
157
168
|
super();
|
|
158
169
|
this.#onCompleteCallback = onComplete;
|
|
@@ -160,6 +171,9 @@ export class MCPAddWizard extends Container {
|
|
|
160
171
|
this.#onOAuthCallback = onOAuth ?? null;
|
|
161
172
|
this.#onTestConnectionCallback = onTestConnection ?? null;
|
|
162
173
|
this.#onRenderCallback = onRender ?? null;
|
|
174
|
+
this.#onCommandPaletteCallback = onCommandPalette ?? null;
|
|
175
|
+
this.#onOAuthCredentialCleanupCallback = onOAuthCredentialCleanup ?? null;
|
|
176
|
+
this.#onOAuthCredentialCleanupErrorCallback = onOAuthCredentialCleanupError ?? null;
|
|
163
177
|
if (initialName && initialName.trim().length > 0) {
|
|
164
178
|
this.#state.name = initialName.trim();
|
|
165
179
|
this.#currentStep = "transport";
|
|
@@ -190,6 +204,15 @@ export class MCPAddWizard extends Container {
|
|
|
190
204
|
if (this.#disposed) return;
|
|
191
205
|
this.#disposed = true;
|
|
192
206
|
this.#asyncGeneration += 1;
|
|
207
|
+
this.#oauthAbortController?.abort(new Error("OAuth flow cancelled"));
|
|
208
|
+
this.#oauthAbortController = undefined;
|
|
209
|
+
if (!this.#completed && !this.#oauthCredentialCleanupStarted && this.#state.oauthCredentialId) {
|
|
210
|
+
this.#oauthCredentialCleanupStarted = true;
|
|
211
|
+
const credentialId = this.#state.oauthCredentialId;
|
|
212
|
+
void Promise.resolve(this.#onOAuthCredentialCleanupCallback?.(credentialId)).catch(error => {
|
|
213
|
+
this.#onOAuthCredentialCleanupErrorCallback?.(credentialId, error);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
193
216
|
for (const timer of this.#transitionTimers) {
|
|
194
217
|
clearTimeout(timer);
|
|
195
218
|
}
|
|
@@ -518,6 +541,10 @@ export class MCPAddWizard extends Container {
|
|
|
518
541
|
|
|
519
542
|
// Handle Escape (always handled by wizard)
|
|
520
543
|
if (matchesAppInterrupt(keyData)) {
|
|
544
|
+
if (this.#oauthAbortController) {
|
|
545
|
+
this.#onCancelCallback();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
521
548
|
if (this.#currentStep === "name") {
|
|
522
549
|
// Cancel wizard
|
|
523
550
|
this.#onCancelCallback();
|
|
@@ -528,6 +555,8 @@ export class MCPAddWizard extends Container {
|
|
|
528
555
|
return;
|
|
529
556
|
}
|
|
530
557
|
|
|
558
|
+
if (this.#onCommandPaletteCallback?.(keyData) === true) return;
|
|
559
|
+
|
|
531
560
|
// If we have an input field, let it handle the input
|
|
532
561
|
if (this.#inputField) {
|
|
533
562
|
// Handle Enter to proceed
|
|
@@ -635,6 +664,7 @@ export class MCPAddWizard extends Container {
|
|
|
635
664
|
break;
|
|
636
665
|
case "oauth-scopes":
|
|
637
666
|
this.#state.oauthScopes = value; // Optional
|
|
667
|
+
this.#inputField = null;
|
|
638
668
|
// Launch OAuth flow
|
|
639
669
|
void this.#launchOAuthFlow();
|
|
640
670
|
return;
|
|
@@ -1153,7 +1183,7 @@ export class MCPAddWizard extends Container {
|
|
|
1153
1183
|
}
|
|
1154
1184
|
|
|
1155
1185
|
async #launchOAuthFlow(): Promise<void> {
|
|
1156
|
-
if (this.#disposed) return;
|
|
1186
|
+
if (this.#disposed || this.#oauthAbortController) return;
|
|
1157
1187
|
const generation = ++this.#asyncGeneration;
|
|
1158
1188
|
if (!this.#onOAuthCallback) {
|
|
1159
1189
|
this.#contentContainer.clear();
|
|
@@ -1189,6 +1219,8 @@ export class MCPAddWizard extends Container {
|
|
|
1189
1219
|
this.#contentContainer.addChild(new Text(theme.fg("muted", "(Press Esc to cancel)"), 0, 0));
|
|
1190
1220
|
this.#requestRender();
|
|
1191
1221
|
|
|
1222
|
+
const oauthAbortController = new AbortController();
|
|
1223
|
+
this.#oauthAbortController = oauthAbortController;
|
|
1192
1224
|
try {
|
|
1193
1225
|
// Call OAuth handler
|
|
1194
1226
|
const oauthResult = await this.#onOAuthCallback(
|
|
@@ -1198,6 +1230,7 @@ export class MCPAddWizard extends Container {
|
|
|
1198
1230
|
this.#state.oauthClientId,
|
|
1199
1231
|
this.#state.oauthClientSecret,
|
|
1200
1232
|
this.#state.oauthScopes,
|
|
1233
|
+
oauthAbortController.signal,
|
|
1201
1234
|
);
|
|
1202
1235
|
if (this.#disposed || generation !== this.#asyncGeneration) return;
|
|
1203
1236
|
|
|
@@ -1276,6 +1309,7 @@ export class MCPAddWizard extends Container {
|
|
|
1276
1309
|
healthPassed ? 1000 : 2000,
|
|
1277
1310
|
);
|
|
1278
1311
|
} catch (error) {
|
|
1312
|
+
if (this.#disposed || generation !== this.#asyncGeneration) return;
|
|
1279
1313
|
// Show error with options to retry or go back
|
|
1280
1314
|
const errorMsg = sanitize(error instanceof Error ? error.message : String(error));
|
|
1281
1315
|
this.#contentContainer.clear();
|
|
@@ -1311,11 +1345,14 @@ export class MCPAddWizard extends Container {
|
|
|
1311
1345
|
// Set up as a selector step
|
|
1312
1346
|
this.#selectedIndex = 0;
|
|
1313
1347
|
this.#currentStep = "oauth-error";
|
|
1348
|
+
} finally {
|
|
1349
|
+
if (this.#oauthAbortController === oauthAbortController) this.#oauthAbortController = undefined;
|
|
1314
1350
|
}
|
|
1315
1351
|
}
|
|
1316
1352
|
|
|
1317
1353
|
#complete(): void {
|
|
1318
1354
|
if (!this.#state.scope) return;
|
|
1355
|
+
this.#completed = true;
|
|
1319
1356
|
|
|
1320
1357
|
// Build the config
|
|
1321
1358
|
const config: MCPServerConfig = this.#buildConfig();
|
|
@@ -1009,6 +1009,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1009
1009
|
options = [
|
|
1010
1010
|
{ value: "en", label: uiString(language, "settings.language.english") },
|
|
1011
1011
|
{ value: "ko", label: uiString(language, "settings.language.korean") },
|
|
1012
|
+
{ value: "ja", label: uiString(language, "settings.language.japanese") },
|
|
1012
1013
|
];
|
|
1013
1014
|
}
|
|
1014
1015
|
if (def.path === "statusLine.preset") {
|
|
@@ -709,6 +709,27 @@ export class StatusLineComponent implements Component {
|
|
|
709
709
|
return partsWidth + sepTotal + 2 + capWidth;
|
|
710
710
|
}
|
|
711
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Render the overflow cue for evicted status segments.
|
|
714
|
+
*
|
|
715
|
+
* Deliberately bypasses `#renderStatusGroup`: that primitive spends two
|
|
716
|
+
* padding cells plus any end cap before content (see `#groupWidth`), so it
|
|
717
|
+
* cannot produce marker-only output at one or two columns. Emits the bare
|
|
718
|
+
* marker with no padding and no caps so the final `truncateToWidth` in
|
|
719
|
+
* `render()` stays a defensive guard rather than the thing that decides
|
|
720
|
+
* whether the cue survives.
|
|
721
|
+
*
|
|
722
|
+
* `…+N` needs `2 + digits(N)` cells. When that does not fit, the count is
|
|
723
|
+
* intentionally omitted rather than truncated into a wrong number, so the
|
|
724
|
+
* cue is either exact or countless — never misleading.
|
|
725
|
+
*/
|
|
726
|
+
#renderOverflowMarker(count: number, available: number): string {
|
|
727
|
+
if (available <= 0 || count <= 0) return "";
|
|
728
|
+
const withCount = `…+${count}`;
|
|
729
|
+
const text = visibleWidth(withCount) <= available ? withCount : "…";
|
|
730
|
+
return theme.fg("dim", text);
|
|
731
|
+
}
|
|
732
|
+
|
|
712
733
|
#renderStatusGroup(
|
|
713
734
|
parts: string[],
|
|
714
735
|
direction: "left" | "right",
|
|
@@ -855,54 +876,84 @@ export class StatusLineComponent implements Component {
|
|
|
855
876
|
const seg = precollected ?? this.#collectStatusSegments(width, this.#resolveSettings());
|
|
856
877
|
const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
|
|
857
878
|
const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
|
|
858
|
-
const left = [...seg.left];
|
|
859
|
-
const leftIds = [...seg.leftSegIds];
|
|
860
|
-
const right = [...seg.right];
|
|
861
879
|
const topFillWidth = Math.max(0, width);
|
|
862
880
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Evict against `budget` using the existing configured-order semantics
|
|
883
|
+
* (shrink path, then pop right, then pop left) and report how many
|
|
884
|
+
* segments were dropped. Runs from the original segment lists each time so
|
|
885
|
+
* the marker reservation can be recomputed without compounding evictions.
|
|
886
|
+
*/
|
|
887
|
+
const layout = (budget: number) => {
|
|
888
|
+
const left = [...seg.left];
|
|
889
|
+
const leftIds = [...seg.leftSegIds];
|
|
890
|
+
const right = [...seg.right];
|
|
891
|
+
let leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
892
|
+
let rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
893
|
+
const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
|
|
894
|
+
let dropped = 0;
|
|
895
|
+
|
|
896
|
+
if (topFillWidth > 0) {
|
|
897
|
+
// Shrink path before dropping right-side telemetry — path is the only elastic segment,
|
|
898
|
+
// and presets such as default-usage should not hide usage just because cwd is long.
|
|
899
|
+
const pathIdx = leftIds.indexOf("path");
|
|
900
|
+
if (pathIdx >= 0 && totalWidth() > budget) {
|
|
901
|
+
const overflow = totalWidth() - budget;
|
|
902
|
+
const shrunk = this.#shrinkPathToWidth(left[pathIdx], ctx, overflow);
|
|
903
|
+
if (shrunk !== null) {
|
|
904
|
+
left[pathIdx] = previewHighlightSegment === "path" ? `\x1b[7m${shrunk}\x1b[27m` : shrunk;
|
|
905
|
+
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
while (totalWidth() > budget && right.length > 0) {
|
|
909
|
+
right.pop();
|
|
910
|
+
dropped += 1;
|
|
911
|
+
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
912
|
+
}
|
|
913
|
+
while (totalWidth() > budget && left.length > 0) {
|
|
914
|
+
left.pop();
|
|
915
|
+
leftIds.pop();
|
|
916
|
+
dropped += 1;
|
|
876
917
|
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
877
918
|
}
|
|
878
919
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
920
|
+
return { left, right, leftWidth, rightWidth, dropped };
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// First pass with the full budget establishes whether anything is lost at
|
|
924
|
+
// all. Only after loss is detected do we reserve marker cells, so a rail
|
|
925
|
+
// that already fits is byte-identical to before.
|
|
926
|
+
let placed = layout(topFillWidth);
|
|
927
|
+
let reserved = 0;
|
|
928
|
+
if (placed.dropped > 0 && topFillWidth > 0) {
|
|
929
|
+
// Re-evict against the reduced budget so the cue never overflows the
|
|
930
|
+
// rail. Reserving cells can push out another segment, which can in turn
|
|
931
|
+
// widen the count (9 -> 10 gains a digit), so converge instead of
|
|
932
|
+
// reserving once: otherwise the exact count would degrade to a bare
|
|
933
|
+
// marker even when the rail had room for it.
|
|
934
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
935
|
+
const needed = Math.min(topFillWidth, visibleWidth(`…+${placed.dropped}`));
|
|
936
|
+
if (needed <= reserved) break;
|
|
937
|
+
reserved = needed;
|
|
938
|
+
placed = layout(Math.max(0, topFillWidth - reserved));
|
|
887
939
|
}
|
|
888
940
|
}
|
|
941
|
+
const marker = this.#renderOverflowMarker(placed.dropped, reserved);
|
|
889
942
|
|
|
890
|
-
const leftGroup = this.#renderStatusGroup(left, "left", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
891
|
-
const rightGroup = this.#renderStatusGroup(right, "right", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
892
|
-
if (!leftGroup && !rightGroup) return
|
|
943
|
+
const leftGroup = this.#renderStatusGroup(placed.left, "left", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
944
|
+
const rightGroup = this.#renderStatusGroup(placed.right, "right", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
945
|
+
if (!leftGroup && !rightGroup) return marker;
|
|
893
946
|
|
|
894
|
-
if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
|
|
895
|
-
return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
|
|
947
|
+
if (topFillWidth === 0 || placed.left.length === 0 || placed.right.length === 0) {
|
|
948
|
+
return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup + marker;
|
|
896
949
|
}
|
|
897
950
|
|
|
898
|
-
|
|
899
|
-
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
900
|
-
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
|
|
951
|
+
const gapWidth = Math.max(1, topFillWidth - placed.leftWidth - placed.rightWidth - visibleWidth(marker));
|
|
901
952
|
const sessionName = seg.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
|
|
902
953
|
const accentHex = sessionName ? getSessionAccentHex(sessionName) : undefined;
|
|
903
954
|
const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
|
|
904
955
|
const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
|
|
905
|
-
return leftGroup + gapFill + rightGroup;
|
|
956
|
+
return leftGroup + gapFill + rightGroup + marker;
|
|
906
957
|
}
|
|
907
958
|
|
|
908
959
|
/**
|
|
@@ -986,7 +1037,9 @@ export class StatusLineComponent implements Component {
|
|
|
986
1037
|
|
|
987
1038
|
const packedRows: string[][] = [];
|
|
988
1039
|
let current: string[] = [];
|
|
989
|
-
|
|
1040
|
+
let dropped = 0;
|
|
1041
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
1042
|
+
const item = items[index];
|
|
990
1043
|
if (current.length === 0) {
|
|
991
1044
|
current.push(item.content);
|
|
992
1045
|
continue;
|
|
@@ -997,16 +1050,67 @@ export class StatusLineComponent implements Component {
|
|
|
997
1050
|
} else {
|
|
998
1051
|
packedRows.push(current);
|
|
999
1052
|
current = [item.content];
|
|
1000
|
-
if (packedRows.length >= maxRows)
|
|
1053
|
+
if (packedRows.length >= maxRows) {
|
|
1054
|
+
// `current` holds the item that opened the row we cannot admit,
|
|
1055
|
+
// and everything after it is never visited: all of it is lost.
|
|
1056
|
+
dropped = items.length - index;
|
|
1057
|
+
current = [];
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1001
1060
|
}
|
|
1002
1061
|
}
|
|
1003
|
-
if (
|
|
1004
|
-
packedRows.push(current);
|
|
1062
|
+
if (current.length > 0) {
|
|
1063
|
+
if (packedRows.length < maxRows) packedRows.push(current);
|
|
1064
|
+
else dropped += current.length;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Prune every row down to what actually fits. Without this, an oversized
|
|
1068
|
+
// row survives here and the defensive truncation in `render()` trims it
|
|
1069
|
+
// into a lone ellipsis, which reads exactly like the overflow cue on a row
|
|
1070
|
+
// that is not supposed to carry one. Emptied rows are removed rather than
|
|
1071
|
+
// emitted blank, so at tiny widths the marker ends up alone on one row.
|
|
1072
|
+
for (let index = packedRows.length - 1; index >= 0; index -= 1) {
|
|
1073
|
+
const row = packedRows[index];
|
|
1074
|
+
while (row.length > 0 && this.#groupWidth(row, seg.leftCapWidth, seg.leftSepWidth) > topFillWidth) {
|
|
1075
|
+
row.pop();
|
|
1076
|
+
dropped += 1;
|
|
1077
|
+
}
|
|
1078
|
+
if (row.length === 0) packedRows.splice(index, 1);
|
|
1005
1079
|
}
|
|
1006
1080
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1081
|
+
// Reserve marker cells on whichever row ends up last, evicting from its
|
|
1082
|
+
// tail until the cue fits. Never opens a new row: that would contradict
|
|
1083
|
+
// maxRows. Emptying that row is fine -- the marker then occupies it alone.
|
|
1084
|
+
// Popping it instead would move the marker onto a row that carries no
|
|
1085
|
+
// reservation, and the defensive truncation in `render()` would recreate
|
|
1086
|
+
// the lookalike-ellipsis failure one row further up.
|
|
1087
|
+
let marker = "";
|
|
1088
|
+
if (dropped > 0 && topFillWidth > 0) {
|
|
1089
|
+
let reserved = Math.min(topFillWidth, visibleWidth(`…+${dropped}`));
|
|
1090
|
+
while (packedRows.length > 0) {
|
|
1091
|
+
const lastRow = packedRows[packedRows.length - 1];
|
|
1092
|
+
if (this.#groupWidth(lastRow, seg.leftCapWidth, seg.leftSepWidth) + reserved <= topFillWidth) break;
|
|
1093
|
+
if (lastRow.length === 0) break;
|
|
1094
|
+
lastRow.pop();
|
|
1095
|
+
dropped += 1;
|
|
1096
|
+
reserved = Math.min(topFillWidth, visibleWidth(`…+${dropped}`));
|
|
1097
|
+
}
|
|
1098
|
+
marker = this.#renderOverflowMarker(dropped, reserved);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
rows = packedRows.map((row, index) => {
|
|
1102
|
+
const rendered = this.#renderStatusGroup(
|
|
1103
|
+
row,
|
|
1104
|
+
"left",
|
|
1105
|
+
seg.separatorDef,
|
|
1106
|
+
seg.bgAnsi,
|
|
1107
|
+
seg.fgAnsi,
|
|
1108
|
+
seg.sepAnsi,
|
|
1109
|
+
);
|
|
1110
|
+
return index === packedRows.length - 1 ? rendered + marker : rendered;
|
|
1111
|
+
});
|
|
1112
|
+
// Every content row was pruned away, so the cue is the only thing left.
|
|
1113
|
+
if (packedRows.length === 0 && marker) rows = [marker];
|
|
1010
1114
|
}
|
|
1011
1115
|
|
|
1012
1116
|
this.#renderedRowsCache = { key: cacheKey, rows };
|
|
@@ -1058,7 +1162,11 @@ export class StatusLineComponent implements Component {
|
|
|
1058
1162
|
|
|
1059
1163
|
const statusRows = this.#buildStatusRows(width, this.#resolveMaxRows());
|
|
1060
1164
|
for (const statusRow of statusRows) {
|
|
1061
|
-
if (statusRow)
|
|
1165
|
+
if (!statusRow) continue;
|
|
1166
|
+
// Guard on the truncated value: at width 0 the truncation collapses to an
|
|
1167
|
+
// empty string, and emitting that would add a blank row rather than none.
|
|
1168
|
+
const truncated = truncateToWidth(statusRow, width);
|
|
1169
|
+
if (truncated) lines.push(truncated);
|
|
1062
1170
|
}
|
|
1063
1171
|
|
|
1064
1172
|
const showHooks = this.#settings.showHookStatus ?? true;
|
|
@@ -24,6 +24,8 @@ export interface WelcomeComponentOptions {
|
|
|
24
24
|
collapseChangelog?: boolean;
|
|
25
25
|
buildLabel?: string;
|
|
26
26
|
keyDisplayContext?: KeyDisplayContext;
|
|
27
|
+
/** Render the resting logo frame instead of playing the startup gradient sweep. */
|
|
28
|
+
reducedMotion?: boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS = 9;
|
|
@@ -73,6 +75,12 @@ export class WelcomeComponent implements Component {
|
|
|
73
75
|
*/
|
|
74
76
|
playIntro(requestRender: () => void): void {
|
|
75
77
|
this.#stopAnimation();
|
|
78
|
+
if (this.options.reducedMotion) {
|
|
79
|
+
// `#animStart` stays null, which `#currentLogoFrame` already resolves to
|
|
80
|
+
// the resting frame, so one render settles the surface with no timer.
|
|
81
|
+
requestRender();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
76
84
|
this.#animStart = performance.now();
|
|
77
85
|
requestRender();
|
|
78
86
|
this.#animTimer = setInterval(() => {
|
|
@@ -1154,6 +1154,7 @@ export class ExtensionUiController {
|
|
|
1154
1154
|
customInput: dialogOptions?.customInput
|
|
1155
1155
|
? {
|
|
1156
1156
|
optionLabel: dialogOptions.customInput.optionLabel,
|
|
1157
|
+
allowEmpty: (dialogOptions.customInput as { allowEmpty?: boolean }).allowEmpty,
|
|
1157
1158
|
onSubmit: text => {
|
|
1158
1159
|
const optionLabel = dialogOptions.customInput?.optionLabel;
|
|
1159
1160
|
this.hideHookSelector();
|