@oh-my-pi/pi-coding-agent 17.0.3 → 17.0.5
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 +88 -35
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/task/types.d.ts +6 -6
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +18 -111
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-loader.ts +31 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/stt/recorder.ts +68 -55
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/types.ts +11 -8
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
package/src/stt/recorder.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RecordingHandle {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const isWindows = process.platform === "win32";
|
|
14
|
+
const linuxFFmpegFormats = new Map<string, "pulse" | "alsa">();
|
|
15
|
+
const ffmpegCaptureFlags = ["-hide_banner", "-loglevel", "error", "-nostats"];
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Returns available recording tools in priority order.
|
|
@@ -36,6 +38,36 @@ async function detectWindowsAudioDevice(bin: string): Promise<string> {
|
|
|
36
38
|
return audioDevices[0];
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
async function ffmpegInputArgs(bin: string): Promise<string[]> {
|
|
42
|
+
if (isWindows) {
|
|
43
|
+
return ["-f", "dshow", "-i", `audio=${await detectWindowsAudioDevice(bin)}`];
|
|
44
|
+
}
|
|
45
|
+
if (process.platform === "darwin") {
|
|
46
|
+
return ["-f", "avfoundation", "-i", ":default"];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let format = linuxFFmpegFormats.get(bin);
|
|
50
|
+
if (!format) {
|
|
51
|
+
const result = await $`${bin} -hide_banner -demuxers`.quiet().nothrow();
|
|
52
|
+
if (result.exitCode !== 0) {
|
|
53
|
+
const stderr = result.stderr.toString().trim();
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Could not inspect ffmpeg input formats (code ${result.exitCode}): ${stderr || "(no output)"}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const demuxers = result.stdout.toString();
|
|
59
|
+
if (/^\s*D\s+(?:d\s+)?pulse(?:\s|$)/m.test(demuxers)) {
|
|
60
|
+
format = "pulse";
|
|
61
|
+
} else if (/^\s*D\s+(?:d\s+)?alsa(?:\s|$)/m.test(demuxers)) {
|
|
62
|
+
format = "alsa";
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error("ffmpeg supports neither PulseAudio nor ALSA input on Linux");
|
|
65
|
+
}
|
|
66
|
+
linuxFFmpegFormats.set(bin, format);
|
|
67
|
+
}
|
|
68
|
+
return ["-f", format, "-i", "default"];
|
|
69
|
+
}
|
|
70
|
+
|
|
39
71
|
// ── Recording implementations ──────────────────────────────────────
|
|
40
72
|
|
|
41
73
|
async function startSoxRecording(bin: string, outputPath: string): Promise<RecordingHandle> {
|
|
@@ -44,7 +76,7 @@ async function startSoxRecording(bin: string, outputPath: string): Promise<Recor
|
|
|
44
76
|
|
|
45
77
|
const proc = Bun.spawn([bin, ...inputArgs, "-r", "16000", "-c", "1", "-b", "16", "-t", "wav", outputPath], {
|
|
46
78
|
stdout: "pipe",
|
|
47
|
-
stderr: "
|
|
79
|
+
stderr: "pipe",
|
|
48
80
|
});
|
|
49
81
|
await verifyProcessAlive(proc, "sox");
|
|
50
82
|
return {
|
|
@@ -56,48 +88,24 @@ async function startSoxRecording(bin: string, outputPath: string): Promise<Recor
|
|
|
56
88
|
}
|
|
57
89
|
|
|
58
90
|
async function startFFmpegRecording(bin: string, outputPath: string): Promise<RecordingHandle> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"-sample_fmt",
|
|
73
|
-
"s16",
|
|
74
|
-
"-y",
|
|
75
|
-
outputPath,
|
|
76
|
-
];
|
|
77
|
-
} else if (process.platform === "darwin") {
|
|
78
|
-
args = [
|
|
79
|
-
bin,
|
|
80
|
-
"-f",
|
|
81
|
-
"avfoundation",
|
|
82
|
-
"-i",
|
|
83
|
-
":default",
|
|
84
|
-
"-ar",
|
|
85
|
-
"16000",
|
|
86
|
-
"-ac",
|
|
87
|
-
"1",
|
|
88
|
-
"-sample_fmt",
|
|
89
|
-
"s16",
|
|
90
|
-
"-y",
|
|
91
|
-
outputPath,
|
|
92
|
-
];
|
|
93
|
-
} else {
|
|
94
|
-
args = [bin, "-f", "pulse", "-i", "default", "-ar", "16000", "-ac", "1", "-sample_fmt", "s16", "-y", outputPath];
|
|
95
|
-
}
|
|
91
|
+
const args = [
|
|
92
|
+
bin,
|
|
93
|
+
...ffmpegCaptureFlags,
|
|
94
|
+
...(await ffmpegInputArgs(bin)),
|
|
95
|
+
"-ar",
|
|
96
|
+
"16000",
|
|
97
|
+
"-ac",
|
|
98
|
+
"1",
|
|
99
|
+
"-sample_fmt",
|
|
100
|
+
"s16",
|
|
101
|
+
"-y",
|
|
102
|
+
outputPath,
|
|
103
|
+
];
|
|
96
104
|
|
|
97
105
|
const proc = Bun.spawn(args, {
|
|
98
106
|
stdin: "pipe",
|
|
99
107
|
stdout: "pipe",
|
|
100
|
-
stderr: "
|
|
108
|
+
stderr: "pipe",
|
|
101
109
|
});
|
|
102
110
|
await verifyProcessAlive(proc, "ffmpeg");
|
|
103
111
|
|
|
@@ -119,7 +127,7 @@ async function startFFmpegRecording(bin: string, outputPath: string): Promise<Re
|
|
|
119
127
|
async function startArecordRecording(bin: string, outputPath: string): Promise<RecordingHandle> {
|
|
120
128
|
const proc = Bun.spawn([bin, "-f", "S16_LE", "-r", "16000", "-c", "1", outputPath], {
|
|
121
129
|
stdout: "pipe",
|
|
122
|
-
stderr: "
|
|
130
|
+
stderr: "pipe",
|
|
123
131
|
});
|
|
124
132
|
await verifyProcessAlive(proc, "arecord");
|
|
125
133
|
return {
|
|
@@ -260,20 +268,19 @@ async function startPowerShellRecording(outputPath: string): Promise<RecordingHa
|
|
|
260
268
|
|
|
261
269
|
// ── Health check ───────────────────────────────────────────────────
|
|
262
270
|
|
|
263
|
-
type RecorderProcess = Subprocess<"ignore" | "pipe", "pipe", "
|
|
271
|
+
type RecorderProcess = Subprocess<"ignore" | "pipe", "pipe", "pipe">;
|
|
264
272
|
|
|
265
273
|
async function verifyProcessAlive(proc: RecorderProcess, tool: string): Promise<void> {
|
|
266
274
|
await Bun.sleep(300);
|
|
267
275
|
|
|
268
276
|
const exited = await Promise.race([proc.exited.then(code => code), Bun.sleep(0).then(() => "running" as const)]);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (proc.stderr && typeof proc.stderr !== "number") {
|
|
273
|
-
stderr = await new Response(proc.stderr as ReadableStream).text();
|
|
274
|
-
}
|
|
275
|
-
throw new Error(`${tool} exited immediately (code ${exited}): ${stderr.trim() || "(no output)"}`);
|
|
277
|
+
if (exited === "running") {
|
|
278
|
+
void proc.stderr.pipeTo(new WritableStream<Uint8Array>()).catch(() => {});
|
|
279
|
+
return;
|
|
276
280
|
}
|
|
281
|
+
|
|
282
|
+
const stderr = await new Response(proc.stderr).text();
|
|
283
|
+
throw new Error(`${tool} exited immediately (code ${exited}): ${stderr.trim() || "(no output)"}`);
|
|
277
284
|
}
|
|
278
285
|
|
|
279
286
|
// ── Public API ─────────────────────────────────────────────────────
|
|
@@ -415,12 +422,18 @@ async function streamingRecorderArgs(recorder: ResolvedRecorder): Promise<string
|
|
|
415
422
|
case "arecord":
|
|
416
423
|
return [bin, "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw", "-"];
|
|
417
424
|
case "ffmpeg": {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
425
|
+
return [
|
|
426
|
+
bin,
|
|
427
|
+
...ffmpegCaptureFlags,
|
|
428
|
+
...(await ffmpegInputArgs(bin)),
|
|
429
|
+
"-ar",
|
|
430
|
+
"16000",
|
|
431
|
+
"-ac",
|
|
432
|
+
"1",
|
|
433
|
+
"-f",
|
|
434
|
+
"s16le",
|
|
435
|
+
"pipe:1",
|
|
436
|
+
];
|
|
424
437
|
}
|
|
425
438
|
case "powershell":
|
|
426
439
|
throw new Error("PowerShell recorder cannot stream PCM to a pipe");
|
|
@@ -439,7 +452,7 @@ async function startStreamingRecordingWithRecorder(
|
|
|
439
452
|
): Promise<StreamingRecordingHandle> {
|
|
440
453
|
const args = await streamingRecorderArgs(recorder);
|
|
441
454
|
logger.debug("Starting streaming audio recording", { tool: recorder.tool, bin: recorder.bin });
|
|
442
|
-
const proc = Bun.spawn(args, { stdin: "pipe", stdout: "pipe", stderr: "
|
|
455
|
+
const proc = Bun.spawn(args, { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
|
|
443
456
|
|
|
444
457
|
// Read s16le bytes off stdout, carrying any trailing odd byte across chunk
|
|
445
458
|
// boundaries so a sample is never split. Runs until the process closes stdout.
|
package/src/task/executor.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
formatModelSelectorValue,
|
|
16
16
|
formatModelStringWithRouting,
|
|
17
17
|
resolveAgentPrewalkPattern,
|
|
18
|
+
resolveConfiguredModelPatterns,
|
|
18
19
|
resolveModelOverride,
|
|
19
20
|
resolveModelOverrideWithAuthFallback,
|
|
20
21
|
} from "../config/model-resolver";
|
|
@@ -161,22 +162,44 @@ function resolveSubagentRetryFallbackCandidates(
|
|
|
161
162
|
return candidates;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
function resolveSubagentDefaultRetryFallbackChain(settings: Settings): string[] | undefined {
|
|
166
|
+
const fallbackChain = settings.get("retry.fallbackChains")?.default;
|
|
167
|
+
if (
|
|
168
|
+
!Array.isArray(fallbackChain) ||
|
|
169
|
+
fallbackChain.length === 0 ||
|
|
170
|
+
!fallbackChain.every(entry => typeof entry === "string")
|
|
171
|
+
) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
return fallbackChain;
|
|
175
|
+
}
|
|
176
|
+
|
|
164
177
|
function installSubagentRetryFallbackChain(args: {
|
|
165
178
|
settings: Settings;
|
|
166
179
|
id: string;
|
|
167
180
|
candidates: SubagentRetryFallbackCandidate[];
|
|
181
|
+
defaultFallbackChain: string[] | undefined;
|
|
168
182
|
model: Model<Api> | undefined;
|
|
169
183
|
authFallbackUsed: boolean;
|
|
170
184
|
}): string | undefined {
|
|
171
|
-
const { settings, id, candidates, model, authFallbackUsed } = args;
|
|
172
|
-
if (!model || authFallbackUsed || candidates.length
|
|
185
|
+
const { settings, id, candidates, defaultFallbackChain, model, authFallbackUsed } = args;
|
|
186
|
+
if (!model || authFallbackUsed || candidates.length === 0) return undefined;
|
|
173
187
|
|
|
174
188
|
const selectedIndex = candidates.findIndex(
|
|
175
189
|
candidate => candidate.model.provider === model.provider && candidate.model.id === model.id,
|
|
176
190
|
);
|
|
177
191
|
if (selectedIndex < 0) return undefined;
|
|
178
192
|
const fallbackSelectors = candidates.slice(selectedIndex + 1).map(candidate => candidate.selector);
|
|
179
|
-
|
|
193
|
+
const existingFallbackChains = settings.get("retry.fallbackChains");
|
|
194
|
+
// A single explicit model may reuse a configured default chain, but never an implicit parent fallback.
|
|
195
|
+
const fallbackChain = fallbackSelectors.length > 0 ? fallbackSelectors : defaultFallbackChain;
|
|
196
|
+
if (
|
|
197
|
+
!Array.isArray(fallbackChain) ||
|
|
198
|
+
fallbackChain.length === 0 ||
|
|
199
|
+
!fallbackChain.every(entry => typeof entry === "string")
|
|
200
|
+
) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
180
203
|
|
|
181
204
|
const role = `${SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX}${id}`;
|
|
182
205
|
const modelRoles: Record<string, string> = {};
|
|
@@ -189,10 +212,10 @@ function installSubagentRetryFallbackChain(args: {
|
|
|
189
212
|
}
|
|
190
213
|
modelRoles[role] = candidates[selectedIndex].selector;
|
|
191
214
|
settings.override("modelRoles", modelRoles);
|
|
215
|
+
// Insert the task-specific role first so another role assigned to the same model cannot capture fallback routing.
|
|
192
216
|
const fallbackChains: Record<string, string[]> = {
|
|
193
|
-
[role]:
|
|
217
|
+
[role]: fallbackChain,
|
|
194
218
|
};
|
|
195
|
-
const existingFallbackChains = settings.get("retry.fallbackChains");
|
|
196
219
|
for (const existingRole in existingFallbackChains) {
|
|
197
220
|
if (existingRole !== role) {
|
|
198
221
|
fallbackChains[existingRole] = existingFallbackChains[existingRole];
|
|
@@ -914,7 +937,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
914
937
|
const finalOutputChunks: string[] = [];
|
|
915
938
|
const RECENT_OUTPUT_TAIL_BYTES = 8 * 1024;
|
|
916
939
|
let recentOutputTail = "";
|
|
917
|
-
let
|
|
940
|
+
let recentOutputDirty = false;
|
|
918
941
|
let resolved = false;
|
|
919
942
|
let abortSent = false;
|
|
920
943
|
let abortReason: AbortReason | undefined;
|
|
@@ -1054,7 +1077,22 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1054
1077
|
let lastProgressEmitMs = 0;
|
|
1055
1078
|
let progressTimeoutId: NodeJS.Timeout | null = null;
|
|
1056
1079
|
|
|
1080
|
+
// Recompute progress.recentOutput from the capped tail. Deferred: text_delta
|
|
1081
|
+
// appends only extend the tail and mark it dirty; the (up to 8KB) split/filter
|
|
1082
|
+
// runs synchronously here, immediately before the ONLY places the progress
|
|
1083
|
+
// object is snapshotted ({...progress} for onProgress and the eventBus
|
|
1084
|
+
// progress channel, both inside emitProgressNow — including the
|
|
1085
|
+
// scheduleProgress(flush) finalize/error/cancel paths). Observers therefore
|
|
1086
|
+
// always see exact state; no staleness beyond the existing 150ms coalescing.
|
|
1087
|
+
const refreshRecentOutput = () => {
|
|
1088
|
+
if (!recentOutputDirty) return;
|
|
1089
|
+
recentOutputDirty = false;
|
|
1090
|
+
const filtered = recentOutputTail.split("\n").filter(line => line.trim());
|
|
1091
|
+
progress.recentOutput = filtered.slice(-8).reverse();
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1057
1094
|
const emitProgressNow = () => {
|
|
1095
|
+
refreshRecentOutput();
|
|
1058
1096
|
progress.durationMs = Date.now() - startTime;
|
|
1059
1097
|
onProgress?.({ ...progress });
|
|
1060
1098
|
const activityGist =
|
|
@@ -1137,36 +1175,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1137
1175
|
return message.usage;
|
|
1138
1176
|
};
|
|
1139
1177
|
|
|
1140
|
-
const updateRecentOutputLines = () => {
|
|
1141
|
-
const lines = recentOutputTail.split("\n");
|
|
1142
|
-
const filtered = lines.filter(line => line.trim());
|
|
1143
|
-
progress.recentOutput = filtered.slice(-8).reverse();
|
|
1144
|
-
// The tail's last raw segment (after its final newline) is "represented"
|
|
1145
|
-
// in recentOutput only when it trims non-empty — an empty/whitespace-only
|
|
1146
|
-
// trailing segment is filtered out, so recentOutput[0] is then the line
|
|
1147
|
-
// before it, not the tail's true last line.
|
|
1148
|
-
tailLastLineRepresentable = lines[lines.length - 1].trim().length > 0;
|
|
1149
|
-
};
|
|
1150
|
-
|
|
1151
1178
|
const appendRecentOutputTail = (text: string) => {
|
|
1152
1179
|
if (!text) return;
|
|
1153
1180
|
recentOutputTail += text;
|
|
1154
|
-
|
|
1155
|
-
if (truncated) {
|
|
1181
|
+
if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
|
|
1156
1182
|
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
|
|
1157
1183
|
}
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
|
|
1162
|
-
// already represented (trims non-empty) — otherwise boundaries shift and a
|
|
1163
|
-
// full recompute is required. Appending to a non-empty line keeps it non-empty,
|
|
1164
|
-
// so the flag stays valid across consecutive fast-path tokens.
|
|
1165
|
-
if (truncated || text.includes("\n") || !tailLastLineRepresentable || progress.recentOutput.length === 0) {
|
|
1166
|
-
updateRecentOutputLines();
|
|
1167
|
-
} else {
|
|
1168
|
-
progress.recentOutput = [progress.recentOutput[0] + text, ...progress.recentOutput.slice(1)];
|
|
1169
|
-
}
|
|
1184
|
+
// O(chunk) hot path: this runs on every text_delta token (hundreds/
|
|
1185
|
+
// thousands per second while streaming). Line reconstruction is deferred
|
|
1186
|
+
// to refreshRecentOutput() at the emit boundary.
|
|
1187
|
+
recentOutputDirty = true;
|
|
1170
1188
|
};
|
|
1171
1189
|
|
|
1172
1190
|
const replaceRecentOutputFromContent = (content: unknown[]) => {
|
|
@@ -1181,12 +1199,12 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1181
1199
|
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
|
|
1182
1200
|
}
|
|
1183
1201
|
}
|
|
1184
|
-
|
|
1202
|
+
recentOutputDirty = true;
|
|
1185
1203
|
};
|
|
1186
1204
|
|
|
1187
1205
|
const resetRecentOutput = () => {
|
|
1188
1206
|
recentOutputTail = "";
|
|
1189
|
-
|
|
1207
|
+
recentOutputDirty = false;
|
|
1190
1208
|
progress.recentOutput = [];
|
|
1191
1209
|
};
|
|
1192
1210
|
|
|
@@ -2319,6 +2337,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2319
2337
|
}
|
|
2320
2338
|
checkAbort();
|
|
2321
2339
|
|
|
2340
|
+
const configuredModelPatterns = resolveConfiguredModelPatterns(modelPatterns, settings);
|
|
2341
|
+
const defaultRetryFallbackChain =
|
|
2342
|
+
configuredModelPatterns.length === 1
|
|
2343
|
+
? resolveSubagentDefaultRetryFallbackChain(subagentSettings)
|
|
2344
|
+
: undefined;
|
|
2322
2345
|
const {
|
|
2323
2346
|
model,
|
|
2324
2347
|
thinkingLevel: resolvedThinkingLevel,
|
|
@@ -2352,6 +2375,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2352
2375
|
settings: subagentSettings,
|
|
2353
2376
|
id,
|
|
2354
2377
|
candidates: resolveSubagentRetryFallbackCandidates(modelPatterns, modelRegistry, settings),
|
|
2378
|
+
defaultFallbackChain: defaultRetryFallbackChain,
|
|
2355
2379
|
model,
|
|
2356
2380
|
authFallbackUsed,
|
|
2357
2381
|
});
|
|
@@ -2479,6 +2503,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2479
2503
|
model || modelOverride === undefined ? undefined : options.parentActiveModelPattern,
|
|
2480
2504
|
modelPatternFallbackRole:
|
|
2481
2505
|
model || modelOverride === undefined ? undefined : `${SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX}${id}`,
|
|
2506
|
+
modelPatternDefaultFallbackChain:
|
|
2507
|
+
model || modelOverride === undefined ? undefined : defaultRetryFallbackChain,
|
|
2482
2508
|
thinkingLevel: effectiveThinkingLevel,
|
|
2483
2509
|
toolNames,
|
|
2484
2510
|
outputSchema,
|
package/src/task/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { ToolSession } from "..";
|
|
|
21
21
|
import type { Theme } from "../modes/theme/theme";
|
|
22
22
|
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
23
23
|
import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
|
|
24
|
+
import taskAsyncContractTemplate from "../prompts/tools/task-async-contract.md" with { type: "text" };
|
|
24
25
|
import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
|
|
25
26
|
import { truncateForPrompt } from "../tools/approval";
|
|
26
27
|
import { isIrcEnabled } from "../tools/hub";
|
|
@@ -894,14 +895,16 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
894
895
|
failedSchedules.length > 0
|
|
895
896
|
? ` Failed to schedule ${failedSchedules.length} spawn${failedSchedules.length === 1 ? "" : "s"}: ${failedSchedules.join("; ")}.`
|
|
896
897
|
: "";
|
|
897
|
-
const coordinationHint =
|
|
898
|
+
const coordinationHint = [
|
|
898
899
|
started.length === 1
|
|
899
900
|
? ircEnabled
|
|
900
901
|
? `DM \`${started[0].agentId}\` via \`hub\` send to coordinate while it runs; use \`hub\` only to inspect (\`jobs\`), wait, or cancel a stuck task.`
|
|
901
902
|
: `Use \`hub\` to inspect (\`jobs\`), wait, or cancel a stuck task.`
|
|
902
903
|
: ircEnabled
|
|
903
904
|
? `DM these ids via \`hub\` send to coordinate while they run; use \`hub\` only to inspect (\`jobs\`), wait, or cancel a stuck task.`
|
|
904
|
-
: `Use \`hub\` to inspect (\`jobs\`), wait, or cancel a stuck task by id
|
|
905
|
+
: `Use \`hub\` to inspect (\`jobs\`), wait, or cancel a stuck task by id.`,
|
|
906
|
+
taskAsyncContractTemplate.trim(),
|
|
907
|
+
].join("\n");
|
|
905
908
|
|
|
906
909
|
if (syncSpawns.length === 0) {
|
|
907
910
|
if (spawns.length === 1) {
|
|
@@ -915,7 +918,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
915
918
|
content: [
|
|
916
919
|
{
|
|
917
920
|
type: "text",
|
|
918
|
-
text: `Spawned agent \`${agentId}\` (job \`${jobId}\`).
|
|
921
|
+
text: `Spawned agent \`${agentId}\` (job \`${jobId}\`). Its result auto-delivers on yield unless a settled \`hub jobs\`/\`wait\` snapshot consumes it first. ${coordinationHint}`,
|
|
919
922
|
},
|
|
920
923
|
],
|
|
921
924
|
details: buildAsyncDetails(),
|
|
@@ -932,7 +935,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
932
935
|
content: [
|
|
933
936
|
{
|
|
934
937
|
type: "text",
|
|
935
|
-
text: `Spawned ${started.length} background agents using ${agentLabel}.${scheduleFailureSummary} Each result
|
|
938
|
+
text: `Spawned ${started.length} background agents using ${agentLabel}.${scheduleFailureSummary} Each result auto-delivers on yield unless a settled \`hub jobs\`/\`wait\` snapshot consumes it first.\n${startedListing}\n${coordinationHint}`,
|
|
936
939
|
},
|
|
937
940
|
],
|
|
938
941
|
details: buildAsyncDetails(),
|
|
@@ -997,7 +1000,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
997
1000
|
|
|
998
1001
|
const spawnedSummary =
|
|
999
1002
|
started.length > 0
|
|
1000
|
-
? `Spawned ${started.length} background agent${started.length === 1 ? "" : "s"}.${scheduleFailureSummary} Each result
|
|
1003
|
+
? `Spawned ${started.length} background agent${started.length === 1 ? "" : "s"}.${scheduleFailureSummary} Each result auto-delivers on yield unless a settled \`hub jobs\`/\`wait\` snapshot consumes it first.\n${started.map(({ agentId, jobId }) => `- \`${agentId}\` (job \`${jobId}\`)`).join("\n")}\n${coordinationHint}`
|
|
1001
1004
|
: scheduleFailureSummary.trim();
|
|
1002
1005
|
const text = [merged.contentParts.join("\n\n"), spawnedSummary]
|
|
1003
1006
|
.filter(section => section.trim().length > 0)
|
|
@@ -1078,12 +1081,41 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1078
1081
|
try {
|
|
1079
1082
|
markRunning();
|
|
1080
1083
|
progress.status = "running";
|
|
1081
|
-
await reportProgress(
|
|
1084
|
+
await reportProgress(
|
|
1085
|
+
`Running background task ${agentId}...`,
|
|
1086
|
+
buildDetails() as unknown as Record<string, unknown>,
|
|
1087
|
+
);
|
|
1088
|
+
const forwardSyncProgress: AgentToolUpdateCallback<TaskToolDetails> = async update => {
|
|
1089
|
+
const nextProgress = update.details?.progress?.[0];
|
|
1090
|
+
if (nextProgress) {
|
|
1091
|
+
// The job body owns status and identity (id/index/agent);
|
|
1092
|
+
// copy only the live metrics the subagent streams so the
|
|
1093
|
+
// polling row reflects the resolved model, reasoning level,
|
|
1094
|
+
// and running counters without reverting the "running"
|
|
1095
|
+
// status back to the subagent's initial "pending" snapshot.
|
|
1096
|
+
progress.resolvedModel = nextProgress.resolvedModel;
|
|
1097
|
+
progress.tokens = nextProgress.tokens;
|
|
1098
|
+
progress.requests = nextProgress.requests;
|
|
1099
|
+
progress.contextTokens = nextProgress.contextTokens;
|
|
1100
|
+
progress.contextWindow = nextProgress.contextWindow;
|
|
1101
|
+
progress.cost = nextProgress.cost;
|
|
1102
|
+
progress.toolCount = nextProgress.toolCount;
|
|
1103
|
+
progress.currentTool = nextProgress.currentTool;
|
|
1104
|
+
progress.lastIntent = nextProgress.lastIntent;
|
|
1105
|
+
progress.recentTools = nextProgress.recentTools.slice();
|
|
1106
|
+
progress.recentOutput = nextProgress.recentOutput.slice();
|
|
1107
|
+
progress.retryState = nextProgress.retryState;
|
|
1108
|
+
progress.retryFailure = nextProgress.retryFailure;
|
|
1109
|
+
}
|
|
1110
|
+
const updateText =
|
|
1111
|
+
update.content.find(part => part.type === "text")?.text ?? `Running background task ${agentId}...`;
|
|
1112
|
+
await reportProgress(updateText, buildDetails() as unknown as Record<string, unknown>);
|
|
1113
|
+
};
|
|
1082
1114
|
const result = await this.#executeSync(
|
|
1083
1115
|
toolCallId,
|
|
1084
1116
|
spawnParams,
|
|
1085
1117
|
runSignal,
|
|
1086
|
-
|
|
1118
|
+
forwardSyncProgress,
|
|
1087
1119
|
agentId,
|
|
1088
1120
|
progress.index,
|
|
1089
1121
|
true,
|
|
@@ -1104,11 +1136,16 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1104
1136
|
progress.extractedToolData = singleResult?.extractedToolData;
|
|
1105
1137
|
progress.retryFailure = singleResult?.retryFailure;
|
|
1106
1138
|
progress.retryState = undefined;
|
|
1139
|
+
if (singleResult?.resolvedModel) {
|
|
1140
|
+
progress.resolvedModel = singleResult.resolvedModel;
|
|
1141
|
+
} else {
|
|
1142
|
+
delete progress.resolvedModel;
|
|
1143
|
+
}
|
|
1107
1144
|
onSettled?.(resultFailed);
|
|
1108
1145
|
const statusText = resultFailed
|
|
1109
1146
|
? `Background task ${agentId} failed.`
|
|
1110
1147
|
: `Background task ${agentId} complete.`;
|
|
1111
|
-
await reportProgress(statusText);
|
|
1148
|
+
await reportProgress(statusText, buildDetails() as unknown as Record<string, unknown>);
|
|
1112
1149
|
const deliveryText = `${finalText}${buildFollowUpHint(singleResult?.aborted === true)}`;
|
|
1113
1150
|
if (resultFailed) {
|
|
1114
1151
|
// Mark the job itself failed; the failed agent stays interrogable.
|
|
@@ -1123,7 +1160,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1123
1160
|
progress.durationMs = Math.max(0, Date.now() - startedAt);
|
|
1124
1161
|
onSettled?.(true);
|
|
1125
1162
|
const statusText = `Background task ${agentId} failed.`;
|
|
1126
|
-
await reportProgress(statusText);
|
|
1163
|
+
await reportProgress(statusText, buildDetails() as unknown as Record<string, unknown>);
|
|
1127
1164
|
const message = error instanceof Error ? error.message : String(error);
|
|
1128
1165
|
const hint = AgentRegistry.global().get(agentId) ? buildFollowUpHint(false) : "";
|
|
1129
1166
|
throw new TaskJobError(`${message}${hint}`);
|
package/src/task/types.ts
CHANGED
|
@@ -106,11 +106,14 @@ export interface SubagentLifecyclePayload {
|
|
|
106
106
|
/** Display cap for a normalized one-line label (roster line, registry `displayName`, prompt field). */
|
|
107
107
|
export const LABEL_MAX = 80;
|
|
108
108
|
|
|
109
|
+
// Keep this explicit: ArkType serializes `unknown` as a boolean subschema, which llama.cpp grammars reject.
|
|
110
|
+
const outputSchemaInputSchema = type("object | boolean | string | null");
|
|
111
|
+
|
|
109
112
|
export const taskItemSchema = type({
|
|
110
113
|
"name?": "string",
|
|
111
114
|
agent: "string = 'task'",
|
|
112
115
|
task: "string",
|
|
113
|
-
"outputSchema?":
|
|
116
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
114
117
|
"schemaMode?": '"permissive" | "strict"',
|
|
115
118
|
"+": "delete",
|
|
116
119
|
});
|
|
@@ -118,7 +121,7 @@ const taskItemSchemaIsolated = type({
|
|
|
118
121
|
"name?": "string",
|
|
119
122
|
agent: "string = 'task'",
|
|
120
123
|
task: "string",
|
|
121
|
-
"outputSchema?":
|
|
124
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
122
125
|
"schemaMode?": '"permissive" | "strict"',
|
|
123
126
|
"isolated?": "boolean",
|
|
124
127
|
"+": "delete",
|
|
@@ -144,7 +147,7 @@ export const taskSchema = type({
|
|
|
144
147
|
"name?": "string",
|
|
145
148
|
agent: "string = 'task'",
|
|
146
149
|
task: "string",
|
|
147
|
-
"outputSchema?":
|
|
150
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
148
151
|
"schemaMode?": '"permissive" | "strict"',
|
|
149
152
|
"isolated?": "boolean",
|
|
150
153
|
"+": "delete",
|
|
@@ -153,7 +156,7 @@ const taskSchemaNoIsolation = type({
|
|
|
153
156
|
"name?": "string",
|
|
154
157
|
agent: "string = 'task'",
|
|
155
158
|
task: "string",
|
|
156
|
-
"outputSchema?":
|
|
159
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
157
160
|
"schemaMode?": '"permissive" | "strict"',
|
|
158
161
|
"+": "delete",
|
|
159
162
|
});
|
|
@@ -197,7 +200,7 @@ function createTaskSchema(options: {
|
|
|
197
200
|
"name?": "string",
|
|
198
201
|
agent,
|
|
199
202
|
task: "string",
|
|
200
|
-
"outputSchema?":
|
|
203
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
201
204
|
"schemaMode?": '"permissive" | "strict"',
|
|
202
205
|
"isolated?": "boolean",
|
|
203
206
|
"+": "delete",
|
|
@@ -212,7 +215,7 @@ function createTaskSchema(options: {
|
|
|
212
215
|
"name?": "string",
|
|
213
216
|
agent,
|
|
214
217
|
task: "string",
|
|
215
|
-
"outputSchema?":
|
|
218
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
216
219
|
"schemaMode?": '"permissive" | "strict"',
|
|
217
220
|
"+": "delete",
|
|
218
221
|
});
|
|
@@ -227,7 +230,7 @@ function createTaskSchema(options: {
|
|
|
227
230
|
"name?": "string",
|
|
228
231
|
agent,
|
|
229
232
|
task: "string",
|
|
230
|
-
"outputSchema?":
|
|
233
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
231
234
|
"schemaMode?": '"permissive" | "strict"',
|
|
232
235
|
"isolated?": "boolean",
|
|
233
236
|
"+": "delete",
|
|
@@ -237,7 +240,7 @@ function createTaskSchema(options: {
|
|
|
237
240
|
"name?": "string",
|
|
238
241
|
agent,
|
|
239
242
|
task: "string",
|
|
240
|
-
"outputSchema?":
|
|
243
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
241
244
|
"schemaMode?": '"permissive" | "strict"',
|
|
242
245
|
"+": "delete",
|
|
243
246
|
});
|
package/src/task/worktree.ts
CHANGED
|
@@ -414,6 +414,8 @@ export async function ensureIsolation(
|
|
|
414
414
|
preferred?: IsoBackendKind,
|
|
415
415
|
): Promise<IsolationHandle> {
|
|
416
416
|
const repoRoot = await getRepoRoot(baseCwd);
|
|
417
|
+
const repository = await git.repo.resolve(repoRoot);
|
|
418
|
+
const sourceCommonDir = repository?.commonDir ?? path.join(repoRoot, ".git");
|
|
417
419
|
const baseDir = getWorktreeDir(getTaskIsolationSegment(repoRoot, id));
|
|
418
420
|
const mergedDir = path.join(baseDir, TASK_ISOLATION_MOUNT_DIR);
|
|
419
421
|
const resolution = natives.isoResolve(preferred ?? null);
|
|
@@ -424,6 +426,14 @@ export async function ensureIsolation(
|
|
|
424
426
|
await fs.rm(baseDir, { recursive: true, force: true });
|
|
425
427
|
try {
|
|
426
428
|
await natives.isoStart(candidate, repoRoot, mergedDir);
|
|
429
|
+
// Sever the isolation's git metadata from the source checkout. Copy
|
|
430
|
+
// backends duplicate `repoRoot`'s `.git` verbatim — a linked-worktree
|
|
431
|
+
// pointer file (or the rcopy `git worktree add` registration) leaves
|
|
432
|
+
// the isolation sharing the source's HEAD/index/ref namespace, so a
|
|
433
|
+
// task's git operations would mutate the parent checkout and stack
|
|
434
|
+
// parallel task branches. Detaching gives each isolation a private,
|
|
435
|
+
// frozen repo that still borrows the source object DB via alternates.
|
|
436
|
+
await git.detachGitDir(mergedDir, sourceCommonDir);
|
|
427
437
|
return {
|
|
428
438
|
mergedDir,
|
|
429
439
|
backend: candidate,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ElementHandle, JSHandle, Page } from "puppeteer-core";
|
|
2
|
+
import { ToolError } from "../../tool-errors";
|
|
2
3
|
import ariaBundle from "./aria-snapshot.bundle.txt" with { type: "text" };
|
|
3
4
|
// `aria-snapshot.bundle.txt` is a generated, committed artifact: Playwright's
|
|
4
5
|
// injected ARIA-snapshot sources (pinned, Apache-2.0) bundled to a CJS module.
|
|
@@ -69,15 +70,41 @@ export async function resolveAriaRefHandle(page: Page, ref: string): Promise<Ele
|
|
|
69
70
|
const ARIA_REF_PREFIXES = ["aria-ref=", "aria-ref/", "ariaref/"];
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
* Guard the selector funnels: `tab.click`/`type`/`fill`/`waitFor*`/`scrollIntoView`
|
|
74
|
+
* take string selectors only, but user `run` code routinely passes the ElementHandle
|
|
75
|
+
* from `tab.id(n)`/`tab.ref(...)` (or an un-awaited Promise of one) straight in.
|
|
76
|
+
* Without this the value reaches `.trim()`/`.startsWith()` and throws the opaque,
|
|
77
|
+
* minified `A.trim is not a function` instead of a recovery-naming ToolError.
|
|
78
|
+
*/
|
|
79
|
+
export function assertSelectorString(selector: unknown): asserts selector is string {
|
|
80
|
+
if (typeof selector === "string") return;
|
|
81
|
+
let kind: string;
|
|
82
|
+
if (selector !== null && typeof selector === "object") {
|
|
83
|
+
kind =
|
|
84
|
+
"then" in selector && typeof selector.then === "function" ? "a Promise (missing await?)" : "an ElementHandle";
|
|
85
|
+
} else {
|
|
86
|
+
kind = `a ${typeof selector}`;
|
|
87
|
+
}
|
|
88
|
+
throw new ToolError(
|
|
89
|
+
`Browser selector must be a string; got ${kind}. ` +
|
|
90
|
+
"tab.click/type/fill/waitFor take string selectors only — " +
|
|
91
|
+
'call the handle method directly (e.g. (await tab.id(n)).click()) or pass a string like "aria-ref=eN".',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Recognize a snapshot-ref selector and return the bare ref id, else null.
|
|
97
|
+
* Accepts `aria-ref=e5` (Playwright-MCP style), `aria-ref/e5`, `ariaref/e5`,
|
|
98
|
+
* and bare `e5`/`@e5`: agents copy ids straight out of the snapshot YAML
|
|
99
|
+
* (`[ref=e5]`), so `tab.click("e5")` must act on the ref instead of falling
|
|
100
|
+
* through to a CSS tag selector that can never match. Bare ids are safe to
|
|
101
|
+
* claim here — an eN tag name is not real HTML, and the tab-worker backend's
|
|
102
|
+
* observe ids are numeric (`tab.id(7)`), so refs are its only eN namespace.
|
|
103
|
+
* (The cmux backend parses selectors itself and routes bare `eN` to its own
|
|
104
|
+
* observe ids; either way `eN` means "the id from the last page dump".)
|
|
79
105
|
*/
|
|
80
106
|
export function parseAriaRefSelector(selector: string): string | null {
|
|
107
|
+
assertSelectorString(selector);
|
|
81
108
|
const trimmed = selector.trim();
|
|
82
109
|
for (const prefix of ARIA_REF_PREFIXES) {
|
|
83
110
|
if (trimmed.startsWith(prefix)) {
|
|
@@ -85,7 +112,8 @@ export function parseAriaRefSelector(selector: string): string | null {
|
|
|
85
112
|
return /^e\d+$/.test(id) ? id : null;
|
|
86
113
|
}
|
|
87
114
|
}
|
|
88
|
-
|
|
115
|
+
const bare = /^@?(e\d+)$/.exec(trimmed);
|
|
116
|
+
return bare ? bare[1]! : null;
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
/**
|