pi-agent-browser-native 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +8 -1
- package/docs/ARCHITECTURE.md +10 -5
- package/docs/RELEASE.md +11 -2
- package/docs/REQUIREMENTS.md +7 -1
- package/docs/TOOL_CONTRACT.md +9 -4
- package/extensions/agent-browser/index.ts +596 -61
- package/extensions/agent-browser/lib/process.ts +30 -2
- package/extensions/agent-browser/lib/results/presentation.ts +92 -6
- package/extensions/agent-browser/lib/results/snapshot.ts +15 -6
- package/extensions/agent-browser/lib/runtime.ts +171 -7
- package/extensions/agent-browser/lib/temp.ts +94 -12
- package/package.json +1 -1
|
@@ -12,13 +12,21 @@ import { isToolCallEventType, type ExtensionAPI } from "@mariozechner/pi-coding-
|
|
|
12
12
|
import { Type } from "@sinclair/typebox";
|
|
13
13
|
|
|
14
14
|
import { runAgentBrowserProcess } from "./lib/process.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
buildToolPresentation,
|
|
17
|
+
getAgentBrowserErrorText,
|
|
18
|
+
parseAgentBrowserEnvelope,
|
|
19
|
+
type AgentBrowserBatchResult,
|
|
20
|
+
type AgentBrowserEnvelope,
|
|
21
|
+
} from "./lib/results.js";
|
|
16
22
|
import {
|
|
17
23
|
buildExecutionPlan,
|
|
18
24
|
buildPromptPolicy,
|
|
25
|
+
chooseOpenResultTabCorrection,
|
|
19
26
|
createEphemeralSessionSeed,
|
|
20
27
|
createFreshSessionName,
|
|
21
28
|
createImplicitSessionName,
|
|
29
|
+
extractCommandTokens,
|
|
22
30
|
getImplicitSessionCloseTimeoutMs,
|
|
23
31
|
getImplicitSessionIdleTimeoutMs,
|
|
24
32
|
getLatestUserPrompt,
|
|
@@ -30,8 +38,10 @@ import {
|
|
|
30
38
|
resolveManagedSessionState,
|
|
31
39
|
shouldAppendBrowserSystemPrompt,
|
|
32
40
|
validateToolArgs,
|
|
41
|
+
type CompatibilityWorkaround,
|
|
42
|
+
type OpenResultTabCorrection,
|
|
33
43
|
} from "./lib/runtime.js";
|
|
34
|
-
import { cleanupSecureTempArtifacts } from "./lib/temp.js";
|
|
44
|
+
import { cleanupSecureTempArtifacts, type PersistentSessionArtifactStore } from "./lib/temp.js";
|
|
35
45
|
|
|
36
46
|
const DEFAULT_SESSION_MODE = "auto" as const;
|
|
37
47
|
|
|
@@ -96,15 +106,184 @@ function buildInvocationPreview(effectiveArgs: string[]): string {
|
|
|
96
106
|
return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
const DIRECT_AGENT_BROWSER_EXECUTABLE_PATTERN = /^(?:[.~]|\.\.?|\/)?(?:[^\s;&|]+\/)?agent-browser$/;
|
|
110
|
+
const HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN = /^\s*(?:command\s+-v|which|type\s+-P)\s+agent-browser\s*$/;
|
|
111
|
+
|
|
112
|
+
type ShellQuoteState = 'double' | 'single' | undefined;
|
|
113
|
+
|
|
114
|
+
function isShellAssignmentToken(token: string): boolean {
|
|
115
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stripOuterQuotes(token: string): string {
|
|
119
|
+
if (token.length >= 2 && ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith("'") && token.endsWith("'")))) {
|
|
120
|
+
return token.slice(1, -1);
|
|
121
|
+
}
|
|
122
|
+
return token;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function segmentLaunchesAgentBrowser(tokens: string[]): boolean {
|
|
126
|
+
let index = 0;
|
|
127
|
+
while (index < tokens.length && isShellAssignmentToken(tokens[index])) {
|
|
128
|
+
index += 1;
|
|
129
|
+
}
|
|
130
|
+
if (index >= tokens.length) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let executableToken = tokens[index];
|
|
135
|
+
if (executableToken === 'env') {
|
|
136
|
+
index += 1;
|
|
137
|
+
while (index < tokens.length && isShellAssignmentToken(tokens[index])) {
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
140
|
+
executableToken = tokens[index] ?? '';
|
|
141
|
+
}
|
|
142
|
+
if (executableToken === 'npx' || executableToken === 'bunx') {
|
|
143
|
+
index += 1;
|
|
144
|
+
while (index < tokens.length && tokens[index].startsWith('-')) {
|
|
145
|
+
index += 1;
|
|
146
|
+
}
|
|
147
|
+
executableToken = tokens[index] ?? '';
|
|
148
|
+
}
|
|
149
|
+
if (executableToken === 'pnpm' || executableToken === 'yarn') {
|
|
150
|
+
index += 1;
|
|
151
|
+
if (tokens[index] !== 'dlx') {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
index += 1;
|
|
155
|
+
while (index < tokens.length && tokens[index].startsWith('-')) {
|
|
156
|
+
index += 1;
|
|
157
|
+
}
|
|
158
|
+
executableToken = tokens[index] ?? '';
|
|
159
|
+
}
|
|
160
|
+
return DIRECT_AGENT_BROWSER_EXECUTABLE_PATTERN.test(executableToken);
|
|
161
|
+
}
|
|
105
162
|
|
|
163
|
+
// Best-effort detection for common direct launches only. This is an ergonomics guard,
|
|
164
|
+
// not a general-purpose bash parser or security boundary.
|
|
106
165
|
function looksLikeDirectAgentBrowserBash(command: string): boolean {
|
|
107
|
-
|
|
166
|
+
let currentToken = '';
|
|
167
|
+
let quoteState: ShellQuoteState;
|
|
168
|
+
let awaitingHeredocDelimiter: { stripTabs: boolean } | undefined;
|
|
169
|
+
let pendingHeredoc: { delimiter: string; stripTabs: boolean } | undefined;
|
|
170
|
+
let pendingHeredocLine = '';
|
|
171
|
+
let segmentTokens: string[] = [];
|
|
172
|
+
|
|
173
|
+
const acceptToken = (token: string) => {
|
|
174
|
+
if (token.length === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (awaitingHeredocDelimiter) {
|
|
178
|
+
pendingHeredoc = {
|
|
179
|
+
delimiter: stripOuterQuotes(token),
|
|
180
|
+
stripTabs: awaitingHeredocDelimiter.stripTabs,
|
|
181
|
+
};
|
|
182
|
+
awaitingHeredocDelimiter = undefined;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
segmentTokens.push(token);
|
|
186
|
+
};
|
|
187
|
+
const flushToken = () => {
|
|
188
|
+
acceptToken(currentToken);
|
|
189
|
+
currentToken = '';
|
|
190
|
+
};
|
|
191
|
+
const flushSegment = () => {
|
|
192
|
+
const launchesAgentBrowser = segmentLaunchesAgentBrowser(segmentTokens);
|
|
193
|
+
segmentTokens = [];
|
|
194
|
+
return launchesAgentBrowser;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
198
|
+
const char = command[index];
|
|
199
|
+
if (pendingHeredoc) {
|
|
200
|
+
if (char === '\n') {
|
|
201
|
+
const candidate = pendingHeredoc.stripTabs ? pendingHeredocLine.replace(/^\t+/, '') : pendingHeredocLine;
|
|
202
|
+
if (candidate === pendingHeredoc.delimiter) {
|
|
203
|
+
pendingHeredoc = undefined;
|
|
204
|
+
}
|
|
205
|
+
pendingHeredocLine = '';
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
pendingHeredocLine += char;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (quoteState === 'single') {
|
|
213
|
+
currentToken += char;
|
|
214
|
+
if (char === "'") {
|
|
215
|
+
quoteState = undefined;
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (quoteState === 'double') {
|
|
220
|
+
currentToken += char;
|
|
221
|
+
if (char === '\\' && index + 1 < command.length) {
|
|
222
|
+
currentToken += command[index + 1];
|
|
223
|
+
index += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === '"') {
|
|
227
|
+
quoteState = undefined;
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (char === "'" || char === '"') {
|
|
232
|
+
currentToken += char;
|
|
233
|
+
quoteState = char === "'" ? 'single' : 'double';
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (char === '\\' && index + 1 < command.length) {
|
|
237
|
+
currentToken += char;
|
|
238
|
+
currentToken += command[index + 1];
|
|
239
|
+
index += 1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (char === '\n') {
|
|
243
|
+
flushToken();
|
|
244
|
+
if (flushSegment()) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (/\s/.test(char)) {
|
|
250
|
+
flushToken();
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const threeCharOperator = command.slice(index, index + 3);
|
|
254
|
+
if (threeCharOperator === '<<-') {
|
|
255
|
+
flushToken();
|
|
256
|
+
awaitingHeredocDelimiter = { stripTabs: true };
|
|
257
|
+
index += 2;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const twoCharOperator = command.slice(index, index + 2);
|
|
261
|
+
if (twoCharOperator === '<<') {
|
|
262
|
+
flushToken();
|
|
263
|
+
awaitingHeredocDelimiter = { stripTabs: false };
|
|
264
|
+
index += 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (twoCharOperator === '&&' || twoCharOperator === '||') {
|
|
268
|
+
flushToken();
|
|
269
|
+
if (flushSegment()) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
index += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (char === '|' || char === ';' || char === '&') {
|
|
276
|
+
flushToken();
|
|
277
|
+
if (flushSegment()) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
currentToken += char;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
flushToken();
|
|
286
|
+
return flushSegment();
|
|
108
287
|
}
|
|
109
288
|
|
|
110
289
|
function isHarmlessAgentBrowserInspectionCommand(command: string): boolean {
|
|
@@ -142,41 +321,232 @@ function extractStringResultField(data: unknown, fieldName: "title" | "url"): st
|
|
|
142
321
|
return text.length > 0 ? text : undefined;
|
|
143
322
|
}
|
|
144
323
|
|
|
145
|
-
|
|
324
|
+
const SESSION_TAB_PINNING_EXCLUDED_COMMANDS = new Set(["batch", "close", "goto", "navigate", "open", "session", "tab"]);
|
|
325
|
+
|
|
326
|
+
interface SessionTabTarget {
|
|
327
|
+
title?: string;
|
|
328
|
+
url: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizeComparableUrl(url: string | undefined): string | undefined {
|
|
332
|
+
const normalizedUrl = url?.trim();
|
|
333
|
+
if (!normalizedUrl) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const parsedUrl = new URL(normalizedUrl);
|
|
338
|
+
parsedUrl.hash = "";
|
|
339
|
+
return parsedUrl.toString();
|
|
340
|
+
} catch {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function normalizeSessionTabTarget(target: { title?: string; url?: string } | undefined): SessionTabTarget | undefined {
|
|
346
|
+
if (!target) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
const url = normalizeComparableUrl(target.url);
|
|
350
|
+
if (!url) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
const title = target.title?.trim();
|
|
354
|
+
return { title: title && title.length > 0 ? title : undefined, url };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function extractSessionTabTargetFromData(data: unknown): SessionTabTarget | undefined {
|
|
358
|
+
const directTarget = normalizeSessionTabTarget({
|
|
359
|
+
title: extractStringResultField(data, "title"),
|
|
360
|
+
url: extractStringResultField(data, "url"),
|
|
361
|
+
});
|
|
362
|
+
if (directTarget) {
|
|
363
|
+
return directTarget;
|
|
364
|
+
}
|
|
365
|
+
if (isRecord(data) && typeof data.origin === "string") {
|
|
366
|
+
return normalizeSessionTabTarget({ url: data.origin });
|
|
367
|
+
}
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, SessionTabTarget> {
|
|
372
|
+
const restoredTargets = new Map<string, SessionTabTarget>();
|
|
373
|
+
for (const entry of branch) {
|
|
374
|
+
if (!isRecord(entry) || entry.type !== "message") {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
378
|
+
if (!message || message.toolName !== "agent_browser") {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const details = isRecord(message.details) ? message.details : undefined;
|
|
382
|
+
if (!details) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
386
|
+
if (!sessionName) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const command = typeof details.command === "string" ? details.command : undefined;
|
|
390
|
+
if (command === "close" && message.isError !== true) {
|
|
391
|
+
restoredTargets.delete(sessionName);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const sessionTabTarget = isRecord(details.sessionTabTarget)
|
|
395
|
+
? normalizeSessionTabTarget({
|
|
396
|
+
title: typeof details.sessionTabTarget.title === "string" ? details.sessionTabTarget.title : undefined,
|
|
397
|
+
url: typeof details.sessionTabTarget.url === "string" ? details.sessionTabTarget.url : undefined,
|
|
398
|
+
})
|
|
399
|
+
: undefined;
|
|
400
|
+
if (sessionTabTarget) {
|
|
401
|
+
restoredTargets.set(sessionName, sessionTabTarget);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return restoredTargets;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function shouldPinSessionTabForCommand(options: { command?: string; sessionName?: string; stdin?: string }): boolean {
|
|
408
|
+
return (
|
|
409
|
+
options.sessionName !== undefined &&
|
|
410
|
+
options.stdin === undefined &&
|
|
411
|
+
options.command !== undefined &&
|
|
412
|
+
!SESSION_TAB_PINNING_EXCLUDED_COMMANDS.has(options.command)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function selectSessionTargetTab(options: {
|
|
417
|
+
tabs: Array<{ active?: boolean; index?: number; title?: string; url?: string }>;
|
|
418
|
+
target: SessionTabTarget;
|
|
419
|
+
}): OpenResultTabCorrection | undefined {
|
|
420
|
+
const matchingTabs = options.tabs.filter((tab) => normalizeComparableUrl(tab.url) === options.target.url);
|
|
421
|
+
if (matchingTabs.length === 0) {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
const titledMatch =
|
|
425
|
+
typeof options.target.title === "string"
|
|
426
|
+
? matchingTabs.find((tab) => tab.title?.trim() === options.target.title)
|
|
427
|
+
: undefined;
|
|
428
|
+
const selectedTab = titledMatch ?? matchingTabs[0];
|
|
429
|
+
return typeof selectedTab.index === "number"
|
|
430
|
+
? {
|
|
431
|
+
selectedIndex: selectedTab.index,
|
|
432
|
+
targetTitle: options.target.title,
|
|
433
|
+
targetUrl: options.target.url,
|
|
434
|
+
}
|
|
435
|
+
: undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function deriveSessionTabTarget(options: {
|
|
439
|
+
command?: string;
|
|
440
|
+
data: unknown;
|
|
441
|
+
navigationSummary?: NavigationSummary;
|
|
442
|
+
previousTarget?: SessionTabTarget;
|
|
443
|
+
}): SessionTabTarget | undefined {
|
|
444
|
+
if (options.command === "close") {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
return (
|
|
448
|
+
normalizeSessionTabTarget(options.navigationSummary) ??
|
|
449
|
+
extractSessionTabTargetFromData(options.data) ??
|
|
450
|
+
options.previousTarget
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function unwrapPinnedSessionBatchEnvelope(options: {
|
|
455
|
+
envelope?: AgentBrowserEnvelope;
|
|
456
|
+
includeNavigationSummary: boolean;
|
|
457
|
+
}): { envelope?: AgentBrowserEnvelope; navigationSummary?: NavigationSummary; parseError?: string } {
|
|
458
|
+
if (!options.envelope) {
|
|
459
|
+
return {};
|
|
460
|
+
}
|
|
461
|
+
if (!Array.isArray(options.envelope.data)) {
|
|
462
|
+
return {
|
|
463
|
+
parseError: "agent-browser returned an unexpected response while applying the wrapper's tab-pinning batch.",
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const steps = options.envelope.data.filter(isRecord) as AgentBrowserBatchResult[];
|
|
468
|
+
const tabSelectionStep = steps[0];
|
|
469
|
+
const commandStep = steps[1];
|
|
470
|
+
if (!commandStep) {
|
|
471
|
+
return {
|
|
472
|
+
envelope: {
|
|
473
|
+
success: false,
|
|
474
|
+
error: "agent-browser did not return the corrected command result.",
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (tabSelectionStep?.success === false) {
|
|
479
|
+
return {
|
|
480
|
+
envelope: {
|
|
481
|
+
success: false,
|
|
482
|
+
error: tabSelectionStep.error ?? "agent-browser could not re-select the intended tab before running the command.",
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const titleStep = options.includeNavigationSummary ? steps[2] : undefined;
|
|
488
|
+
const urlStep = options.includeNavigationSummary ? steps[3] : undefined;
|
|
489
|
+
const navigationSummary = normalizeSessionTabTarget({
|
|
490
|
+
title: extractStringResultField(titleStep?.result, "title"),
|
|
491
|
+
url: extractStringResultField(urlStep?.result, "url"),
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
envelope: {
|
|
495
|
+
success: commandStep.success !== false,
|
|
496
|
+
data: commandStep.result,
|
|
497
|
+
error: commandStep.success === false ? commandStep.error : undefined,
|
|
498
|
+
},
|
|
499
|
+
navigationSummary,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function runSessionCommandData(options: {
|
|
504
|
+
args: string[];
|
|
146
505
|
cwd: string;
|
|
147
506
|
sessionName?: string;
|
|
148
507
|
signal?: AbortSignal;
|
|
149
|
-
}): Promise<
|
|
150
|
-
const { cwd, sessionName, signal } = options;
|
|
508
|
+
}): Promise<unknown | undefined> {
|
|
509
|
+
const { args, cwd, sessionName, signal } = options;
|
|
151
510
|
if (!sessionName) return undefined;
|
|
152
511
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
512
|
+
const processResult = await runAgentBrowserProcess({
|
|
513
|
+
args: ["--json", "--session", sessionName, ...args],
|
|
514
|
+
cwd,
|
|
515
|
+
signal,
|
|
516
|
+
});
|
|
517
|
+
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
const parsed = await parseAgentBrowserEnvelope({
|
|
521
|
+
stdout: processResult.stdout,
|
|
522
|
+
stdoutPath: processResult.stdoutSpillPath,
|
|
523
|
+
});
|
|
524
|
+
try {
|
|
525
|
+
if (parsed.parseError || parsed.envelope?.success === false) {
|
|
160
526
|
return undefined;
|
|
161
527
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
if (parsed.parseError || parsed.envelope?.success === false) {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
return extractStringResultField(parsed.envelope?.data, fieldName);
|
|
171
|
-
} finally {
|
|
172
|
-
if (processResult.stdoutSpillPath) {
|
|
173
|
-
await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
174
|
-
}
|
|
528
|
+
return parsed.envelope?.data;
|
|
529
|
+
} finally {
|
|
530
|
+
if (processResult.stdoutSpillPath) {
|
|
531
|
+
await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
175
532
|
}
|
|
176
|
-
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
177
535
|
|
|
178
|
-
|
|
179
|
-
|
|
536
|
+
async function collectNavigationSummary(options: {
|
|
537
|
+
cwd: string;
|
|
538
|
+
sessionName?: string;
|
|
539
|
+
signal?: AbortSignal;
|
|
540
|
+
}): Promise<NavigationSummary | undefined> {
|
|
541
|
+
const { cwd, sessionName, signal } = options;
|
|
542
|
+
const title = extractStringResultField(
|
|
543
|
+
await runSessionCommandData({ args: ["get", "title"], cwd, sessionName, signal }),
|
|
544
|
+
"title",
|
|
545
|
+
);
|
|
546
|
+
const url = extractStringResultField(
|
|
547
|
+
await runSessionCommandData({ args: ["get", "url"], cwd, sessionName, signal }),
|
|
548
|
+
"url",
|
|
549
|
+
);
|
|
180
550
|
if (!title && !url) return undefined;
|
|
181
551
|
return { title, url };
|
|
182
552
|
}
|
|
@@ -188,6 +558,63 @@ function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: Naviga
|
|
|
188
558
|
return { navigationSummary, result: data };
|
|
189
559
|
}
|
|
190
560
|
|
|
561
|
+
async function collectOpenResultTabCorrection(options: {
|
|
562
|
+
cwd: string;
|
|
563
|
+
sessionName?: string;
|
|
564
|
+
signal?: AbortSignal;
|
|
565
|
+
targetTitle?: string;
|
|
566
|
+
targetUrl?: string;
|
|
567
|
+
}): Promise<OpenResultTabCorrection | undefined> {
|
|
568
|
+
const { cwd, sessionName, signal, targetTitle, targetUrl } = options;
|
|
569
|
+
const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, sessionName, signal });
|
|
570
|
+
if (!isRecord(tabData) || !Array.isArray(tabData.tabs)) {
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
const tabs = tabData.tabs.filter(isRecord).map((tab) => ({
|
|
574
|
+
active: tab.active === true,
|
|
575
|
+
index: typeof tab.index === "number" ? tab.index : undefined,
|
|
576
|
+
title: typeof tab.title === "string" ? tab.title : undefined,
|
|
577
|
+
url: typeof tab.url === "string" ? tab.url : undefined,
|
|
578
|
+
}));
|
|
579
|
+
return chooseOpenResultTabCorrection({ tabs, targetTitle, targetUrl });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function collectSessionTabSelection(options: {
|
|
583
|
+
cwd: string;
|
|
584
|
+
sessionName?: string;
|
|
585
|
+
signal?: AbortSignal;
|
|
586
|
+
target: SessionTabTarget;
|
|
587
|
+
}): Promise<OpenResultTabCorrection | undefined> {
|
|
588
|
+
const { cwd, sessionName, signal, target } = options;
|
|
589
|
+
const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, sessionName, signal });
|
|
590
|
+
if (!isRecord(tabData) || !Array.isArray(tabData.tabs)) {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
const tabs = tabData.tabs.filter(isRecord).map((tab) => ({
|
|
594
|
+
active: tab.active === true,
|
|
595
|
+
index: typeof tab.index === "number" ? tab.index : undefined,
|
|
596
|
+
title: typeof tab.title === "string" ? tab.title : undefined,
|
|
597
|
+
url: typeof tab.url === "string" ? tab.url : undefined,
|
|
598
|
+
}));
|
|
599
|
+
return selectSessionTargetTab({ tabs, target });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function applyOpenResultTabCorrection(options: {
|
|
603
|
+
correction: OpenResultTabCorrection;
|
|
604
|
+
cwd: string;
|
|
605
|
+
sessionName?: string;
|
|
606
|
+
signal?: AbortSignal;
|
|
607
|
+
}): Promise<OpenResultTabCorrection | undefined> {
|
|
608
|
+
const { correction, cwd, sessionName, signal } = options;
|
|
609
|
+
const result = await runSessionCommandData({
|
|
610
|
+
args: ["tab", String(correction.selectedIndex)],
|
|
611
|
+
cwd,
|
|
612
|
+
sessionName,
|
|
613
|
+
signal,
|
|
614
|
+
});
|
|
615
|
+
return result === undefined ? undefined : correction;
|
|
616
|
+
}
|
|
617
|
+
|
|
191
618
|
function buildSharedBrowserPlaybookGuidelines(hasBraveApiKey: boolean): string[] {
|
|
192
619
|
return [
|
|
193
620
|
SHARED_BROWSER_PLAYBOOK_GUIDELINES[0],
|
|
@@ -209,6 +636,22 @@ function buildSessionDetailFields(sessionName: string | undefined, usedImplicitS
|
|
|
209
636
|
return sessionName ? { sessionName, usedImplicitSession } : {};
|
|
210
637
|
}
|
|
211
638
|
|
|
639
|
+
function getPersistentSessionArtifactStore(ctx: {
|
|
640
|
+
sessionManager: {
|
|
641
|
+
getSessionDir?: () => string;
|
|
642
|
+
getSessionFile?: () => string | undefined;
|
|
643
|
+
getSessionId: () => string | undefined;
|
|
644
|
+
};
|
|
645
|
+
}): PersistentSessionArtifactStore | undefined {
|
|
646
|
+
const sessionFile = typeof ctx.sessionManager.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : undefined;
|
|
647
|
+
const sessionDir = typeof ctx.sessionManager.getSessionDir === "function" ? ctx.sessionManager.getSessionDir() : undefined;
|
|
648
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
649
|
+
if (!sessionFile || !sessionDir || !sessionId) {
|
|
650
|
+
return undefined;
|
|
651
|
+
}
|
|
652
|
+
return { sessionDir, sessionId };
|
|
653
|
+
}
|
|
654
|
+
|
|
212
655
|
function redactRecoveryHint(recoveryHint: {
|
|
213
656
|
exampleArgs: string[];
|
|
214
657
|
exampleParams: { args: string[]; sessionMode: "fresh" };
|
|
@@ -256,6 +699,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
256
699
|
let managedSessionName = managedSessionBaseName;
|
|
257
700
|
let managedSessionCwd = process.cwd();
|
|
258
701
|
let freshSessionOrdinal = 0;
|
|
702
|
+
let sessionTabTargets = new Map<string, SessionTabTarget>();
|
|
259
703
|
|
|
260
704
|
pi.on("session_start", async (_event, ctx) => {
|
|
261
705
|
managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
|
|
@@ -264,17 +708,12 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
264
708
|
managedSessionName = restoredState.sessionName;
|
|
265
709
|
managedSessionCwd = ctx.cwd;
|
|
266
710
|
freshSessionOrdinal = restoredState.freshSessionOrdinal;
|
|
711
|
+
sessionTabTargets = restoreSessionTabTargetsFromBranch(ctx.sessionManager.getBranch());
|
|
267
712
|
});
|
|
268
713
|
|
|
269
714
|
pi.on("session_shutdown", async () => {
|
|
270
|
-
if (managedSessionActive) {
|
|
271
|
-
await closeManagedSession({
|
|
272
|
-
cwd: managedSessionCwd,
|
|
273
|
-
sessionName: managedSessionName,
|
|
274
|
-
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
715
|
managedSessionActive = false;
|
|
716
|
+
sessionTabTargets = new Map<string, SessionTabTarget>();
|
|
278
717
|
await cleanupSecureTempArtifacts();
|
|
279
718
|
});
|
|
280
719
|
|
|
@@ -332,6 +771,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
332
771
|
});
|
|
333
772
|
const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
|
|
334
773
|
const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
|
|
774
|
+
const compatibilityWorkaround: CompatibilityWorkaround | undefined = executionPlan.compatibilityWorkaround;
|
|
335
775
|
if (executionPlan.managedSessionName === freshSessionName) {
|
|
336
776
|
freshSessionOrdinal += 1;
|
|
337
777
|
}
|
|
@@ -351,21 +791,56 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
351
791
|
};
|
|
352
792
|
}
|
|
353
793
|
|
|
794
|
+
const priorSessionTabTarget = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
|
|
795
|
+
const includePinnedNavigationSummary =
|
|
796
|
+
executionPlan.commandInfo.command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(executionPlan.commandInfo.command);
|
|
797
|
+
let sessionTabCorrection: OpenResultTabCorrection | undefined;
|
|
798
|
+
let processArgs = executionPlan.effectiveArgs;
|
|
799
|
+
let processStdin = params.stdin;
|
|
800
|
+
if (
|
|
801
|
+
priorSessionTabTarget &&
|
|
802
|
+
shouldPinSessionTabForCommand({
|
|
803
|
+
command: executionPlan.commandInfo.command,
|
|
804
|
+
sessionName: executionPlan.sessionName,
|
|
805
|
+
stdin: params.stdin,
|
|
806
|
+
})
|
|
807
|
+
) {
|
|
808
|
+
const plannedSessionTabSelection = await collectSessionTabSelection({
|
|
809
|
+
cwd: ctx.cwd,
|
|
810
|
+
sessionName: executionPlan.sessionName,
|
|
811
|
+
signal,
|
|
812
|
+
target: priorSessionTabTarget,
|
|
813
|
+
});
|
|
814
|
+
const commandTokens = extractCommandTokens(params.args);
|
|
815
|
+
if (plannedSessionTabSelection && commandTokens.length > 0 && executionPlan.sessionName) {
|
|
816
|
+
sessionTabCorrection = plannedSessionTabSelection;
|
|
817
|
+
processArgs = ["--json", "--session", executionPlan.sessionName, "batch"];
|
|
818
|
+
processStdin = JSON.stringify([
|
|
819
|
+
["tab", String(plannedSessionTabSelection.selectedIndex)],
|
|
820
|
+
commandTokens,
|
|
821
|
+
...(includePinnedNavigationSummary ? [["get", "title"], ["get", "url"]] : []),
|
|
822
|
+
]);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const redactedProcessArgs = redactInvocationArgs(processArgs);
|
|
826
|
+
|
|
354
827
|
onUpdate?.({
|
|
355
|
-
content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(
|
|
828
|
+
content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedProcessArgs)}` }],
|
|
356
829
|
details: {
|
|
357
|
-
|
|
830
|
+
compatibilityWorkaround,
|
|
831
|
+
effectiveArgs: redactedProcessArgs,
|
|
358
832
|
sessionMode,
|
|
833
|
+
sessionTabCorrection,
|
|
359
834
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
360
835
|
},
|
|
361
836
|
});
|
|
362
837
|
|
|
363
838
|
const processResult = await runAgentBrowserProcess({
|
|
364
|
-
args:
|
|
839
|
+
args: processArgs,
|
|
365
840
|
cwd: ctx.cwd,
|
|
366
841
|
env: executionPlan.managedSessionName ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: implicitSessionIdleTimeoutMs } : undefined,
|
|
367
842
|
signal,
|
|
368
|
-
stdin:
|
|
843
|
+
stdin: processStdin,
|
|
369
844
|
});
|
|
370
845
|
|
|
371
846
|
if (processResult.spawnError?.message.includes("ENOENT")) {
|
|
@@ -374,8 +849,10 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
374
849
|
content: [{ type: "text", text: errorText }],
|
|
375
850
|
details: {
|
|
376
851
|
args: redactedArgs,
|
|
377
|
-
|
|
852
|
+
compatibilityWorkaround,
|
|
853
|
+
effectiveArgs: redactedProcessArgs,
|
|
378
854
|
sessionMode,
|
|
855
|
+
sessionTabCorrection,
|
|
379
856
|
spawnError: processResult.spawnError.message,
|
|
380
857
|
},
|
|
381
858
|
isError: true,
|
|
@@ -387,26 +864,78 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
387
864
|
stdout: processResult.stdout,
|
|
388
865
|
stdoutPath: processResult.stdoutSpillPath,
|
|
389
866
|
});
|
|
867
|
+
let parseError = parsed.parseError;
|
|
390
868
|
let presentationEnvelope = parsed.envelope;
|
|
869
|
+
let navigationSummary: NavigationSummary | undefined;
|
|
870
|
+
if (sessionTabCorrection) {
|
|
871
|
+
const pinnedBatchResult = unwrapPinnedSessionBatchEnvelope({
|
|
872
|
+
envelope: parsed.envelope,
|
|
873
|
+
includeNavigationSummary: includePinnedNavigationSummary,
|
|
874
|
+
});
|
|
875
|
+
parseError = pinnedBatchResult.parseError ?? parseError;
|
|
876
|
+
presentationEnvelope = pinnedBatchResult.envelope ?? presentationEnvelope;
|
|
877
|
+
navigationSummary = pinnedBatchResult.navigationSummary;
|
|
878
|
+
}
|
|
391
879
|
const processSucceeded = !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
|
|
392
880
|
const plainTextInspection = executionPlan.plainTextInspection && processSucceeded;
|
|
393
|
-
const parseSucceeded = plainTextInspection ||
|
|
394
|
-
const envelopeSuccess = plainTextInspection ? true :
|
|
881
|
+
const parseSucceeded = plainTextInspection || parseError === undefined;
|
|
882
|
+
const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
|
|
395
883
|
const succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
|
|
396
884
|
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
397
885
|
|
|
398
|
-
|
|
399
|
-
if (succeeded && shouldCaptureNavigationSummary(executionPlan.commandInfo.command, parsed.envelope?.data)) {
|
|
886
|
+
if (succeeded && !navigationSummary && shouldCaptureNavigationSummary(executionPlan.commandInfo.command, presentationEnvelope?.data)) {
|
|
400
887
|
navigationSummary = await collectNavigationSummary({
|
|
401
888
|
cwd: ctx.cwd,
|
|
402
889
|
sessionName: executionPlan.sessionName,
|
|
403
890
|
signal,
|
|
404
891
|
});
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
892
|
+
}
|
|
893
|
+
if (navigationSummary && presentationEnvelope) {
|
|
894
|
+
presentationEnvelope = {
|
|
895
|
+
...presentationEnvelope,
|
|
896
|
+
data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
let openResultTabCorrection: OpenResultTabCorrection | undefined;
|
|
901
|
+
if (
|
|
902
|
+
succeeded &&
|
|
903
|
+
executionPlan.sessionName &&
|
|
904
|
+
params.args.some((token) => token === "--profile" || token.startsWith("--profile=")) &&
|
|
905
|
+
(executionPlan.commandInfo.command === "goto" ||
|
|
906
|
+
executionPlan.commandInfo.command === "navigate" ||
|
|
907
|
+
executionPlan.commandInfo.command === "open")
|
|
908
|
+
) {
|
|
909
|
+
const targetTitle = extractStringResultField(presentationEnvelope?.data, "title");
|
|
910
|
+
const targetUrl = extractStringResultField(presentationEnvelope?.data, "url");
|
|
911
|
+
const plannedTabCorrection = await collectOpenResultTabCorrection({
|
|
912
|
+
cwd: ctx.cwd,
|
|
913
|
+
sessionName: executionPlan.sessionName,
|
|
914
|
+
signal,
|
|
915
|
+
targetTitle,
|
|
916
|
+
targetUrl,
|
|
917
|
+
});
|
|
918
|
+
if (plannedTabCorrection) {
|
|
919
|
+
openResultTabCorrection = await applyOpenResultTabCorrection({
|
|
920
|
+
correction: plannedTabCorrection,
|
|
921
|
+
cwd: ctx.cwd,
|
|
922
|
+
sessionName: executionPlan.sessionName,
|
|
923
|
+
signal,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const currentSessionTabTarget = deriveSessionTabTarget({
|
|
929
|
+
command: executionPlan.commandInfo.command,
|
|
930
|
+
data: presentationEnvelope?.data,
|
|
931
|
+
navigationSummary,
|
|
932
|
+
previousTarget: priorSessionTabTarget,
|
|
933
|
+
});
|
|
934
|
+
if (executionPlan.sessionName) {
|
|
935
|
+
if (executionPlan.commandInfo.command === "close" && succeeded) {
|
|
936
|
+
sessionTabTargets.delete(executionPlan.sessionName);
|
|
937
|
+
} else if (currentSessionTabTarget) {
|
|
938
|
+
sessionTabTargets.set(executionPlan.sessionName, currentSessionTabTarget);
|
|
410
939
|
}
|
|
411
940
|
}
|
|
412
941
|
|
|
@@ -425,6 +954,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
425
954
|
managedSessionCwd = ctx.cwd;
|
|
426
955
|
}
|
|
427
956
|
if (replacedManagedSessionName) {
|
|
957
|
+
sessionTabTargets.delete(replacedManagedSessionName);
|
|
428
958
|
await closeManagedSession({
|
|
429
959
|
cwd: priorManagedSessionCwd,
|
|
430
960
|
sessionName: replacedManagedSessionName,
|
|
@@ -434,9 +964,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
434
964
|
|
|
435
965
|
const errorText = getAgentBrowserErrorText({
|
|
436
966
|
aborted: processResult.aborted,
|
|
437
|
-
envelope:
|
|
967
|
+
envelope: presentationEnvelope,
|
|
438
968
|
exitCode: processResult.exitCode,
|
|
439
|
-
parseError
|
|
969
|
+
parseError,
|
|
440
970
|
plainTextInspection,
|
|
441
971
|
spawnError: processResult.spawnError,
|
|
442
972
|
stderr: processResult.stderr,
|
|
@@ -459,6 +989,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
459
989
|
cwd: ctx.cwd,
|
|
460
990
|
envelope: presentationEnvelope,
|
|
461
991
|
errorText,
|
|
992
|
+
persistentArtifactStore: getPersistentSessionArtifactStore(ctx),
|
|
462
993
|
});
|
|
463
994
|
const redactedContent = presentation.content.map((item) =>
|
|
464
995
|
item.type === "text" ? { ...item, text: redactSensitiveText(item.text) } : item,
|
|
@@ -471,19 +1002,23 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
471
1002
|
batchFailure: redactSensitiveValue(presentation.batchFailure),
|
|
472
1003
|
batchSteps: redactSensitiveValue(presentation.batchSteps),
|
|
473
1004
|
command: executionPlan.commandInfo.command,
|
|
1005
|
+
compatibilityWorkaround,
|
|
474
1006
|
subcommand: executionPlan.commandInfo.subcommand,
|
|
475
1007
|
data: redactSensitiveValue(presentation.data),
|
|
476
|
-
error: plainTextInspection ? undefined : redactSensitiveValue(
|
|
1008
|
+
error: plainTextInspection ? undefined : redactSensitiveValue(presentationEnvelope?.error),
|
|
477
1009
|
inspection: plainTextInspection || undefined,
|
|
478
1010
|
navigationSummary: redactSensitiveValue(navigationSummary),
|
|
479
|
-
|
|
1011
|
+
openResultTabCorrection: redactSensitiveValue(openResultTabCorrection),
|
|
1012
|
+
effectiveArgs: redactedProcessArgs,
|
|
480
1013
|
exitCode: processResult.exitCode,
|
|
481
1014
|
fullOutputPath: presentation.fullOutputPath,
|
|
482
1015
|
fullOutputPaths: presentation.fullOutputPaths,
|
|
483
1016
|
imagePath: presentation.imagePath,
|
|
484
1017
|
imagePaths: presentation.imagePaths,
|
|
485
|
-
parseError: plainTextInspection ? undefined :
|
|
1018
|
+
parseError: plainTextInspection ? undefined : parseError,
|
|
486
1019
|
sessionMode,
|
|
1020
|
+
sessionTabCorrection: redactSensitiveValue(sessionTabCorrection),
|
|
1021
|
+
sessionTabTarget: redactSensitiveValue(currentSessionTabTarget),
|
|
487
1022
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
488
1023
|
sessionRecoveryHint: redactedRecoveryHint,
|
|
489
1024
|
startupScopedFlags: executionPlan.startupScopedFlags,
|