@sayknow-cli/coding-agent 0.5.2 → 0.5.8
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 +66 -16
- package/dist/types/cli/update-cli.d.ts +8 -1
- 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 +73 -0
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- 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/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +8 -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/launch-worktree.d.ts +17 -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/system-prompt.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +5 -0
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +13 -7
- package/scripts/compile-args.ts +8 -4
- 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/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +84 -16
- package/src/config/model-registry.ts +60 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +79 -0
- package/src/config/settings.ts +13 -2
- package/src/dap/client.ts +78 -30
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/welcome.ts +5 -0
- 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/types.ts +5 -1
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +27 -18
- package/src/session/agent-session.ts +155 -15
- 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/launch-worktree.ts +277 -4
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +100 -1
- package/src/slash-commands/types.ts +5 -0
- package/src/system-prompt.ts +7 -0
- package/src/tools/ask.ts +5 -0
- package/src/tools/fetch.ts +7 -2
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- 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
|
/**
|
|
@@ -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"]),
|
|
@@ -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(() => {
|
|
@@ -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
|
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
theme,
|
|
41
41
|
} from "../../modes/theme/theme";
|
|
42
42
|
import type { InteractiveModeContext, OAuthSelectorOptions } from "../../modes/types";
|
|
43
|
+
import { acquireResumeProgressLease } from "../../modes/utils/ui-helpers";
|
|
43
44
|
import { getNotificationConfig, isTelegramConfigured, maskToken } from "../../sdk/bus/config";
|
|
44
45
|
import {
|
|
45
46
|
clearTelegramActivationMarker,
|
|
@@ -2052,26 +2053,40 @@ export class SelectorController {
|
|
|
2052
2053
|
return true;
|
|
2053
2054
|
}
|
|
2054
2055
|
|
|
2055
|
-
async handleResumeSession(
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
const
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
this.ctx.updateEditorBorderColor();
|
|
2056
|
+
async handleResumeSession(
|
|
2057
|
+
sessionPath: string,
|
|
2058
|
+
options?: { requireIdle?: boolean },
|
|
2059
|
+
onSwitched?: () => void,
|
|
2060
|
+
): Promise<boolean> {
|
|
2061
|
+
const progress = acquireResumeProgressLease(this.ctx);
|
|
2062
|
+
await progress.committed;
|
|
2063
|
+
try {
|
|
2064
|
+
const previousSessionId = this.ctx.sessionManager.getSessionId();
|
|
2065
|
+
const migrationPolicy =
|
|
2066
|
+
this.ctx.settings?.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
|
|
2067
|
+
const writableSessionPath = await this.ctx.sessionManager.prepareManagedCandidateForWrite(
|
|
2068
|
+
sessionPath,
|
|
2069
|
+
migrationPolicy,
|
|
2070
|
+
);
|
|
2071
2071
|
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2072
|
+
// Switch session via AgentSession (emits hook and tool session events)
|
|
2073
|
+
if (!(await this.ctx.session.switchSession(writableSessionPath, options))) return false;
|
|
2074
|
+
onSwitched?.();
|
|
2075
|
+
this.#clearTransientSessionUi();
|
|
2076
|
+
const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
|
|
2077
|
+
if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
|
|
2078
|
+
this.#refreshSessionTerminalTitle();
|
|
2079
|
+
this.ctx.updateEditorBorderColor();
|
|
2080
|
+
|
|
2081
|
+
this.ctx.rebuildInitialMessages(
|
|
2082
|
+
switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
|
|
2083
|
+
);
|
|
2084
|
+
await this.ctx.reloadTodos();
|
|
2085
|
+
this.ctx.showStatus("Resumed session");
|
|
2086
|
+
return true;
|
|
2087
|
+
} finally {
|
|
2088
|
+
progress.clear();
|
|
2089
|
+
}
|
|
2075
2090
|
}
|
|
2076
2091
|
|
|
2077
2092
|
async handleSessionDeleteCommand(): Promise<void> {
|
|
@@ -2158,6 +2173,7 @@ export class SelectorController {
|
|
|
2158
2173
|
}
|
|
2159
2174
|
|
|
2160
2175
|
this.ctx.showStatus(`Logging in to ${providerId}… (Esc to cancel)`);
|
|
2176
|
+
let authorizationUrlLease: number | undefined;
|
|
2161
2177
|
const manualInput = this.ctx.oauthManualInput;
|
|
2162
2178
|
const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
|
|
2163
2179
|
const abort = new AbortController();
|
|
@@ -2172,6 +2188,7 @@ export class SelectorController {
|
|
|
2172
2188
|
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
2173
2189
|
signal: abort.signal,
|
|
2174
2190
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
2191
|
+
authorizationUrlLease = this.ctx.setOAuthAuthorizationUrl(info.url);
|
|
2175
2192
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
2176
2193
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
2177
2194
|
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
@@ -2184,6 +2201,16 @@ export class SelectorController {
|
|
|
2184
2201
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
2185
2202
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", MANUAL_LOGIN_TIP), 1, 0));
|
|
2186
2203
|
}
|
|
2204
|
+
this.ctx.chatContainer.addChild(
|
|
2205
|
+
new Text(
|
|
2206
|
+
theme.fg(
|
|
2207
|
+
"dim",
|
|
2208
|
+
`Press ${this.ctx.keybindings.getDisplayString("app.oauth.copyUrl")} to copy the authorization URL.`,
|
|
2209
|
+
),
|
|
2210
|
+
1,
|
|
2211
|
+
0,
|
|
2212
|
+
),
|
|
2213
|
+
);
|
|
2187
2214
|
this.ctx.ui.requestRender();
|
|
2188
2215
|
this.ctx.openInBrowser(info.url);
|
|
2189
2216
|
},
|
|
@@ -2235,6 +2262,7 @@ export class SelectorController {
|
|
|
2235
2262
|
if (useManualInput) {
|
|
2236
2263
|
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
|
|
2237
2264
|
}
|
|
2265
|
+
if (authorizationUrlLease !== undefined) this.ctx.clearOAuthAuthorizationUrl(authorizationUrlLease);
|
|
2238
2266
|
}
|
|
2239
2267
|
}
|
|
2240
2268
|
|
|
@@ -312,6 +312,25 @@ export function selectShutdownDraft(editorText: string, hasActiveBtw: boolean):
|
|
|
312
312
|
return hasActiveBtw ? "" : editorText;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
export class OAuthAuthorizationUrlState {
|
|
316
|
+
#active: { url: string; lease: number } | undefined;
|
|
317
|
+
#nextLease = 0;
|
|
318
|
+
|
|
319
|
+
set(url: string): number {
|
|
320
|
+
const lease = ++this.#nextLease;
|
|
321
|
+
this.#active = { url, lease };
|
|
322
|
+
return lease;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
clear(lease: number): void {
|
|
326
|
+
if (this.#active?.lease === lease) this.#active = undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
get current(): string | undefined {
|
|
330
|
+
return this.#active?.url;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
315
334
|
export class InteractiveMode implements InteractiveModeContext {
|
|
316
335
|
session: AgentSession;
|
|
317
336
|
sessionManager: SessionManager;
|
|
@@ -384,6 +403,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
384
403
|
fileSlashCommands: Set<string> = new Set();
|
|
385
404
|
skillCommands: Map<string, Skill> = new Map();
|
|
386
405
|
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
406
|
+
#oauthAuthorizationUrl = new OAuthAuthorizationUrlState();
|
|
387
407
|
promptSuggestion: PromptSuggestionController;
|
|
388
408
|
|
|
389
409
|
#baseSlashCommands: SlashCommand[] = [];
|
|
@@ -697,6 +717,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
697
717
|
|
|
698
718
|
const startupQuiet = settings.get("startup.quiet");
|
|
699
719
|
const welcomeLogoMode = resolveWelcomeLogoMode(settings.get("startup.welcomeBannerMode"));
|
|
720
|
+
const skipLogoAnimation = settings.get("startup.skipLogoAnimation");
|
|
700
721
|
this.#welcomeComponent = undefined;
|
|
701
722
|
|
|
702
723
|
for (const warning of this.session.configWarnings) {
|
|
@@ -722,6 +743,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
722
743
|
rightGutterWidth: COMPOSER_RIGHT_GUTTER_WIDTH,
|
|
723
744
|
collapseChangelog: settings.get("collapseChangelog"),
|
|
724
745
|
keyDisplayContext: this.#keyDisplayContext,
|
|
746
|
+
skipLogoAnimation,
|
|
725
747
|
},
|
|
726
748
|
);
|
|
727
749
|
|
|
@@ -2042,6 +2064,33 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2042
2064
|
openInBrowser(urlOrPath: string): void {
|
|
2043
2065
|
this.#commandController.openInBrowser(urlOrPath);
|
|
2044
2066
|
}
|
|
2067
|
+
setOAuthAuthorizationUrl(url: string): number {
|
|
2068
|
+
return this.#oauthAuthorizationUrl.set(url);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
clearOAuthAuthorizationUrl(lease: number): void {
|
|
2072
|
+
this.#oauthAuthorizationUrl.clear(lease);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
hasOAuthAuthorizationUrl(): boolean {
|
|
2076
|
+
return this.#oauthAuthorizationUrl.current !== undefined;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
async copyOAuthAuthorizationUrl(): Promise<void> {
|
|
2080
|
+
const url = this.#oauthAuthorizationUrl.current;
|
|
2081
|
+
if (!url) {
|
|
2082
|
+
this.showWarning("No OAuth authorization URL is available to copy.");
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
try {
|
|
2086
|
+
await copyToClipboard(url);
|
|
2087
|
+
this.showStatus("OAuth authorization URL copied to clipboard");
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
this.showError(
|
|
2090
|
+
`Failed to copy OAuth authorization URL: ${error instanceof Error ? error.message : String(error)}`,
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2045
2094
|
|
|
2046
2095
|
// Selector handling
|
|
2047
2096
|
showCommandPalette(
|
|
@@ -2104,10 +2153,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2104
2153
|
this.#selectorController.showSessionSelector();
|
|
2105
2154
|
}
|
|
2106
2155
|
|
|
2107
|
-
handleResumeSession(sessionPath: string): Promise<
|
|
2108
|
-
this.#
|
|
2109
|
-
|
|
2110
|
-
|
|
2156
|
+
handleResumeSession(sessionPath: string, options?: { requireIdle?: boolean }): Promise<boolean> {
|
|
2157
|
+
return this.#selectorController.handleResumeSession(sessionPath, options, () => {
|
|
2158
|
+
this.#btwController.dispose();
|
|
2159
|
+
this.resetObserverRegistry();
|
|
2160
|
+
});
|
|
2111
2161
|
}
|
|
2112
2162
|
|
|
2113
2163
|
handleSessionDeleteCommand(): Promise<void> {
|
package/src/modes/types.ts
CHANGED
|
@@ -298,6 +298,10 @@ export interface InteractiveModeContext {
|
|
|
298
298
|
): Promise<CompactionOutcome>;
|
|
299
299
|
|
|
300
300
|
openInBrowser(urlOrPath: string): void;
|
|
301
|
+
setOAuthAuthorizationUrl(url: string): number;
|
|
302
|
+
hasOAuthAuthorizationUrl(): boolean;
|
|
303
|
+
clearOAuthAuthorizationUrl(lease: number): void;
|
|
304
|
+
copyOAuthAuthorizationUrl(): Promise<void>;
|
|
301
305
|
/** Resolved source of truth for slash autocomplete and command palette entries. */
|
|
302
306
|
getSlashCommands?(): readonly SlashCommand[];
|
|
303
307
|
refreshSlashCommandState(cwd?: string): Promise<void>;
|
|
@@ -322,7 +326,7 @@ export interface InteractiveModeContext {
|
|
|
322
326
|
showTreeSelector(): void;
|
|
323
327
|
showSessionSelector(): void;
|
|
324
328
|
showSessionsDashboard(): void;
|
|
325
|
-
handleResumeSession(sessionPath: string): Promise<
|
|
329
|
+
handleResumeSession(sessionPath: string, options?: { requireIdle?: boolean }): Promise<boolean>;
|
|
326
330
|
handleSessionDeleteCommand(): Promise<void>;
|
|
327
331
|
showOAuthSelector(mode: "login" | "logout", providerId?: string, options?: OAuthSelectorOptions): Promise<void>;
|
|
328
332
|
showHookConfirm(title: string, message: string): Promise<boolean>;
|
|
@@ -42,6 +42,14 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
|
|
|
42
42
|
- Do not defer actionable work. Underpromise and overdeliver: report only what is done or in progress, never announce remaining work instead of doing it.
|
|
43
43
|
</communication>
|
|
44
44
|
|
|
45
|
+
{{#if reasoningLanguageEnglish}}
|
|
46
|
+
<reasoning-language>
|
|
47
|
+
- Reason through development and technical problem-solving in English.
|
|
48
|
+
- Keep user-facing answers in the language the user requested or used.
|
|
49
|
+
- This changes reasoning language only; it does not relax correctness, safety, or communication requirements.
|
|
50
|
+
</reasoning-language>
|
|
51
|
+
{{/if}}
|
|
52
|
+
|
|
45
53
|
<completion-contract>
|
|
46
54
|
- Never present partial work as complete.
|
|
47
55
|
- Never suppress tests or warnings to make code pass.
|