@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10
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 +94 -1
- package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
- package/dist/cli.js +13216 -15614
- package/dist/types/async/job-manager.d.ts +12 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +13 -0
- package/dist/types/discovery/agents.d.ts +11 -0
- package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -8
- package/dist/types/extensibility/hooks/types.d.ts +5 -5
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
- package/dist/types/hindsight/state.d.ts +0 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/lsp/utils.d.ts +8 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
- package/dist/types/modes/acp/acp-mode.d.ts +1 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
- package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
- package/dist/types/modes/components/agent-hub.d.ts +12 -5
- package/dist/types/modes/components/model-browser.d.ts +9 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/registry/agent-lifecycle.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +47 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +8 -4
- package/dist/types/session/session-entries.d.ts +10 -0
- package/dist/types/session/session-loader.d.ts +11 -1
- package/dist/types/session/session-manager.d.ts +9 -1
- package/dist/types/session/session-tools.d.ts +10 -1
- package/dist/types/session/turn-recovery.d.ts +6 -2
- package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
- package/dist/types/slash-commands/available-commands.d.ts +1 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +2 -3
- package/dist/types/task/read-only-policy.d.ts +3 -0
- package/dist/types/task/structured-subagent.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +3 -3
- package/dist/types/tools/browser/launch.d.ts +3 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -2
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/read.d.ts +3 -5
- package/dist/types/tools/run-scope.d.ts +27 -1
- package/dist/types/tools/shell-tokenize.d.ts +21 -3
- package/dist/types/tools/terminal-output.d.ts +1 -1
- package/dist/types/utils/changelog.d.ts +3 -2
- package/dist/types/utils/late-cleanup.d.ts +2 -0
- package/dist/types/utils/turndown.d.ts +1 -1
- package/dist/types/vibe/runtime.d.ts +4 -3
- package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
- package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
- package/examples/custom-tools/README.md +1 -1
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/api-demo.ts +5 -6
- package/examples/extensions/chalk-logger.ts +1 -1
- package/examples/extensions/hello.ts +2 -2
- package/examples/extensions/reload-runtime.ts +1 -1
- package/examples/extensions/tools.ts +2 -2
- package/examples/extensions/with-deps/index.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/package.json +15 -31
- package/scripts/bundle-dist.ts +1 -11
- package/scripts/omp +11 -2
- package/src/advisor/runtime.ts +54 -0
- package/src/async/job-manager.ts +26 -0
- package/src/cli/agents-cli.ts +1 -1
- package/src/cli/args.ts +24 -2
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/claude-trace-cli.ts +1 -1
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/flag-tables.ts +4 -0
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/grievances-cli.ts +1 -1
- package/src/cli/help-extra.ts +1 -1
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/read-cli.ts +1 -1
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/shell-cli.ts +1 -1
- package/src/cli/ssh-cli.ts +1 -1
- package/src/cli/stats-cli.ts +4 -4
- package/src/cli/tiny-models-cli.ts +1 -1
- package/src/cli/ttsr-cli.ts +1 -1
- package/src/cli/update-cli.ts +3 -57
- package/src/cli/usage-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli/worktree-cli.ts +1 -1
- package/src/commands/say.ts +1 -1
- package/src/commands/token.ts +1 -1
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/cli.ts +1 -1
- package/src/config/config-file.ts +2 -2
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-discovery.ts +36 -2
- package/src/config/model-registry.ts +4 -1
- package/src/config/model-resolver.ts +54 -7
- package/src/discovery/agents.ts +73 -3
- package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
- package/src/discovery/claude.ts +8 -5
- package/src/discovery/cursor.ts +8 -5
- package/src/discovery/gemini.ts +11 -8
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +4 -2
- package/src/edit/hashline/filesystem.ts +7 -7
- package/src/eval/agent-bridge.ts +1 -3
- package/src/eval/jl/prelude.jl +4 -4
- package/src/eval/js/shared/prelude.txt +2 -2
- package/src/eval/py/prelude.py +0 -3
- package/src/eval/py/runner.py +38 -1
- package/src/eval/rb/prelude.rb +1 -2
- package/src/extensibility/custom-commands/loader.ts +4 -4
- package/src/extensibility/custom-commands/types.ts +5 -5
- package/src/extensibility/custom-tools/loader.ts +4 -4
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -6
- package/src/extensibility/extensions/types.ts +39 -8
- package/src/extensibility/hooks/loader.ts +4 -5
- package/src/extensibility/hooks/types.ts +5 -5
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
- package/src/hindsight/state.ts +2 -22
- package/src/index.ts +2 -2
- package/src/internal-urls/memory-protocol.ts +33 -0
- package/src/launch/terminal-output.ts +1 -1
- package/src/lsp/client.ts +2 -2
- package/src/lsp/utils.ts +29 -0
- package/src/main.ts +85 -21
- package/src/markit/converters/docx.ts +1 -1
- package/src/markit/converters/epub.ts +1 -1
- package/src/markit/converters/pptx.ts +1 -1
- package/src/markit/converters/xlsx.ts +1 -1
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/acp/acp-mode.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +248 -0
- package/src/modes/components/agent-hub-renderer.ts +194 -0
- package/src/modes/components/agent-hub.ts +670 -192
- package/src/modes/components/agent-transcript-viewer.ts +1 -3
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/custom-editor.ts +2 -2
- package/src/modes/components/extensions/inspector-panel.ts +11 -6
- package/src/modes/components/footer.ts +1 -3
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-browser.ts +11 -3
- package/src/modes/components/model-hub.ts +4 -1
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/components/status-line/segments.ts +4 -3
- package/src/modes/controllers/event-controller.ts +188 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/mcp-command-controller.ts +81 -20
- package/src/modes/controllers/selector-controller.ts +40 -49
- package/src/modes/controllers/streaming-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/print-mode.ts +29 -12
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/session-observer-registry.ts +5 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/tools/computer.md +1 -1
- package/src/registry/agent-lifecycle.ts +35 -10
- package/src/registry/agent-registry.ts +65 -2
- package/src/registry/persisted-agents.ts +341 -16
- package/src/sdk.ts +11 -5
- package/src/session/agent-session-types.ts +8 -0
- package/src/session/agent-session.ts +177 -188
- package/src/session/session-entries.ts +10 -0
- package/src/session/session-listing.ts +1 -1
- package/src/session/session-loader.ts +74 -8
- package/src/session/session-manager.ts +22 -2
- package/src/session/session-paths.ts +105 -110
- package/src/session/session-tools.ts +28 -1
- package/src/session/settings-stream-fn.ts +7 -3
- package/src/session/turn-recovery.ts +236 -18
- package/src/session/unexpected-stop-classifier.ts +12 -3
- package/src/slash-commands/acp-builtins.ts +1 -1
- package/src/slash-commands/available-commands.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +173 -47
- package/src/task/index.ts +9 -28
- package/src/task/isolation-runner.ts +43 -12
- package/src/task/persisted-revive.ts +12 -5
- package/src/task/read-only-policy.ts +27 -0
- package/src/task/structured-subagent.ts +31 -6
- package/src/task/types.ts +4 -0
- package/src/tiny/worker.ts +1 -1
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/bash-interactive.ts +4 -4
- package/src/tools/bash-interceptor.ts +8 -4
- package/src/tools/bash.ts +6 -6
- package/src/tools/browser/cmux/cmux-tab.ts +66 -18
- package/src/tools/browser/launch.ts +76 -19
- package/src/tools/browser/readable.ts +9 -9
- package/src/tools/browser/tab-supervisor.ts +12 -3
- package/src/tools/browser/tab-worker-entry.ts +1 -1
- package/src/tools/browser/tab-worker.ts +101 -12
- package/src/tools/fetch.ts +1 -1
- package/src/tools/glob.ts +4 -2
- package/src/tools/path-utils.ts +22 -5
- package/src/tools/read.ts +22 -5
- package/src/tools/run-scope.ts +290 -4
- package/src/tools/shell-tokenize.ts +38 -9
- package/src/tools/terminal-output.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/utils/changelog.ts +5 -4
- package/src/utils/clipboard.ts +10 -2
- package/src/utils/external-editor.ts +4 -2
- package/src/utils/jj.ts +1 -1
- package/src/utils/late-cleanup.ts +17 -0
- package/src/utils/turndown.ts +1 -2
- package/src/vibe/runtime.ts +202 -58
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/hackage.ts +1 -12
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +2 -1
- package/src/web/scrapers/twitter.ts +2 -2
- package/src/web/scrapers/types.ts +1 -1
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +12 -39
- package/src/web/search/providers/codex.ts +3 -26
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/ecosia.ts +2 -2
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/firecrawl.ts +1 -1
- package/src/web/search/providers/google.ts +1 -1
- package/src/web/search/providers/mojeek.ts +2 -2
- package/src/web/search/providers/perplexity.ts +1 -1
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/startpage.ts +2 -2
- package/src/markit/converters/mammoth.d.ts +0 -24
|
@@ -613,9 +613,7 @@ export class AgentTranscriptViewer implements Component {
|
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
#statsLine(): string {
|
|
616
|
-
const observed: ObservableSession | undefined = this.deps.observers
|
|
617
|
-
?.getSessions()
|
|
618
|
-
.find(s => s.id === this.deps.agentId);
|
|
616
|
+
const observed: ObservableSession | undefined = this.deps.observers?.getSession(this.deps.agentId);
|
|
619
617
|
const progress = observed?.progress;
|
|
620
618
|
if (!progress) return "";
|
|
621
619
|
const stats: string[] = [];
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
Text,
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
14
|
-
import chalk from "chalk";
|
|
14
|
+
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
15
15
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
16
16
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
17
17
|
import { expandKeyHint, getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
@@ -466,7 +466,7 @@ export class CustomEditor extends Editor {
|
|
|
466
466
|
/** Decorate magic keywords, attachments, and the queue-composer header/list markers.
|
|
467
467
|
* Queue shorthand reserves its first logical line as a dim `Queueing` label; sequential
|
|
468
468
|
* item markers use the accent color so separate follow-ups remain visible while composing. */
|
|
469
|
-
decorateText = (text: string): string => {
|
|
469
|
+
override decorateText = (text: string): string => {
|
|
470
470
|
const editorText = this.getText();
|
|
471
471
|
const animated = this.focused && this.#shimmerEnabled() && hasMagicKeyword(editorText);
|
|
472
472
|
const phase = animated ? (Date.now() % CustomEditor.SHIMMER_PERIOD_MS) / CustomEditor.SHIMMER_PERIOD_MS : 0;
|
|
@@ -775,7 +775,7 @@ export class CustomEditor extends Editor {
|
|
|
775
775
|
void promise.then(this.#onPasteSettled, this.#onPasteSettled);
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
-
handleInput(data: string): void {
|
|
778
|
+
override handleInput(data: string): void {
|
|
779
779
|
// Serialize behind any in-flight async paste so a trailing Enter / follow-up key can't
|
|
780
780
|
// submit before the clipboard image reaches `pendingImages` (Codex PR #3602 review).
|
|
781
781
|
if (this.#pasteInFlight > 0) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Shows name, description, origin, status, and kind-specific preview.
|
|
5
5
|
*/
|
|
6
6
|
import * as os from "node:os";
|
|
7
|
-
import {
|
|
7
|
+
import { arkToWireSchema, isArkSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
8
8
|
import { type Component, truncateToWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { theme } from "../../../modes/theme/theme";
|
|
10
10
|
import { shortenPath } from "../../../tools/render-utils";
|
|
@@ -168,16 +168,21 @@ export class InspectorPanel implements Component {
|
|
|
168
168
|
lines.push(theme.fg("dim", theme.boxRound.horizontal.repeat(Math.min(width - 2, 40))));
|
|
169
169
|
|
|
170
170
|
try {
|
|
171
|
-
const tool = raw as
|
|
172
|
-
const wire = (
|
|
173
|
-
const paramSchema = wire(tool
|
|
174
|
-
const inputSchema = wire(tool
|
|
171
|
+
const tool = raw as { parameters?: unknown; inputSchema?: unknown };
|
|
172
|
+
const wire = (schema: unknown): unknown => (isArkSchema(schema) ? arkToWireSchema(schema) : schema);
|
|
173
|
+
const paramSchema = wire(tool.parameters) as Record<string, unknown> | undefined;
|
|
174
|
+
const inputSchema = wire(tool.inputSchema) as Record<string, unknown> | undefined;
|
|
175
175
|
const params = paramSchema?.properties || inputSchema?.properties || {};
|
|
176
176
|
|
|
177
177
|
if (Object.keys(params).length === 0) {
|
|
178
178
|
lines.push(theme.fg("dim", " (no arguments)"));
|
|
179
179
|
} else {
|
|
180
|
-
const
|
|
180
|
+
const requiredValue = paramSchema?.required ?? inputSchema?.required;
|
|
181
|
+
const required = new Set(
|
|
182
|
+
Array.isArray(requiredValue)
|
|
183
|
+
? requiredValue.filter((value): value is string => typeof value === "string")
|
|
184
|
+
: [],
|
|
185
|
+
);
|
|
181
186
|
|
|
182
187
|
for (const [name, spec] of Object.entries(params)) {
|
|
183
188
|
const param = spec as any;
|
|
@@ -212,9 +212,7 @@ export class FooterComponent implements Component {
|
|
|
212
212
|
rightSide = `${modelName} • ${resolved ? resolved : `${theme.thinking.autoPending} auto`}`;
|
|
213
213
|
} else {
|
|
214
214
|
const thinkingLevel = state.thinkingLevel ?? ThinkingLevel.Off;
|
|
215
|
-
|
|
216
|
-
rightSide = `${modelName} • ${thinkingLevel}`;
|
|
217
|
-
}
|
|
215
|
+
rightSide = `${modelName} • ${thinkingLevel}`;
|
|
218
216
|
}
|
|
219
217
|
}
|
|
220
218
|
|
|
@@ -135,7 +135,7 @@ class OutlinedList extends Container {
|
|
|
135
135
|
this.invalidate();
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
render(width: number): readonly string[] {
|
|
138
|
+
override render(width: number): readonly string[] {
|
|
139
139
|
const borderColor = (text: string) => theme.fg("border", text);
|
|
140
140
|
const horizontal = borderColor(theme.boxRound.horizontal.repeat(Math.max(1, width)));
|
|
141
141
|
const innerWidth = Math.max(1, width - 2);
|
|
@@ -685,7 +685,7 @@ export class HookSelectorComponent extends Container {
|
|
|
685
685
|
return super.render(renderWidth);
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
-
dispose(): void {
|
|
688
|
+
override dispose(): void {
|
|
689
689
|
this.#countdown?.dispose();
|
|
690
690
|
}
|
|
691
691
|
}
|
|
@@ -254,8 +254,16 @@ export function thinkingLevelGlyph(level: ConfiguredThinkingLevel): string {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* A slim role chip: `●default ◉` — solid dot for configured assignments,
|
|
257
|
+
* A slim role chip: `● default ◉` — solid dot for configured assignments,
|
|
258
258
|
* hollow for auto-selected fallbacks, thinking glyph attached when set.
|
|
259
|
+
*
|
|
260
|
+
* The space after the status glyph is load-bearing. Under the `nerd` preset
|
|
261
|
+
* these are Nerd Font private-use icons (U+F111 / U+F10C) whose glyphs are
|
|
262
|
+
* drawn two cells wide, while `visibleWidth` counts them as one
|
|
263
|
+
* (`ambiguousIsNarrow: true` in tui/utils.ts — the PUA block is
|
|
264
|
+
* East_Asian_Width=Ambiguous). Without a separator the icon overhangs and
|
|
265
|
+
* eats the label's first character (`● default` renders as `●efault`).
|
|
266
|
+
* Mirrors the spacing already used for `status.success` in model-hub.
|
|
259
267
|
*/
|
|
260
268
|
export function formatRoleChip(role: string, assignment: RoleAssignment, settings: Settings): string {
|
|
261
269
|
const info = getRoleInfo(role, settings);
|
|
@@ -263,9 +271,9 @@ export function formatRoleChip(role: string, assignment: RoleAssignment, setting
|
|
|
263
271
|
const glyph = thinkingLevelGlyph(assignment.thinkingLevel);
|
|
264
272
|
const suffix = glyph ? ` ${theme.fg("dim", glyph)}` : "";
|
|
265
273
|
if (assignment.autoSelected) {
|
|
266
|
-
return theme.fg("dim", `${theme.status.shadowed}${label}`) + suffix;
|
|
274
|
+
return theme.fg("dim", `${theme.status.shadowed} ${label}`) + suffix;
|
|
267
275
|
}
|
|
268
|
-
return theme.fg(info.color ?? "muted", `${theme.status.enabled}${label}`) + suffix;
|
|
276
|
+
return theme.fg(info.color ?? "muted", `${theme.status.enabled} ${label}`) + suffix;
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
/** `$in/out` per-million cost pair; `free` when both legs are zero. */
|
|
@@ -837,7 +837,10 @@ export class ModelHubComponent implements Component {
|
|
|
837
837
|
chips.push({
|
|
838
838
|
label,
|
|
839
839
|
styled: assignedHere
|
|
840
|
-
?
|
|
840
|
+
? // Separator required: under the `nerd` preset this glyph is a
|
|
841
|
+
// two-cell-wide PUA icon that `visibleWidth` counts as one, so
|
|
842
|
+
// without it the icon overhangs and eats `label`'s first char.
|
|
843
|
+
theme.fg(info.color ?? "muted", `${theme.status.enabled} ${label}`) +
|
|
841
844
|
theme.fg("dim", ` ${theme.status.success}`)
|
|
842
845
|
: theme.fg(info.color ?? "muted", label),
|
|
843
846
|
role,
|
|
@@ -905,7 +905,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
905
905
|
* is mounted the list is detached from the child tree, so Container's
|
|
906
906
|
* child-walking dispose would miss its pending history-merge timer.
|
|
907
907
|
*/
|
|
908
|
-
dispose(): void {
|
|
908
|
+
override dispose(): void {
|
|
909
909
|
this.#sessionList.dispose();
|
|
910
910
|
super.dispose();
|
|
911
911
|
}
|
|
@@ -971,7 +971,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
971
971
|
* footer is always visible and never drifts as the list window resizes. The
|
|
972
972
|
* in-editor selector just appends the footer directly.
|
|
973
973
|
*/
|
|
974
|
-
render(width: number): readonly string[] {
|
|
974
|
+
override render(width: number): readonly string[] {
|
|
975
975
|
const lines: string[] = [];
|
|
976
976
|
for (const child of this.children) {
|
|
977
977
|
const childLines = child.render(width);
|
|
@@ -117,9 +117,10 @@ const modelSegment: StatusLineSegment = {
|
|
|
117
117
|
: `${theme.thinking.autoPending} auto`;
|
|
118
118
|
} else {
|
|
119
119
|
const level = state.thinkingLevel ?? ThinkingLevel.Off;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
thinkingDisplay =
|
|
121
|
+
level === ThinkingLevel.Off
|
|
122
|
+
? `${theme.status.disabled} off`
|
|
123
|
+
: (theme.thinking[level as keyof typeof theme.thinking] ?? level);
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -170,6 +170,26 @@ export class EventController {
|
|
|
170
170
|
#prevHideThinking = false;
|
|
171
171
|
#handlers: AgentSessionEventHandlers;
|
|
172
172
|
#terminalProgressActive = false;
|
|
173
|
+
// Coalescing window for `message_update` events at the subscription boundary.
|
|
174
|
+
// `message_update` carries the CUMULATIVE assistant message (every update
|
|
175
|
+
// re-lists all content blocks), so when a burst of deltas arrives faster than
|
|
176
|
+
// this window only the latest snapshot needs to rebuild streaming state — the
|
|
177
|
+
// intermediate rebuilds are redundant work. The TUI already caps the paint
|
|
178
|
+
// rate via its own render cadence; this caps the per-token handler work that
|
|
179
|
+
// feeds it. Speech stays intact: `#vocalizeDelta` runs at ARRIVAL for every
|
|
180
|
+
// delta before the snapshot is coalesced away.
|
|
181
|
+
#pendingMessageUpdate: Extract<AgentSessionEvent, { type: "message_update" }> | undefined = undefined;
|
|
182
|
+
#messageUpdateTimer: NodeJS.Timeout | undefined = undefined;
|
|
183
|
+
/** Tail of the serialized dispatch chain; see #runSerialized. */
|
|
184
|
+
#dispatchTail: Promise<void> = Promise.resolve();
|
|
185
|
+
/** Whether a chained run is currently in flight (awaiting its own awaits). */
|
|
186
|
+
#dispatchInFlight = false;
|
|
187
|
+
// Deltas already fed to speech at arrival by the coalescer. `#handleMessageUpdate`
|
|
188
|
+
// also vocalizes so the direct `handleEvent` path (tests, session focus replay)
|
|
189
|
+
// keeps working — the WeakSet makes the coalesced path speak each delta exactly
|
|
190
|
+
// once instead of twice.
|
|
191
|
+
#vocalizedMessageUpdates = new WeakSet<object>();
|
|
192
|
+
static readonly #MESSAGE_UPDATE_COALESCE_MS = 33;
|
|
173
193
|
|
|
174
194
|
constructor(private ctx: InteractiveModeContext) {
|
|
175
195
|
// Enhanced speech (`speech.enhanced`) rewrites blocks through the
|
|
@@ -253,6 +273,11 @@ export class EventController {
|
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
dispose(): void {
|
|
276
|
+
if (this.#messageUpdateTimer) {
|
|
277
|
+
clearTimeout(this.#messageUpdateTimer);
|
|
278
|
+
this.#messageUpdateTimer = undefined;
|
|
279
|
+
}
|
|
280
|
+
this.#pendingMessageUpdate = undefined;
|
|
256
281
|
this.#streamingReveal.stop();
|
|
257
282
|
this.#toolArgsReveal.stop();
|
|
258
283
|
this.#cancelIdleCompaction();
|
|
@@ -425,10 +450,135 @@ export class EventController {
|
|
|
425
450
|
}
|
|
426
451
|
|
|
427
452
|
subscribeToAgent(): void {
|
|
453
|
+
// Serialize non-update dispatch behind any in-flight handler run:
|
|
454
|
+
// AgentSession.#emit fires listeners fire-and-forget (it does not await
|
|
455
|
+
// listener promises), so without this a rapid stream tail
|
|
456
|
+
// (message_update → message_end → agent_end) could let a later callback
|
|
457
|
+
// overtake the coalesced flush's handler mid-await — agent_end removing
|
|
458
|
+
// `streamingComponent` before #handleMessageEnd finalizes and records
|
|
459
|
+
// the final message (issue #7443 follow-up). When the tail has settled,
|
|
460
|
+
// dispatch stays synchronous: the flush's streaming rebuild runs before
|
|
461
|
+
// the listener's first await, preserving the timing the coalescing
|
|
462
|
+
// tests assert on. `message_update` enqueue is itself synchronous and
|
|
463
|
+
// needs no serialization.
|
|
428
464
|
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
|
429
|
-
|
|
465
|
+
// Coalesce the cumulative `message_update` deltas of a streaming turn
|
|
466
|
+
// into at most one handler run per window. `#handleMessageUpdate` is
|
|
467
|
+
// synchronous, so without this every token re-runs the whole
|
|
468
|
+
// streaming rebuild (splitAssistantMessageToolTimeline, reveal
|
|
469
|
+
// setTarget, per-block tool-call reconciliation) even though the TUI
|
|
470
|
+
// paints at most ~30fps — at 40-100 tps the handler work then
|
|
471
|
+
// dominates the CPU profile of an idle-looking streaming session
|
|
472
|
+
// (issue #7443). Only the latest snapshot is meaningful; non-update
|
|
473
|
+
// events flush the pending snapshot first so ordering is preserved.
|
|
474
|
+
if (event.type === "message_update") {
|
|
475
|
+
this.#enqueueMessageUpdate(event);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
await this.#runSerialized(async () => {
|
|
479
|
+
await this.#flushPendingMessageUpdate();
|
|
480
|
+
await this.handleEvent(event);
|
|
481
|
+
});
|
|
430
482
|
});
|
|
431
483
|
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Run `run` in the serialized dispatch chain: every run is its own link on
|
|
487
|
+
* the tail, so a burst of events queued behind an in-flight run start one
|
|
488
|
+
* after the other, never concurrently. This closes two races (issue #7443
|
|
489
|
+
* follow-up): a rapid stream tail (message_update → message_end →
|
|
490
|
+
* agent_end) cannot overtake the coalesced flush mid-await — agent_end
|
|
491
|
+
* removing `streamingComponent` before #handleMessageEnd finalizes and
|
|
492
|
+
* records the final message — and two+ events landing in the same window
|
|
493
|
+
* cannot all resume from one shared await and dispatch in parallel. When
|
|
494
|
+
* the chain is drained, `run` starts synchronously (no intermediate
|
|
495
|
+
* microtask), preserving the synchronous-flush timing the coalescing
|
|
496
|
+
* tests assert on. A rejection propagates to the caller (the session's
|
|
497
|
+
* fire-and-forget emit) and the next event starts a fresh chain link
|
|
498
|
+
* instead of being dropped.
|
|
499
|
+
*/
|
|
500
|
+
async #runSerialized(run: () => Promise<void>): Promise<void> {
|
|
501
|
+
if (this.#dispatchInFlight) {
|
|
502
|
+
// Queue behind the CURRENT tail: the next run starts only after
|
|
503
|
+
// the previous one settles. Each waiter gets its own link, so a
|
|
504
|
+
// burst cannot fan out from the same shared await.
|
|
505
|
+
const link = this.#dispatchTail.then(
|
|
506
|
+
() => run(),
|
|
507
|
+
() => run(),
|
|
508
|
+
);
|
|
509
|
+
this.#dispatchTail = link;
|
|
510
|
+
void link.then(
|
|
511
|
+
() => {
|
|
512
|
+
// Only the tail owner clears the flag: a later chained
|
|
513
|
+
// link clears it when it settles as the tail.
|
|
514
|
+
if (this.#dispatchTail === link) this.#dispatchInFlight = false;
|
|
515
|
+
},
|
|
516
|
+
() => {
|
|
517
|
+
if (this.#dispatchTail === link) this.#dispatchInFlight = false;
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
await link;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
this.#dispatchInFlight = true;
|
|
524
|
+
const link = run();
|
|
525
|
+
this.#dispatchTail = link;
|
|
526
|
+
void link.then(
|
|
527
|
+
() => {
|
|
528
|
+
if (this.#dispatchTail === link) this.#dispatchInFlight = false;
|
|
529
|
+
},
|
|
530
|
+
() => {
|
|
531
|
+
if (this.#dispatchTail === link) this.#dispatchInFlight = false;
|
|
532
|
+
},
|
|
533
|
+
);
|
|
534
|
+
await link;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Queue a streaming `message_update` for the next coalesced handler run.
|
|
539
|
+
* Speech is per-delta, so the delta is vocalized at arrival before the
|
|
540
|
+
* snapshot is (possibly) superseded by a newer one.
|
|
541
|
+
*/
|
|
542
|
+
#enqueueMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): void {
|
|
543
|
+
// Speech is per-delta: every delta is spoken at arrival even when its
|
|
544
|
+
// cumulative snapshot is later superseded and never rebuilt.
|
|
545
|
+
this.#vocalizeDelta(event);
|
|
546
|
+
this.#vocalizedMessageUpdates.add(event);
|
|
547
|
+
this.#pendingMessageUpdate = event;
|
|
548
|
+
if (this.#messageUpdateTimer) return;
|
|
549
|
+
this.#messageUpdateTimer = setTimeout(() => {
|
|
550
|
+
this.#messageUpdateTimer = undefined;
|
|
551
|
+
// Mirror AgentSession.#emit: attach a catch so a streaming rebuild
|
|
552
|
+
// failure surfaces as a logged warning instead of a process-level
|
|
553
|
+
// unhandled rejection (the timer path has no listener to attach one).
|
|
554
|
+
// Runs inside the serialized dispatch chain so a message_end /
|
|
555
|
+
// agent_end landing mid-window cannot overtake this flush (issue
|
|
556
|
+
// #7443 follow-up).
|
|
557
|
+
void this.#runSerialized(async () => {
|
|
558
|
+
await this.#flushPendingMessageUpdate();
|
|
559
|
+
}).catch(err => {
|
|
560
|
+
logger.warn("Message update flush rejected", {
|
|
561
|
+
error: err instanceof Error ? err.message : String(err),
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
}, EventController.#MESSAGE_UPDATE_COALESCE_MS);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Run the coalesced `message_update` handler on the latest pending snapshot
|
|
569
|
+
* (dropping any superseded intermediates) and clear the queue. Safe to call
|
|
570
|
+
* more than once; no-ops when nothing is pending.
|
|
571
|
+
*/
|
|
572
|
+
async #flushPendingMessageUpdate(): Promise<void> {
|
|
573
|
+
if (this.#messageUpdateTimer) {
|
|
574
|
+
clearTimeout(this.#messageUpdateTimer);
|
|
575
|
+
this.#messageUpdateTimer = undefined;
|
|
576
|
+
}
|
|
577
|
+
const event = this.#pendingMessageUpdate;
|
|
578
|
+
if (!event) return;
|
|
579
|
+
this.#pendingMessageUpdate = undefined;
|
|
580
|
+
await this.handleEvent(event);
|
|
581
|
+
}
|
|
432
582
|
/**
|
|
433
583
|
* Clear every transcript-anchored/turn-scoped piece of state. Used by the
|
|
434
584
|
* session focus proxy when re-pointing the transcript at another session:
|
|
@@ -436,6 +586,11 @@ export class EventController {
|
|
|
436
586
|
* session's transcript and must not bleed into the new one.
|
|
437
587
|
*/
|
|
438
588
|
resetTranscriptAnchors(): void {
|
|
589
|
+
if (this.#messageUpdateTimer) {
|
|
590
|
+
clearTimeout(this.#messageUpdateTimer);
|
|
591
|
+
this.#messageUpdateTimer = undefined;
|
|
592
|
+
}
|
|
593
|
+
this.#pendingMessageUpdate = undefined;
|
|
439
594
|
this.#resetReadGroup();
|
|
440
595
|
this.#lastVisibleBlockCount = 0;
|
|
441
596
|
this.#renderedCustomMessages.clear();
|
|
@@ -814,7 +969,9 @@ export class EventController {
|
|
|
814
969
|
|
|
815
970
|
async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
|
|
816
971
|
this.#ensureWorkingLoaderWhileStreaming();
|
|
817
|
-
this.#
|
|
972
|
+
if (!this.#vocalizedMessageUpdates.delete(event)) {
|
|
973
|
+
this.#vocalizeDelta(event);
|
|
974
|
+
}
|
|
818
975
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
819
976
|
const unlockedThinkingVisibility = this.ctx.noteDisplayableThinkingContent(event.message);
|
|
820
977
|
if (unlockedThinkingVisibility) {
|
|
@@ -1448,11 +1605,29 @@ export class EventController {
|
|
|
1448
1605
|
typeof details.title === "string" &&
|
|
1449
1606
|
typeof details.planExists === "boolean"
|
|
1450
1607
|
) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1608
|
+
// Dispatch the approval WITHOUT blocking the serialized event
|
|
1609
|
+
// dispatch chain. `handlePlanApproval` -> `#approvePlan` awaits
|
|
1610
|
+
// `session.prompt` for the ENTIRE approved-execution turn; awaiting
|
|
1611
|
+
// it here (this handler runs inside `#runSerialized`) would hold the
|
|
1612
|
+
// single dispatch link for the whole run, so the run's own
|
|
1613
|
+
// agent_start / message_start / tool / coalesced message_update
|
|
1614
|
+
// events queue behind it on `#dispatchTail` and the chat stays blank
|
|
1615
|
+
// until execution finishes (issue #7684, follow-up to #5688 which
|
|
1616
|
+
// only closed the overlay). Detaching frees the link the moment this
|
|
1617
|
+
// handler returns; the approval overlay and the turn's live events
|
|
1618
|
+
// then render. The approval flow surfaces its own failures via
|
|
1619
|
+
// `showError`, so only an unexpected rejection is logged here.
|
|
1620
|
+
void this.ctx
|
|
1621
|
+
.handlePlanApproval({
|
|
1622
|
+
planFilePath: details.planFilePath,
|
|
1623
|
+
title: details.title,
|
|
1624
|
+
planExists: details.planExists,
|
|
1625
|
+
})
|
|
1626
|
+
.catch(err => {
|
|
1627
|
+
logger.warn("Plan approval dispatch failed", {
|
|
1628
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1629
|
+
});
|
|
1630
|
+
});
|
|
1456
1631
|
}
|
|
1457
1632
|
}
|
|
1458
1633
|
}
|
|
@@ -1479,6 +1654,12 @@ export class EventController {
|
|
|
1479
1654
|
// model/thinking level until the terminal settle.
|
|
1480
1655
|
if (event.isTerminal === false) {
|
|
1481
1656
|
await this.ctx.flushPendingModelSwitch();
|
|
1657
|
+
// Reaching here means the first guard passed, so `isStreaming` is already
|
|
1658
|
+
// false: a command issued from now on mounts immediately. Leaving earlier
|
|
1659
|
+
// panels queued would render them out of order, minutes later, after the
|
|
1660
|
+
// user was told they were only waiting for the turn. The transcript is
|
|
1661
|
+
// quiescent at a settle, which is the condition #4806 wanted.
|
|
1662
|
+
this.ctx.flushPendingCommandOutput();
|
|
1482
1663
|
return;
|
|
1483
1664
|
}
|
|
1484
1665
|
setTerminalTitleState("idle");
|
|
@@ -128,7 +128,7 @@ export class ExtensionUiController {
|
|
|
128
128
|
};
|
|
129
129
|
this.ctx.setToolUIContext(uiContext, true);
|
|
130
130
|
this.#toolUIContext = uiContext;
|
|
131
|
-
this.ctx.session.setUsageFallbackConfirmer?.(confirmation => {
|
|
131
|
+
this.ctx.session.setUsageFallbackConfirmer?.((confirmation, signal) => {
|
|
132
132
|
const reserve =
|
|
133
133
|
confirmation.remainingPercent === undefined
|
|
134
134
|
? "inside the configured reserve margin"
|
|
@@ -136,6 +136,7 @@ export class ExtensionUiController {
|
|
|
136
136
|
return this.showHookConfirm(
|
|
137
137
|
"Coding-plan reserve reached",
|
|
138
138
|
`${confirmation.from} has ${reserve}. Switch to ${confirmation.to}? Choose No to keep using the current plan.`,
|
|
139
|
+
{ signal },
|
|
139
140
|
);
|
|
140
141
|
});
|
|
141
142
|
|
|
@@ -165,7 +166,7 @@ export class ExtensionUiController {
|
|
|
165
166
|
this.ctx.sessionManager.appendLabelChange(targetId, label);
|
|
166
167
|
},
|
|
167
168
|
getActiveTools: () => this.ctx.session.getEnabledToolNames(),
|
|
168
|
-
getAllTools: () => this.ctx.session.
|
|
169
|
+
getAllTools: () => this.ctx.session.getAllToolInfos(),
|
|
169
170
|
setActiveTools: toolNames => this.ctx.session.setActiveToolsByName(toolNames),
|
|
170
171
|
setModel: async model => {
|
|
171
172
|
const key = await this.ctx.session.modelRegistry.getApiKey(model);
|
|
@@ -398,7 +399,7 @@ export class ExtensionUiController {
|
|
|
398
399
|
this.ctx.sessionManager.appendLabelChange(targetId, label);
|
|
399
400
|
},
|
|
400
401
|
getActiveTools: () => this.ctx.session.getEnabledToolNames(),
|
|
401
|
-
getAllTools: () => this.ctx.session.
|
|
402
|
+
getAllTools: () => this.ctx.session.getAllToolInfos(),
|
|
402
403
|
setActiveTools: toolNames => this.ctx.session.setActiveToolsByName(toolNames),
|
|
403
404
|
setModel: async model => {
|
|
404
405
|
const key = await this.ctx.session.modelRegistry.getApiKey(model);
|
|
@@ -89,6 +89,66 @@ function raceAbortSignal<T>(promise: Promise<T>, signal: AbortSignal, createErro
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
type ActiveMCPOAuthFlow = {
|
|
93
|
+
cancel: (reason: string) => void;
|
|
94
|
+
completion: Promise<void>;
|
|
95
|
+
complete: () => void;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
type MCPOAuthFlowCoordinator = {
|
|
99
|
+
active?: ActiveMCPOAuthFlow;
|
|
100
|
+
transition: Promise<void>;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const mcpOAuthFlowCoordinators = new WeakMap<object, MCPOAuthFlowCoordinator>();
|
|
104
|
+
const MCP_OAUTH_SUPERSEDED_REASON = "MCP OAuth flow superseded by a new login";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Serialize MCP OAuth ownership across slash-command controller instances.
|
|
108
|
+
* Interactive mode creates a new controller for every command, while the
|
|
109
|
+
* manual-input manager remains stable for the session and is therefore the
|
|
110
|
+
* lifecycle key.
|
|
111
|
+
*/
|
|
112
|
+
async function claimMCPOAuthFlow(owner: object, cancel: (reason: string) => void): Promise<{ release: () => void }> {
|
|
113
|
+
let coordinator = mcpOAuthFlowCoordinators.get(owner);
|
|
114
|
+
if (!coordinator) {
|
|
115
|
+
coordinator = { transition: Promise.resolve() };
|
|
116
|
+
mcpOAuthFlowCoordinators.set(owner, coordinator);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const precedingTransition = coordinator.transition;
|
|
120
|
+
const transition = Promise.withResolvers<void>();
|
|
121
|
+
coordinator.transition = transition.promise;
|
|
122
|
+
await precedingTransition;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const active = coordinator.active;
|
|
126
|
+
if (active) {
|
|
127
|
+
active.cancel(MCP_OAUTH_SUPERSEDED_REASON);
|
|
128
|
+
await active.completion;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const completion = Promise.withResolvers<void>();
|
|
132
|
+
const flow: ActiveMCPOAuthFlow = {
|
|
133
|
+
cancel,
|
|
134
|
+
completion: completion.promise,
|
|
135
|
+
complete: () => completion.resolve(),
|
|
136
|
+
};
|
|
137
|
+
coordinator.active = flow;
|
|
138
|
+
let released = false;
|
|
139
|
+
return {
|
|
140
|
+
release: () => {
|
|
141
|
+
if (released) return;
|
|
142
|
+
released = true;
|
|
143
|
+
if (coordinator.active === flow) coordinator.active = undefined;
|
|
144
|
+
flow.complete();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
} finally {
|
|
148
|
+
transition.resolve();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
92
152
|
/**
|
|
93
153
|
* Minimum column budget for URL wrapping. Below this the terminal is
|
|
94
154
|
* effectively unusable, but we still emit chunks so no character is silently
|
|
@@ -782,28 +842,23 @@ export class MCPCommandController {
|
|
|
782
842
|
const resolvedClientSecret = clientSecret.trim() || undefined;
|
|
783
843
|
|
|
784
844
|
const manualInput = this.ctx.oauthManualInput;
|
|
785
|
-
if (manualInput.hasPending()) {
|
|
786
|
-
const pendingProvider = manualInput.pendingProviderId ?? "another provider";
|
|
787
|
-
throw new Error(
|
|
788
|
-
`OAuth login already in progress for ${pendingProvider}. Complete or cancel it before starting MCP OAuth.`,
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
845
|
let manualInputClaim: { promise: Promise<string>; clear: (reason?: string) => void } | undefined;
|
|
792
846
|
const oauthTimeout = new AbortController();
|
|
793
|
-
//
|
|
794
|
-
// its own reason and leaves this flag false so the
|
|
795
|
-
//
|
|
796
|
-
let
|
|
797
|
-
const
|
|
798
|
-
|
|
847
|
+
// Esc, external aborts, and a replacement MCP flow route through here;
|
|
848
|
+
// the timeout path sets its own reason and leaves this flag false so the
|
|
849
|
+
// catch can distinguish cancellation (status) from deadline failure.
|
|
850
|
+
let cancellationRequested = false;
|
|
851
|
+
const requestCancellation = (reason: string): void => {
|
|
852
|
+
cancellationRequested = true;
|
|
799
853
|
if (!oauthTimeout.signal.aborted) oauthTimeout.abort(reason);
|
|
800
854
|
};
|
|
855
|
+
const flowClaim = await claimMCPOAuthFlow(manualInput, requestCancellation);
|
|
801
856
|
const originalOnEscape = this.ctx.editor.onEscape;
|
|
802
|
-
this.ctx.editor.onEscape = () =>
|
|
857
|
+
this.ctx.editor.onEscape = () => requestCancellation(MCP_OAUTH_USER_CANCEL_REASON);
|
|
803
858
|
const externalSignal = opts?.abortSignal;
|
|
804
859
|
const onExternalAbort = (): void => {
|
|
805
860
|
const reason = externalSignal?.reason;
|
|
806
|
-
|
|
861
|
+
requestCancellation(typeof reason === "string" ? reason : MCP_OAUTH_USER_CANCEL_REASON);
|
|
807
862
|
};
|
|
808
863
|
if (externalSignal?.aborted) {
|
|
809
864
|
onExternalAbort();
|
|
@@ -811,6 +866,12 @@ export class MCPCommandController {
|
|
|
811
866
|
externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
812
867
|
}
|
|
813
868
|
try {
|
|
869
|
+
if (manualInput.hasPending()) {
|
|
870
|
+
const pendingProvider = manualInput.pendingProviderId ?? "another provider";
|
|
871
|
+
throw new Error(
|
|
872
|
+
`OAuth login already in progress for ${pendingProvider}. Complete or cancel it before starting MCP OAuth.`,
|
|
873
|
+
);
|
|
874
|
+
}
|
|
814
875
|
// Create OAuth flow
|
|
815
876
|
const flow = new MCPOAuthFlow(
|
|
816
877
|
{
|
|
@@ -887,7 +948,7 @@ export class MCPCommandController {
|
|
|
887
948
|
|
|
888
949
|
const createAbortError = (): Error => {
|
|
889
950
|
const reason = String(oauthTimeout.signal.reason ?? "MCP OAuth flow aborted");
|
|
890
|
-
return
|
|
951
|
+
return cancellationRequested ? new MCPOAuthCancelledError() : new Error(reason);
|
|
891
952
|
};
|
|
892
953
|
if (oauthTimeout.signal.aborted) throw createAbortError();
|
|
893
954
|
|
|
@@ -934,11 +995,10 @@ export class MCPCommandController {
|
|
|
934
995
|
resource: flow.resource,
|
|
935
996
|
};
|
|
936
997
|
} catch (error) {
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
if (userCancelled) {
|
|
998
|
+
// Esc, an external abort, or a newer MCP flow are neutral
|
|
999
|
+
// cancellations. The timeout path also aborts the controller but does
|
|
1000
|
+
// not set this flag, so it remains a surfaced error.
|
|
1001
|
+
if (cancellationRequested) {
|
|
942
1002
|
throw new MCPOAuthCancelledError();
|
|
943
1003
|
}
|
|
944
1004
|
|
|
@@ -960,6 +1020,7 @@ export class MCPCommandController {
|
|
|
960
1020
|
this.ctx.editor.onEscape = originalOnEscape;
|
|
961
1021
|
externalSignal?.removeEventListener("abort", onExternalAbort);
|
|
962
1022
|
manualInputClaim?.clear("Manual MCP OAuth input cleared");
|
|
1023
|
+
flowClaim.release();
|
|
963
1024
|
}
|
|
964
1025
|
}
|
|
965
1026
|
|