@sayknow-cli/coding-agent 0.5.1 → 0.5.6
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 +185 -14
- package/dist/types/cli/update-cli.d.ts +8 -1
- package/dist/types/commands/session.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/telegram-autostart.d.ts +9 -1
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- package/dist/types/modes/components/pet-capability.d.ts +8 -7
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +13 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-storage.d.ts +17 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +22 -0
- package/dist/types/session-import/command.d.ts +20 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +28 -0
- package/dist/types/session-import/types.d.ts +125 -0
- package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
- package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
- package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
- package/dist/types/skc-runtime/session-restore.d.ts +99 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
- package/dist/types/slash-commands/types.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +169 -4
- package/package.json +16 -7
- package/scripts/generate-sdk-operation-inventory.ts +5 -6
- package/src/cli/config-cli.ts +23 -8
- package/src/cli/update-cli.ts +265 -21
- package/src/commands/session.ts +88 -2
- package/src/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +69 -1
- package/src/config/model-registry.ts +72 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +9 -0
- package/src/config/settings.ts +13 -2
- package/src/config/telegram-autostart.ts +11 -4
- package/src/dap/client.ts +78 -30
- package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/main.ts +1 -1
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/pet-capability.ts +22 -13
- package/src/modes/components/pet-selector.ts +1 -1
- package/src/modes/components/sayknow-pet-widget.ts +41 -7
- package/src/modes/components/welcome.ts +5 -0
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +71 -6
- package/src/modes/controllers/plan-mode-controller.ts +8 -1
- package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +47 -19
- package/src/modes/interactive-mode.ts +54 -4
- package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
- package/src/modes/types.ts +5 -1
- package/src/notifications/lifecycle-control-runtime.ts +258 -179
- package/src/prompts/system/eager-todo.md +2 -0
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/system-prompt.md +4 -2
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +25 -18
- package/src/session/agent-session.ts +120 -25
- package/src/session/internal/managed-session-scope.ts +41 -6
- package/src/session/internal/managed-session-storage.ts +14 -0
- package/src/session/session-manager.ts +113 -11
- package/src/session/session-storage.ts +70 -0
- package/src/session-import/claude.ts +382 -0
- package/src/session-import/codex.ts +458 -0
- package/src/session-import/command.ts +61 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +27 -0
- package/src/session-import/redact.ts +125 -0
- package/src/session-import/service.ts +749 -0
- package/src/session-import/types.ts +163 -0
- package/src/skc-runtime/boot-generation.ts +172 -0
- package/src/skc-runtime/launch-tmux.ts +219 -41
- package/src/skc-runtime/launch-worktree.ts +277 -4
- package/src/skc-runtime/session-restore-runtime.ts +120 -0
- package/src/skc-runtime/session-restore.ts +296 -0
- package/src/skc-runtime/session-state-sidecar.ts +41 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
- package/src/skc-runtime/tmux-sessions.ts +284 -108
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +109 -5
- package/src/slash-commands/types.ts +5 -0
- package/src/tools/ask.ts +188 -10
- package/src/tools/eval.ts +2 -2
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2348
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
package/src/lsp/client.ts
CHANGED
|
@@ -89,12 +89,10 @@ function deleteClientLock(key: string, clientPromise: Promise<LspClient>): void
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function evictDeadCachedClient(
|
|
92
|
+
function evictDeadCachedClient(client: LspClient): void {
|
|
93
93
|
if (client.proc.exitCode === null && !client.proc.killed && !client.owner?.disposed && !killedClients.has(client))
|
|
94
94
|
return;
|
|
95
|
-
|
|
96
|
-
client.resolveProjectLoaded();
|
|
97
|
-
rejectPendingRequests(client, new Error("LSP server exited"));
|
|
95
|
+
terminalizeTransport(client, new Error("LSP server exited"));
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
function terminalizeTransport(client: LspClient, cause: unknown): Error {
|
|
@@ -275,7 +273,9 @@ async function writeMessage(
|
|
|
275
273
|
if (terminalError) throw terminalError;
|
|
276
274
|
|
|
277
275
|
try {
|
|
278
|
-
|
|
276
|
+
await Promise.resolve(
|
|
277
|
+
client.proc.stdin.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n${content}`),
|
|
278
|
+
);
|
|
279
279
|
await client.proc.stdin.flush();
|
|
280
280
|
} catch (error) {
|
|
281
281
|
if (isKnownSinkPeerClosedError(error)) {
|
|
@@ -302,7 +302,7 @@ function queueWriteMessage(
|
|
|
302
302
|
* Start background message reader for a client.
|
|
303
303
|
* Routes responses to pending requests and handles notifications.
|
|
304
304
|
*/
|
|
305
|
-
async function startMessageReader(client: LspClient): Promise<void> {
|
|
305
|
+
export async function startMessageReader(client: LspClient): Promise<void> {
|
|
306
306
|
if (client.isReading) return;
|
|
307
307
|
client.isReading = true;
|
|
308
308
|
|
|
@@ -311,7 +311,10 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
311
311
|
try {
|
|
312
312
|
while (true) {
|
|
313
313
|
const { done, value } = await reader.read();
|
|
314
|
-
if (done)
|
|
314
|
+
if (done) {
|
|
315
|
+
terminalizeTransport(client, new Error("LSP stdout closed"));
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
315
318
|
|
|
316
319
|
// Atomically update buffer before processing
|
|
317
320
|
const currentBuffer: Buffer = Buffer.concat([client.messageBuffer, value]);
|
|
@@ -368,11 +371,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
368
371
|
client.messageBuffer = workingBuffer;
|
|
369
372
|
}
|
|
370
373
|
} catch (err) {
|
|
371
|
-
|
|
372
|
-
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
373
|
-
pending.reject(new Error(`LSP connection closed: ${err}`));
|
|
374
|
-
}
|
|
375
|
-
client.pendingRequests.clear();
|
|
374
|
+
terminalizeTransport(client, err);
|
|
376
375
|
} finally {
|
|
377
376
|
reader.releaseLock();
|
|
378
377
|
client.isReading = false;
|
|
@@ -488,7 +487,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
488
487
|
// Check if client already exists
|
|
489
488
|
const existingClient = clients.get(key);
|
|
490
489
|
if (existingClient) {
|
|
491
|
-
evictDeadCachedClient(
|
|
490
|
+
evictDeadCachedClient(existingClient);
|
|
492
491
|
if (clients.has(key)) {
|
|
493
492
|
existingClient.lastActivity = Date.now();
|
|
494
493
|
return existingClient;
|
|
@@ -558,13 +557,10 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
558
557
|
};
|
|
559
558
|
clients.set(key, client);
|
|
560
559
|
|
|
561
|
-
// Register crash recovery
|
|
560
|
+
// Register crash recovery and durably terminalize retained references on exit.
|
|
562
561
|
proc.exited.then(async () => {
|
|
563
|
-
deleteCachedClient(key, client);
|
|
564
562
|
deleteClientLock(key, clientPromise);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// Reject any pending requests — the server is gone, they will never complete.
|
|
563
|
+
let error = new Error(`LSP server exited unexpectedly (code ${proc.exitCode})`);
|
|
568
564
|
if (client.pendingRequests.size > 0) {
|
|
569
565
|
// Strip informational log lines (e.g. marksman's [INF]/[DBG] prefix)
|
|
570
566
|
// — they are startup noise, not actionable errors.
|
|
@@ -588,16 +584,13 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
588
584
|
);
|
|
589
585
|
const diagnosticSuffix = crashNotice ? `\n${crashNotice}` : "";
|
|
590
586
|
const code = proc.exitCode;
|
|
591
|
-
|
|
587
|
+
error = new Error(
|
|
592
588
|
stderr
|
|
593
589
|
? `LSP server exited (code ${code}): ${stderr}${diagnosticSuffix}`
|
|
594
590
|
: `LSP server exited unexpectedly (code ${code})${diagnosticSuffix}`,
|
|
595
591
|
);
|
|
596
|
-
for (const pending of client.pendingRequests.values()) {
|
|
597
|
-
pending.reject(err);
|
|
598
|
-
}
|
|
599
|
-
client.pendingRequests.clear();
|
|
600
592
|
}
|
|
593
|
+
terminalizeTransport(client, error);
|
|
601
594
|
});
|
|
602
595
|
|
|
603
596
|
// Start background message reader
|
|
@@ -980,12 +973,7 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
980
973
|
};
|
|
981
974
|
|
|
982
975
|
client.lastActivity = Date.now();
|
|
983
|
-
|
|
984
|
-
await queueWriteMessage(client, notification);
|
|
985
|
-
} catch (error) {
|
|
986
|
-
if (transportClosedErrors.get(client) === error) return;
|
|
987
|
-
throw error;
|
|
988
|
-
}
|
|
976
|
+
await queueWriteMessage(client, notification);
|
|
989
977
|
}
|
|
990
978
|
|
|
991
979
|
/**
|
package/src/main.ts
CHANGED
|
@@ -1331,7 +1331,7 @@ export async function runRootCommand(
|
|
|
1331
1331
|
// Initialize discovery system with settings for provider persistence
|
|
1332
1332
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
|
1333
1333
|
// Auto-start Telegram Remote gateway if enabled in settings
|
|
1334
|
-
if (!parsedArgs.print && !autoPrint) await maybeAutostartTelegramRemote();
|
|
1334
|
+
if (!parsedArgs.print && !autoPrint) await maybeAutostartTelegramRemote(settingsInstance);
|
|
1335
1335
|
|
|
1336
1336
|
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted).
|
|
1337
1337
|
// Precedence per role: CLI flag > documented SKC_*_MODEL > legacy PI_*_MODEL.
|
|
@@ -39,6 +39,7 @@ export const APP_ACTION_METADATA: readonly ActionMetadata[] = [
|
|
|
39
39
|
action("app.model.select", "Select model", "Model", ["composer"]),
|
|
40
40
|
action("app.model.selectTemporary", "Select temporary model", "Model", ["composer"]),
|
|
41
41
|
action("app.tools.expand", "Toggle tool expansion", "Tools", ["composer"]),
|
|
42
|
+
action("app.todo.toggle", "Toggle todo expansion", "Tasks", ["composer"]),
|
|
42
43
|
action("app.tool.backgroundFold", "Fold foreground tool", "Tools", ["composer"]),
|
|
43
44
|
action("app.editor.external", "Open external editor", "Editor", ["composer"]),
|
|
44
45
|
action("app.message.followUp", "Send follow-up", "Messages", ["composer"]),
|
|
@@ -47,6 +48,7 @@ export const APP_ACTION_METADATA: readonly ActionMetadata[] = [
|
|
|
47
48
|
action("app.clipboard.pasteImage", "Paste image", "Clipboard", ["composer"]),
|
|
48
49
|
action("app.clipboard.copyLine", "Copy line", "Clipboard", ["composer"]),
|
|
49
50
|
action("app.clipboard.copyPrompt", "Copy prompt", "Clipboard", ["composer"]),
|
|
51
|
+
action("app.oauth.copyUrl", "Copy OAuth authorization URL", "OAuth", ["composer"]),
|
|
50
52
|
action("app.session.new", "New session", "Session", ["composer"]),
|
|
51
53
|
action("app.session.tree", "Session tree", "Session", ["composer"]),
|
|
52
54
|
action("app.session.fork", "Branch from message", "Session", ["composer"]),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
getOverlayImageProtocol,
|
|
2
3
|
ImageProtocol,
|
|
3
4
|
isUnderTerminalMultiplexer,
|
|
5
|
+
isUnderTmux,
|
|
4
6
|
onImageProtocolChanged,
|
|
5
7
|
type SelectItem,
|
|
8
|
+
shouldProbeKittyPassthrough,
|
|
6
9
|
shouldProbeSixelCapability,
|
|
7
|
-
TERMINAL,
|
|
8
10
|
} from "@sayknow-cli/tui";
|
|
9
11
|
|
|
10
12
|
export type PetPixelProtocol = "sixel" | "kitty";
|
|
@@ -14,16 +16,22 @@ export const PET_SAVED_UNAVAILABLE_DESCRIPTION =
|
|
|
14
16
|
"Saved, unavailable — requires compatible Kitty or Sixel overlay rendering";
|
|
15
17
|
export const PET_UNAVAILABLE_WARNING =
|
|
16
18
|
"⚠ Pets aren’t available in this terminal. Its image support isn’t compatible with Sayknow Pet’s overlay rendering yet. Try Kitty, Ghostty, WezTerm, or a terminal with compatible Sixel support.";
|
|
19
|
+
const PET_TMUX_UNAVAILABLE_WARNING =
|
|
20
|
+
"⚠ Sayknow Pet graphics are unavailable: the terminal attached to this tmux client answered neither the Kitty nor the Sixel capability query forwarded through DCS passthrough. Attach from Ghostty, Kitty, WezTerm, or a Sixel-capable terminal, and leave tmux `allow-passthrough` enabled.";
|
|
17
21
|
const PET_MULTIPLEXER_UNAVAILABLE_WARNING =
|
|
18
|
-
"⚠ Sayknow Pet graphics are unavailable inside
|
|
22
|
+
"⚠ Sayknow Pet graphics are unavailable inside screen and zellij: neither forwards image escapes to the outer terminal. Run skc under tmux (which has DCS passthrough) or outside the multiplexer.";
|
|
19
23
|
|
|
20
24
|
export function getPetUnavailableWarning(env: NodeJS.ProcessEnv = Bun.env): string {
|
|
25
|
+
if (isUnderTmux(env)) return PET_TMUX_UNAVAILABLE_WARNING;
|
|
21
26
|
return isUnderTerminalMultiplexer(env) ? PET_MULTIPLEXER_UNAVAILABLE_WARNING : PET_UNAVAILABLE_WARNING;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export function getPetPixelProtocol(): PetPixelProtocol | null {
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
// Overlay channel: inline graphics when the terminal renders them directly,
|
|
31
|
+
// otherwise the tmux passthrough protocol proven by the startup probe.
|
|
32
|
+
const protocol = getOverlayImageProtocol();
|
|
33
|
+
if (protocol === ImageProtocol.Kitty) return "kitty";
|
|
34
|
+
if (protocol === ImageProtocol.Sixel) return "sixel";
|
|
27
35
|
return null;
|
|
28
36
|
}
|
|
29
37
|
|
|
@@ -55,24 +63,25 @@ export function createPetSelectItems(
|
|
|
55
63
|
|
|
56
64
|
/**
|
|
57
65
|
* Grace period before declaring the terminal pet-incapable at startup. The
|
|
58
|
-
* asynchronous
|
|
59
|
-
*
|
|
60
|
-
* supported terminal is never told it is
|
|
61
|
-
* still in flight.
|
|
66
|
+
* asynchronous capability probes start inside `TUI.start()` and answer within
|
|
67
|
+
* their own deadline (250 ms directly, 600 ms through tmux passthrough); this
|
|
68
|
+
* margin covers probe scheduling so a supported terminal is never told it is
|
|
69
|
+
* incompatible while a probe is still in flight.
|
|
62
70
|
*/
|
|
63
|
-
export const PET_CAPABILITY_SETTLE_MS =
|
|
71
|
+
export const PET_CAPABILITY_SETTLE_MS = 1_500;
|
|
64
72
|
|
|
65
73
|
/**
|
|
66
|
-
* Whether
|
|
67
|
-
* graphics for this session, meaning current
|
|
74
|
+
* Whether an asynchronous startup capability probe (Sixel, or Kitty through
|
|
75
|
+
* tmux passthrough) may still enable graphics for this session, meaning current
|
|
76
|
+
* unavailability is not final.
|
|
68
77
|
*/
|
|
69
78
|
export function isPetCapabilityProbePending(
|
|
70
79
|
env: NodeJS.ProcessEnv = Bun.env,
|
|
71
80
|
platform: NodeJS.Platform = process.platform,
|
|
72
81
|
): boolean {
|
|
73
|
-
if (
|
|
82
|
+
if (getPetPixelProtocol() !== null) return false;
|
|
74
83
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
75
|
-
return shouldProbeSixelCapability(env, platform);
|
|
84
|
+
return shouldProbeSixelCapability(env, platform) || shouldProbeKittyPassthrough(env);
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
/**
|
|
@@ -13,7 +13,7 @@ const PET_OPTIONS: { value: PetMode; label: string; description: string }[] = [
|
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Theme-style picker for the sayknow pet skin (Off /
|
|
16
|
+
* Theme-style picker for the sayknow pet skin (Off / RedOctopus / BlueOctopus). Preview
|
|
17
17
|
* fires as the selection moves; select commits, cancel restores.
|
|
18
18
|
*/
|
|
19
19
|
export class PetSelectorComponent extends Container {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type Component,
|
|
5
5
|
type Container,
|
|
6
6
|
getCellDimensions,
|
|
7
|
+
getTmuxOverlayImageProtocol,
|
|
7
8
|
isUnderTmux,
|
|
8
9
|
PARA_PARA_STEPS,
|
|
9
10
|
PET_SKINS,
|
|
@@ -14,8 +15,10 @@ import {
|
|
|
14
15
|
registerAnimationCallback,
|
|
15
16
|
type SayknowPixelFrameName,
|
|
16
17
|
type SayknowPixelFrames,
|
|
18
|
+
TERMINAL,
|
|
17
19
|
type TUI,
|
|
18
20
|
tmuxOwnsSixel,
|
|
21
|
+
tmuxPaneOffset,
|
|
19
22
|
wrapTmuxPassthrough,
|
|
20
23
|
} from "@sayknow-cli/tui";
|
|
21
24
|
import type { CustomEditor } from "./custom-editor";
|
|
@@ -216,7 +219,7 @@ export class SayknowPetWidget {
|
|
|
216
219
|
|
|
217
220
|
/** Live preview during a selector: change the sprite without re-mounting the
|
|
218
221
|
* composer editor (that would tear down the open overlay). After a short idle
|
|
219
|
-
* eye-roll it fires the signature burst once (
|
|
222
|
+
* eye-roll it fires the signature burst once (RedOctopus flex, BlueOctopus para-para
|
|
220
223
|
* then sob) so the selector demos the animation instead of waiting the random gap. */
|
|
221
224
|
previewMode(mode: PetMode): void {
|
|
222
225
|
this.#applyMode(mode, false);
|
|
@@ -341,7 +344,7 @@ export class SayknowPetWidget {
|
|
|
341
344
|
const mode = this.#mode;
|
|
342
345
|
if (mode === "off") return "base";
|
|
343
346
|
// Random idle burst → the skin's own animation, driven by its burst descriptor
|
|
344
|
-
// (
|
|
347
|
+
// (RedOctopus holds a flex; BlueOctopus dances the para-para then sobs).
|
|
345
348
|
if (now < this.#flexUntil) {
|
|
346
349
|
const burst = PET_SKINS[mode].burst;
|
|
347
350
|
const elapsed = now - (this.#flexUntil - petBurstDurationMs(burst));
|
|
@@ -384,7 +387,7 @@ export class SayknowPetWidget {
|
|
|
384
387
|
}
|
|
385
388
|
}
|
|
386
389
|
// Random show-off, both while idle and while working. Each skin's burst runs for
|
|
387
|
-
// its own length (
|
|
390
|
+
// its own length (RedOctopus a brief flex; BlueOctopus a para-para cycle plus sob).
|
|
388
391
|
if (this.#autoFlexGapMs && now >= this.#flexUntil) {
|
|
389
392
|
if (this.#nextAutoFlexAt === 0) {
|
|
390
393
|
this.#scheduleAutoFlex(now);
|
|
@@ -429,11 +432,35 @@ export class SayknowPetWidget {
|
|
|
429
432
|
return out;
|
|
430
433
|
}
|
|
431
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Whether pet art must be smuggled to the outer terminal through tmux's DCS
|
|
437
|
+
* passthrough envelope. True exactly when the multiplexer suppressed inline
|
|
438
|
+
* graphics and the startup probe proved the outer terminal can draw them.
|
|
439
|
+
*/
|
|
440
|
+
#usesTmuxPassthrough(): boolean {
|
|
441
|
+
return TERMINAL.imageProtocol === null && getTmuxOverlayImageProtocol() !== null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Cursor-position escape for the pet's top-left cell.
|
|
446
|
+
*
|
|
447
|
+
* Passthrough payloads address the OUTER terminal's screen, so they carry the
|
|
448
|
+
* pane origin; tmux-level payloads stay pane-relative.
|
|
449
|
+
*/
|
|
450
|
+
#cursorTo(x: number, y: number, outer: boolean): string {
|
|
451
|
+
const offset = outer ? tmuxPaneOffset() : { top: 0, left: 0 };
|
|
452
|
+
return `\x1b[${y + offset.top + 1};${x + offset.left + 1}H`;
|
|
453
|
+
}
|
|
454
|
+
|
|
432
455
|
/** Pending on-screen image cleanup. Pure: authority is consumed separately, on delivery. */
|
|
433
456
|
#imageCleanupPayload(): string {
|
|
434
457
|
let out = "";
|
|
435
458
|
if (this.#kittyCleanupPending && this.#kittyImageId !== undefined) {
|
|
436
|
-
|
|
459
|
+
// Deleting by image id needs no cursor addressing, but it still has to
|
|
460
|
+
// reach the terminal that holds the placement — the outer one when the
|
|
461
|
+
// art was delivered through passthrough.
|
|
462
|
+
const del = `\x1b_Ga=d,d=I,i=${this.#kittyImageId},q=2\x1b\\`;
|
|
463
|
+
out += this.#usesTmuxPassthrough() ? wrapTmuxPassthrough(del) : del;
|
|
437
464
|
}
|
|
438
465
|
if (this.#lastSixelFootprint) {
|
|
439
466
|
out += this.#clearSixelFootprint(this.#lastSixelFootprint);
|
|
@@ -491,8 +518,10 @@ export class SayknowPetWidget {
|
|
|
491
518
|
// A full frame supersedes any cleanup-only frame still awaiting ack.
|
|
492
519
|
this.#frameCleanupAwaitingAck = false;
|
|
493
520
|
const { x, y } = pos;
|
|
521
|
+
const frame = pixel.frames[this.#frame];
|
|
494
522
|
let out = "";
|
|
495
|
-
const positioned =
|
|
523
|
+
const positioned = `${this.#cursorTo(x, y, false)}${frame}`;
|
|
524
|
+
const positionedOuter = `${this.#cursorTo(x, y, true)}${frame}`;
|
|
496
525
|
|
|
497
526
|
if (pixel.protocol === "sixel") {
|
|
498
527
|
const footprint = { x, y, columns: pixel.columns, rows: pixel.rasterRows };
|
|
@@ -518,7 +547,7 @@ export class SayknowPetWidget {
|
|
|
518
547
|
out += tmuxOwnsSixel()
|
|
519
548
|
? positioned
|
|
520
549
|
: isUnderTmux()
|
|
521
|
-
? wrapTmuxPassthrough(`\x1b7${
|
|
550
|
+
? wrapTmuxPassthrough(`\x1b7${positionedOuter}\x1b8`)
|
|
522
551
|
: positioned;
|
|
523
552
|
return out;
|
|
524
553
|
}
|
|
@@ -526,7 +555,12 @@ export class SayknowPetWidget {
|
|
|
526
555
|
// A kitty frame (re)places the image cursor-neutrally, so cleanup is pending
|
|
527
556
|
// again even if a narrow-terminal pass consumed it earlier.
|
|
528
557
|
this.#kittyCleanupPending = true;
|
|
529
|
-
|
|
558
|
+
// Under tmux the placement is cursor-neutral for the OUTER terminal but its
|
|
559
|
+
// addressing is not: tmux forwards passthrough bytes without first syncing
|
|
560
|
+
// the physical cursor, so the frame carries its own outer-screen position,
|
|
561
|
+
// and the save/restore pair sits inside the envelope so the outer cursor is
|
|
562
|
+
// exactly where tmux believes it left it.
|
|
563
|
+
out += this.#usesTmuxPassthrough() ? wrapTmuxPassthrough(`\x1b7${positionedOuter}\x1b8`) : positioned;
|
|
530
564
|
return out;
|
|
531
565
|
}
|
|
532
566
|
}
|
|
@@ -25,6 +25,7 @@ export interface WelcomeComponentOptions {
|
|
|
25
25
|
collapseChangelog?: boolean;
|
|
26
26
|
buildLabel?: string;
|
|
27
27
|
keyDisplayContext?: KeyDisplayContext;
|
|
28
|
+
skipLogoAnimation?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS = 15;
|
|
@@ -75,6 +76,10 @@ export class WelcomeComponent implements Component {
|
|
|
75
76
|
*/
|
|
76
77
|
playIntro(requestRender: () => void): void {
|
|
77
78
|
this.#stopAnimation();
|
|
79
|
+
if (this.options.skipLogoAnimation) {
|
|
80
|
+
requestRender();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
78
83
|
this.#animStart = performance.now();
|
|
79
84
|
requestRender();
|
|
80
85
|
this.#animTimer = setInterval(() => {
|
|
@@ -869,7 +869,7 @@ export class EventController {
|
|
|
869
869
|
(content: { type: string; text?: string }) => content.type === "text",
|
|
870
870
|
)?.text;
|
|
871
871
|
this.ctx.showWarning(
|
|
872
|
-
`Todo update failed${textContent ? `: ${textContent}` : ".
|
|
872
|
+
`Todo update failed${textContent ? `: ${textContent}` : ""}. Work continues without visible todo progress for this turn.`,
|
|
873
873
|
);
|
|
874
874
|
}
|
|
875
875
|
if (event.toolName === "resolve" && !event.isError) {
|
|
@@ -264,12 +264,15 @@ export class GoalModeController {
|
|
|
264
264
|
if (this.#enabled || this.#paused) this.ctx.modeGate.enter("goal");
|
|
265
265
|
if (restored?.goal) {
|
|
266
266
|
this.#previousTools = this.ctx.session.getActiveToolNames();
|
|
267
|
-
|
|
267
|
+
if (!this.ctx.session.hasExplicitlyDisabledTools()) {
|
|
268
|
+
await this.ctx.session.setActiveToolsByName([...new Set([...this.#previousTools, "goal"])]);
|
|
269
|
+
}
|
|
268
270
|
}
|
|
269
271
|
this.#updateStatus();
|
|
270
272
|
return true;
|
|
271
273
|
}
|
|
272
274
|
if (recoveryHydration) return false;
|
|
275
|
+
if (this.ctx.session.hasExplicitlyDisabledTools()) return false;
|
|
273
276
|
const pendingGoal = goalEnabled
|
|
274
277
|
? await consumePendingGoalModeRequest(this.ctx.sessionManager.getCwd(), this.ctx.sessionManager.getSessionId())
|
|
275
278
|
: null;
|
|
@@ -321,7 +324,9 @@ export class GoalModeController {
|
|
|
321
324
|
objective: options.objective ?? "",
|
|
322
325
|
provenance: options.provenance,
|
|
323
326
|
});
|
|
324
|
-
|
|
327
|
+
if (!this.ctx.session.hasExplicitlyDisabledTools()) {
|
|
328
|
+
await this.ctx.session.setActiveToolsByName([...new Set([...this.#previousTools, "goal"])]);
|
|
329
|
+
}
|
|
325
330
|
this.ctx.session.setGoalModeState(state);
|
|
326
331
|
this.#enabled = true;
|
|
327
332
|
this.#resetContinuationSuppression();
|
|
@@ -91,6 +91,7 @@ export class InputController {
|
|
|
91
91
|
"app.model.select": () => this.ctx.showModelSelector(),
|
|
92
92
|
"app.model.selectTemporary": () => this.ctx.showModelSelector({ temporaryOnly: true }),
|
|
93
93
|
"app.tools.expand": () => this.toggleToolOutputExpansion(),
|
|
94
|
+
"app.todo.toggle": () => this.ctx.toggleTodoExpansion(),
|
|
94
95
|
"app.tool.backgroundFold": () => {
|
|
95
96
|
this.handleForegroundToolBackgroundFold();
|
|
96
97
|
},
|
|
@@ -103,6 +104,7 @@ export class InputController {
|
|
|
103
104
|
},
|
|
104
105
|
"app.clipboard.copyLine": () => this.handleCopyCurrentLine(),
|
|
105
106
|
"app.clipboard.copyPrompt": () => this.handleCopyPrompt(),
|
|
107
|
+
"app.oauth.copyUrl": () => this.ctx.copyOAuthAuthorizationUrl(),
|
|
106
108
|
"app.session.new": async () => {
|
|
107
109
|
await this.ctx.handleClearCommand();
|
|
108
110
|
},
|
|
@@ -153,6 +155,8 @@ export class InputController {
|
|
|
153
155
|
return this.ctx.session.getRoleModelCycleCandidateCount() > 1;
|
|
154
156
|
case "app.tools.expand":
|
|
155
157
|
return this.ctx.chatContainer.children.some(isExpandable);
|
|
158
|
+
case "app.todo.toggle":
|
|
159
|
+
return this.ctx.todoPhases.some(phase => phase.tasks.length > 0);
|
|
156
160
|
case "app.tool.backgroundFold":
|
|
157
161
|
return Boolean(this.ctx.session.hasForegroundBashBackgroundRequestHandler?.());
|
|
158
162
|
case "app.editor.external":
|
|
@@ -178,6 +182,8 @@ export class InputController {
|
|
|
178
182
|
);
|
|
179
183
|
case "app.clipboard.copyPrompt":
|
|
180
184
|
return this.ctx.editor.getText().length > 0;
|
|
185
|
+
case "app.oauth.copyUrl":
|
|
186
|
+
return this.ctx.hasOAuthAuthorizationUrl();
|
|
181
187
|
case "app.session.tree":
|
|
182
188
|
case "app.session.fork":
|
|
183
189
|
return this.ctx.session.messages.length > 0;
|
|
@@ -258,14 +264,20 @@ export class InputController {
|
|
|
258
264
|
* so abort cleanup going idle cannot turn the second Esc into an idle action. */
|
|
259
265
|
#steerConsumePending = false;
|
|
260
266
|
#commandPaletteActions = new Map<AppKeybinding, CommandPaletteAction>();
|
|
267
|
+
readonly #availabilityGatedPaletteActions = new Set<AppKeybinding>();
|
|
261
268
|
#paletteCommandInFlight = false;
|
|
262
269
|
|
|
263
|
-
#registerCommandPaletteAction(
|
|
270
|
+
#registerCommandPaletteAction(
|
|
271
|
+
action: AppKeybinding,
|
|
272
|
+
handler: () => void | Promise<void>,
|
|
273
|
+
availabilityGated = false,
|
|
274
|
+
): void {
|
|
264
275
|
this.#commandPaletteActions.set(action, {
|
|
265
276
|
id: action,
|
|
266
277
|
label: KEYBINDINGS[action].description,
|
|
267
278
|
handler,
|
|
268
279
|
});
|
|
280
|
+
if (availabilityGated) this.#availabilityGatedPaletteActions.add(action);
|
|
269
281
|
}
|
|
270
282
|
|
|
271
283
|
#globalInterruptUnsubscribe: (() => void) | undefined;
|
|
@@ -623,14 +635,12 @@ export class InputController {
|
|
|
623
635
|
this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
|
|
624
636
|
this.ctx.editor.onExpandTools = () => this.#executeAction("app.tools.expand");
|
|
625
637
|
this.#registerCommandPaletteAction("app.tools.expand", expandTools);
|
|
626
|
-
const dequeue = () => this.handleDequeue();
|
|
627
638
|
this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
|
|
628
639
|
this.ctx.editor.onDequeue = () => this.#executeAction("app.message.dequeue");
|
|
629
|
-
this.#registerCommandPaletteAction("app.message.dequeue", dequeue);
|
|
630
|
-
const queue = () => this.handleQueueSubmit();
|
|
640
|
+
this.#registerCommandPaletteAction("app.message.dequeue", () => this.#executeAction("app.message.dequeue"), true);
|
|
631
641
|
this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
|
|
632
642
|
this.ctx.editor.onQueue = () => this.#executeAction("app.message.queue");
|
|
633
|
-
this.#registerCommandPaletteAction("app.message.queue", queue);
|
|
643
|
+
this.#registerCommandPaletteAction("app.message.queue", () => this.#executeAction("app.message.queue"), true);
|
|
634
644
|
|
|
635
645
|
this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
|
|
636
646
|
this.ctx.editor.onViewportFollowLive = () => {
|
|
@@ -699,6 +709,11 @@ export class InputController {
|
|
|
699
709
|
return true;
|
|
700
710
|
});
|
|
701
711
|
}
|
|
712
|
+
this.#registerCommandPaletteAction(
|
|
713
|
+
"app.message.followUp",
|
|
714
|
+
() => this.#executeAction("app.message.followUp"),
|
|
715
|
+
true,
|
|
716
|
+
);
|
|
702
717
|
for (const key of this.ctx.keybindings.getKeys("app.message.followUp")) {
|
|
703
718
|
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
704
719
|
if (!this.actionRegistry.isAvailable("app.message.followUp")) return false;
|
|
@@ -706,6 +721,46 @@ export class InputController {
|
|
|
706
721
|
return true;
|
|
707
722
|
});
|
|
708
723
|
}
|
|
724
|
+
this.#registerCommandPaletteAction(
|
|
725
|
+
"app.transcript.prevTurn",
|
|
726
|
+
() => this.#executeAction("app.transcript.prevTurn"),
|
|
727
|
+
true,
|
|
728
|
+
);
|
|
729
|
+
for (const key of this.ctx.keybindings.getKeys("app.transcript.prevTurn")) {
|
|
730
|
+
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
731
|
+
if (!this.actionRegistry.isAvailable("app.transcript.prevTurn")) return false;
|
|
732
|
+
this.#executeAction("app.transcript.prevTurn");
|
|
733
|
+
return true;
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
this.#registerCommandPaletteAction(
|
|
737
|
+
"app.transcript.nextTurn",
|
|
738
|
+
() => this.#executeAction("app.transcript.nextTurn"),
|
|
739
|
+
true,
|
|
740
|
+
);
|
|
741
|
+
for (const key of this.ctx.keybindings.getKeys("app.transcript.nextTurn")) {
|
|
742
|
+
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
743
|
+
if (!this.actionRegistry.isAvailable("app.transcript.nextTurn")) return false;
|
|
744
|
+
this.#executeAction("app.transcript.nextTurn");
|
|
745
|
+
return true;
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
this.#registerCommandPaletteAction("app.message.sendNow", () => this.#executeAction("app.message.sendNow"), true);
|
|
749
|
+
for (const key of this.ctx.keybindings.getKeys("app.message.sendNow")) {
|
|
750
|
+
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
751
|
+
if (!this.actionRegistry.isAvailable("app.message.sendNow")) return false;
|
|
752
|
+
this.#executeAction("app.message.sendNow");
|
|
753
|
+
return true;
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
this.#registerCommandPaletteAction("app.todo.toggle", () => this.#executeAction("app.todo.toggle"), true);
|
|
757
|
+
for (const key of this.ctx.keybindings.getKeys("app.todo.toggle")) {
|
|
758
|
+
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
759
|
+
if (!this.actionRegistry.isAvailable("app.todo.toggle")) return false;
|
|
760
|
+
this.#executeAction("app.todo.toggle");
|
|
761
|
+
return true;
|
|
762
|
+
});
|
|
763
|
+
}
|
|
709
764
|
for (const key of this.ctx.keybindings.getKeys("app.stt.toggle")) {
|
|
710
765
|
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
711
766
|
this.#executeAction("app.stt.toggle");
|
|
@@ -717,6 +772,14 @@ export class InputController {
|
|
|
717
772
|
this.#executeAction("app.clipboard.copyLine");
|
|
718
773
|
});
|
|
719
774
|
}
|
|
775
|
+
this.#registerCommandPaletteAction("app.oauth.copyUrl", () => this.#executeAction("app.oauth.copyUrl"), true);
|
|
776
|
+
for (const key of this.ctx.keybindings.getKeys("app.oauth.copyUrl")) {
|
|
777
|
+
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
778
|
+
if (!this.actionRegistry.isAvailable("app.oauth.copyUrl")) return false;
|
|
779
|
+
this.#executeAction("app.oauth.copyUrl");
|
|
780
|
+
return true;
|
|
781
|
+
});
|
|
782
|
+
}
|
|
720
783
|
for (const key of this.ctx.keybindings.getKeys("app.session.observe")) {
|
|
721
784
|
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
722
785
|
this.#executeAction("app.session.observe");
|
|
@@ -1900,7 +1963,9 @@ export class InputController {
|
|
|
1900
1963
|
return;
|
|
1901
1964
|
}
|
|
1902
1965
|
|
|
1903
|
-
const actions = [...this.#commandPaletteActions.
|
|
1966
|
+
const actions = [...this.#commandPaletteActions.entries()]
|
|
1967
|
+
.filter(([id]) => !this.#availabilityGatedPaletteActions.has(id) || this.actionRegistry.isAvailable(id))
|
|
1968
|
+
.map(([, action]) => action);
|
|
1904
1969
|
const slashCommands = [...(this.ctx.getSlashCommands?.() ?? this.#slashCommands)];
|
|
1905
1970
|
if (!this.ctx.showCommandPalette) {
|
|
1906
1971
|
let overlayHandle: ReturnType<typeof this.ctx.ui.showOverlay> | undefined;
|
|
@@ -103,6 +103,11 @@ export class PlanModeController {
|
|
|
103
103
|
this.ctx.sessionManager.appendModeChange("none");
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
|
+
if (this.ctx.session.hasExplicitlyDisabledTools()) {
|
|
107
|
+
if (sessionContext.mode === "plan" || sessionContext.mode === "plan_paused")
|
|
108
|
+
this.ctx.sessionManager.appendModeChange("none");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
106
111
|
if (sessionContext.mode === "plan") {
|
|
107
112
|
await this.enter({ planFilePath: sessionContext.modeData?.planFilePath as string | undefined });
|
|
108
113
|
} else if (sessionContext.mode === "plan_paused") {
|
|
@@ -115,6 +120,8 @@ export class PlanModeController {
|
|
|
115
120
|
|
|
116
121
|
async enter(options?: { planFilePath?: string; workflow?: "parallel" | "iterative" }): Promise<void> {
|
|
117
122
|
if (this.#enabled) return;
|
|
123
|
+
if (this.ctx.session.hasExplicitlyDisabledTools())
|
|
124
|
+
return this.ctx.showWarning("Plan mode requires the resolve tool.");
|
|
118
125
|
if (!this.ctx.modeGate.enter("plan")) return this.ctx.showWarning("Exit goal mode first.");
|
|
119
126
|
this.#paused = false;
|
|
120
127
|
const planFilePath = options?.planFilePath ?? "local://PLAN.md";
|
|
@@ -143,7 +150,7 @@ export class PlanModeController {
|
|
|
143
150
|
async exit(options?: { silent?: boolean; paused?: boolean }): Promise<void> {
|
|
144
151
|
if (!this.#enabled) return;
|
|
145
152
|
await this.ctx.session.abort({ timeoutMs: ABORT_TIMEOUT_MS });
|
|
146
|
-
if (this.#previousTools
|
|
153
|
+
if (this.#previousTools !== undefined) await this.ctx.session.setActiveToolsByName(this.#previousTools);
|
|
147
154
|
if (this.#providerSessionScope && !this.ctx.session.isStreaming) {
|
|
148
155
|
if (this.ctx.session.restoreTemporaryProviderSessionScope(this.#providerSessionScope))
|
|
149
156
|
this.#providerSessionScope = undefined;
|
|
@@ -540,6 +540,7 @@ export class MCPCommandController {
|
|
|
540
540
|
|
|
541
541
|
const resolvedClientId = clientId.trim() || parsedAuthUrl.searchParams.get("client_id") || undefined;
|
|
542
542
|
const resolvedClientSecret = clientSecret.trim() || undefined;
|
|
543
|
+
let authorizationUrlLease: number | undefined;
|
|
543
544
|
|
|
544
545
|
try {
|
|
545
546
|
// Create OAuth flow
|
|
@@ -556,6 +557,7 @@ export class MCPCommandController {
|
|
|
556
557
|
},
|
|
557
558
|
{
|
|
558
559
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
560
|
+
authorizationUrlLease = this.ctx.setOAuthAuthorizationUrl(info.url);
|
|
559
561
|
// Show auth URL prominently in chat
|
|
560
562
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
561
563
|
this.ctx.chatContainer.addChild(
|
|
@@ -565,6 +567,16 @@ export class MCPCommandController {
|
|
|
565
567
|
this.ctx.chatContainer.addChild(
|
|
566
568
|
new Text(theme.fg("muted", "Preparing browser authorization..."), 1, 0),
|
|
567
569
|
);
|
|
570
|
+
this.ctx.chatContainer.addChild(
|
|
571
|
+
new Text(
|
|
572
|
+
theme.fg(
|
|
573
|
+
"muted",
|
|
574
|
+
`Press ${this.ctx.keybindings.getDisplayString("app.oauth.copyUrl")} to copy the authorization URL.`,
|
|
575
|
+
),
|
|
576
|
+
1,
|
|
577
|
+
0,
|
|
578
|
+
),
|
|
579
|
+
);
|
|
568
580
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
569
581
|
this.ctx.chatContainer.addChild(
|
|
570
582
|
new Text(
|
|
@@ -661,6 +673,8 @@ export class MCPCommandController {
|
|
|
661
673
|
} else {
|
|
662
674
|
throw new Error(`OAuth authentication failed: ${errorMsg}`);
|
|
663
675
|
}
|
|
676
|
+
} finally {
|
|
677
|
+
if (authorizationUrlLease !== undefined) this.ctx.clearOAuthAuthorizationUrl(authorizationUrlLease);
|
|
664
678
|
}
|
|
665
679
|
}
|
|
666
680
|
|