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.
@@ -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 { buildToolPresentation, getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "./lib/results.js";
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 AGENT_BROWSER_BASH_PREFIX = String.raw`(?:env(?:\s+[A-Za-z_][A-Za-z0-9_]*=[^\s;&|]+)*\s+)?(?:(?:npx|bunx)(?:\s+-[^\s;&|]+|\s+--[^\s;&|]+(?:=[^\s;&|]+)?)*\s+|(?:pnpm|yarn)\s+dlx(?:\s+-[^\s;&|]+|\s+--[^\s;&|]+(?:=[^\s;&|]+)?)*\s+)?`;
100
- const AGENT_BROWSER_BASH_EXECUTABLE = String.raw`(?:[.~]|\.\.?|\/)?(?:[^\s;&|]+\/)?agent-browser`;
101
- const DIRECT_AGENT_BROWSER_BASH_PATTERN = new RegExp(
102
- String.raw`(^|[\s;&|])${AGENT_BROWSER_BASH_PREFIX}${AGENT_BROWSER_BASH_EXECUTABLE}(?=\s|$)`,
103
- );
104
- const HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN = /(command\s+-v|which|type\s+-P)\s+agent-browser\b/;
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
- return DIRECT_AGENT_BROWSER_BASH_PATTERN.test(command);
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
- async function collectNavigationSummary(options: {
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<NavigationSummary | undefined> {
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 readField = async (fieldName: "title" | "url"): Promise<string | undefined> => {
154
- const processResult = await runAgentBrowserProcess({
155
- args: ["--json", "--session", sessionName, "get", fieldName],
156
- cwd,
157
- signal,
158
- });
159
- if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
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
- const parsed = await parseAgentBrowserEnvelope({
163
- stdout: processResult.stdout,
164
- stdoutPath: processResult.stdoutSpillPath,
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
- const title = await readField("title");
179
- const url = await readField("url");
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(redactedEffectiveArgs)}` }],
828
+ content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedProcessArgs)}` }],
356
829
  details: {
357
- effectiveArgs: redactedEffectiveArgs,
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: executionPlan.effectiveArgs,
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: params.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
- effectiveArgs: redactedEffectiveArgs,
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 || parsed.parseError === undefined;
394
- const envelopeSuccess = plainTextInspection ? true : parsed.envelope?.success !== false;
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
- let navigationSummary: NavigationSummary | undefined;
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
- if (navigationSummary && presentationEnvelope) {
406
- presentationEnvelope = {
407
- ...presentationEnvelope,
408
- data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
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: parsed.envelope,
967
+ envelope: presentationEnvelope,
438
968
  exitCode: processResult.exitCode,
439
- parseError: parsed.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(parsed.envelope?.error),
1008
+ error: plainTextInspection ? undefined : redactSensitiveValue(presentationEnvelope?.error),
477
1009
  inspection: plainTextInspection || undefined,
478
1010
  navigationSummary: redactSensitiveValue(navigationSummary),
479
- effectiveArgs: redactedEffectiveArgs,
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 : parsed.parseError,
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,