pi-agent-browser-native 0.2.3 → 0.2.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.5 - 2026-04-14
4
+
5
+ ### Changed
6
+ - refreshed the development and release-verification baseline to `@mariozechner/pi-coding-agent` `0.67.2` and `@types/node` `25.6.0`, keeping local typechecking and package verification aligned with the latest stable pi release used for this extension
7
+ - re-locked the compatible transitive development dependency set pulled by the updated pi toolchain without changing the published `agent_browser` runtime contract
8
+
9
+ ## 0.2.4 - 2026-04-13
10
+
11
+ ### Fixed
12
+ - wrapper-spawned local Unix `agent-browser` runs now use a short private socket directory under `/tmp`, so extension-generated session names no longer fail the upstream Unix socket-path length limit in longer cwd/session-name combinations
13
+ - once the wrapper knows which tab a session should stay on, later active-tab commands like `click` and `snapshot -i` now best-effort pin that same tab inside the same upstream invocation instead of letting reconnect drift send the action to a restored/background tab
14
+ - persisted `sessionTabTarget` state now survives `/reload` / `/resume` for both managed and explicit sessions, so the reconnect-time tab pinning behavior can continue after restart/resume flows
15
+ - README, requirements, architecture notes, and tool-contract docs now describe the socket-path mitigation and the follow-up-command tab-pinning behavior
16
+
3
17
  ## 0.2.3 - 2026-04-13
4
18
 
5
19
  ### Fixed
package/README.md CHANGED
@@ -192,8 +192,10 @@ Current cautions:
192
192
  - startup-scoped flags like `--profile`, `--session-name`, and `--cdp` are for the first command that launches a session; if the implicit session is already active, retry that call with `sessionMode: "fresh"` or provide an explicit `--session ...` for the new launch
193
193
  - implicit `piab-*` sessions are extension-managed convenience sessions; they stay alive across `pi` shutdown/reload so later default calls can keep following the active managed browser on `/reload` or `/resume`, rely on the configured idle timeout to reduce stale background daemons, store persisted-session large snapshot spill files under a private session-scoped artifact directory with a bounded per-session budget so `details.fullOutputPath` survives reload/resume without unbounded growth, and still clean up process-private temp spill artifacts on shutdown
194
194
  - `sessionMode: "fresh"` without an explicit `--session` rotates that extension-managed session to the new browser so later auto calls keep using it
195
+ - for local Unix launches, the wrapper uses a short private socket directory under `/tmp` so extension-generated session names do not trip upstream Unix socket-path limits in longer cwd/session-name combinations
195
196
  - for direct headless local Chrome launches to `chatgpt.com` and `chat.openai.com`, the extension injects a normal Chrome user agent when the caller did not explicitly provide `--user-agent`; this keeps the default headless workflow usable without forcing `--headed` or `--auto-connect`
196
197
  - after profiled `open` calls, the extension best-effort re-selects the tab that matches the returned page URL when restored profile tabs steal focus during launch
198
+ - after a target tab is known, later active-tab commands like `click` and `snapshot -i` best-effort pin that same tab inside the same upstream invocation when a reconnect would otherwise drift to a restored tab
197
199
  - explicit caller-provided `--session` values are treated as user-managed and are not auto-closed by the extension
198
200
  - explicit caller-provided `--user-agent` values win over the ChatGPT/OpenAI compatibility workaround
199
201
  - tool progress/details redact sensitive invocation values such as `--headers`, proxy credentials, and auth-bearing URL parameters before echoing them back into Pi
@@ -88,6 +88,8 @@ Practical policy:
88
88
  - if an unnamed fresh launch replaces an active extension-managed session, best-effort close the old managed session after the switch succeeds
89
89
  - leave explicit caller-provided `--session` choices alone unless the caller closes them explicitly
90
90
  - after profiled `open` / `goto` / `navigate` calls, verify the active tab still matches the returned page URL and best-effort switch back when restored profile tabs steal focus
91
+ - once the wrapper knows which tab the agent is operating on, later active-tab commands may synthesize a tiny upstream `batch` that re-selects that tab and then runs the requested command in the same upstream invocation; this stays thin while avoiding reconnect-time drift on profile-restored sessions
92
+ - for local Unix launches, set a short private socket directory so extension-generated session names do not fail on the upstream Unix socket-path length limit
91
93
 
92
94
  This is primarily about ownership clarity and avoiding surprise, not adding a heavy safety wrapper. If the extension invented the session, the extension should own its lifecycle without breaking reload/resume semantics. If the caller explicitly chose the upstream session model, the extension should stay out of the way.
93
95
 
@@ -98,6 +98,8 @@ The design should comfortably support workflows such as:
98
98
  - Keep mitigations for legacy-skill coexistence simple; do not add extra moving parts unless observed behavior justifies them.
99
99
  - Prefer narrow, evidence-backed compatibility mitigations over broad stealth layers when a specific upstream site starts rejecting the default headless launch fingerprint.
100
100
  - Preserve the page that a profiled `open` just navigated to; if restored profile tabs steal focus during launch, the wrapper should best-effort switch back to the returned page URL before handing control back to the agent.
101
+ - Once a tab target is known for a session, later active-tab commands should best-effort pin that same tab inside the same upstream invocation when reconnect drift would otherwise land on a restored/background tab.
102
+ - On local Unix launches, extension-generated session names should not fail just because the upstream default socket path is too long; the wrapper should choose a shorter socket directory when needed.
101
103
 
102
104
  ## Open design questions
103
105
 
@@ -183,6 +183,8 @@ If `agent-browser` is not on `PATH`, fail with a message that:
183
183
  - treat explicit caller-provided `--session` choices as user-managed
184
184
  - pass explicit `--profile` straight through to upstream `agent-browser`; no profile-cloning or isolation layer is added in v1
185
185
  - after profiled `open` / `goto` / `navigate`, if upstream leaves a restored profile tab active instead of the page that was just opened, best-effort switch back to the tab whose URL matches the returned open result before returning control to the agent
186
+ - once the wrapper has a known tab target for a session, later active-tab commands may best-effort pin that tab inside the same upstream invocation so reconnect drift does not send a `click`, `snapshot`, or similar action to a restored/background tab instead
187
+ - on local Unix launches, set a short private socket directory for wrapper-spawned `agent-browser` processes so extension-generated session names do not fail the upstream Unix socket-path length limit in longer cwd/session-name combinations
186
188
  - treat successful plain-text inspection commands like `--help` and `--version` as stateless: do not inject the implicit managed session and do not let those calls claim the managed-session slot
187
189
  - if startup-scoped flags like `--profile`, `--session-name`, or `--cdp` are supplied after the implicit session is already active while `sessionMode` is `"auto"`, return a validation error with a structured recovery hint that recommends `sessionMode: "fresh"`
188
190
  - for direct headless local Chrome launches to `chatgpt.com` / `chat.openai.com`, allow a narrow compatibility fallback that injects a normal Chrome `--user-agent` only when the caller did not explicitly provide one and did not choose `--headed`, `--cdp`, `--auto-connect`, or a provider-backed launch
@@ -12,7 +12,13 @@ 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,
@@ -20,6 +26,7 @@ import {
20
26
  createEphemeralSessionSeed,
21
27
  createFreshSessionName,
22
28
  createImplicitSessionName,
29
+ extractCommandTokens,
23
30
  getImplicitSessionCloseTimeoutMs,
24
31
  getImplicitSessionIdleTimeoutMs,
25
32
  getLatestUserPrompt,
@@ -314,6 +321,185 @@ function extractStringResultField(data: unknown, fieldName: "title" | "url"): st
314
321
  return text.length > 0 ? text : undefined;
315
322
  }
316
323
 
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
+
317
503
  async function runSessionCommandData(options: {
318
504
  args: string[];
319
505
  cwd: string;
@@ -393,6 +579,26 @@ async function collectOpenResultTabCorrection(options: {
393
579
  return chooseOpenResultTabCorrection({ tabs, targetTitle, targetUrl });
394
580
  }
395
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
+
396
602
  async function applyOpenResultTabCorrection(options: {
397
603
  correction: OpenResultTabCorrection;
398
604
  cwd: string;
@@ -493,6 +699,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
493
699
  let managedSessionName = managedSessionBaseName;
494
700
  let managedSessionCwd = process.cwd();
495
701
  let freshSessionOrdinal = 0;
702
+ let sessionTabTargets = new Map<string, SessionTabTarget>();
496
703
 
497
704
  pi.on("session_start", async (_event, ctx) => {
498
705
  managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
@@ -501,10 +708,12 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
501
708
  managedSessionName = restoredState.sessionName;
502
709
  managedSessionCwd = ctx.cwd;
503
710
  freshSessionOrdinal = restoredState.freshSessionOrdinal;
711
+ sessionTabTargets = restoreSessionTabTargetsFromBranch(ctx.sessionManager.getBranch());
504
712
  });
505
713
 
506
714
  pi.on("session_shutdown", async () => {
507
715
  managedSessionActive = false;
716
+ sessionTabTargets = new Map<string, SessionTabTarget>();
508
717
  await cleanupSecureTempArtifacts();
509
718
  });
510
719
 
@@ -582,22 +791,56 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
582
791
  };
583
792
  }
584
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
+
585
827
  onUpdate?.({
586
- content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedEffectiveArgs)}` }],
828
+ content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedProcessArgs)}` }],
587
829
  details: {
588
830
  compatibilityWorkaround,
589
- effectiveArgs: redactedEffectiveArgs,
831
+ effectiveArgs: redactedProcessArgs,
590
832
  sessionMode,
833
+ sessionTabCorrection,
591
834
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
592
835
  },
593
836
  });
594
837
 
595
838
  const processResult = await runAgentBrowserProcess({
596
- args: executionPlan.effectiveArgs,
839
+ args: processArgs,
597
840
  cwd: ctx.cwd,
598
841
  env: executionPlan.managedSessionName ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: implicitSessionIdleTimeoutMs } : undefined,
599
842
  signal,
600
- stdin: params.stdin,
843
+ stdin: processStdin,
601
844
  });
602
845
 
603
846
  if (processResult.spawnError?.message.includes("ENOENT")) {
@@ -607,8 +850,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
607
850
  details: {
608
851
  args: redactedArgs,
609
852
  compatibilityWorkaround,
610
- effectiveArgs: redactedEffectiveArgs,
853
+ effectiveArgs: redactedProcessArgs,
611
854
  sessionMode,
855
+ sessionTabCorrection,
612
856
  spawnError: processResult.spawnError.message,
613
857
  },
614
858
  isError: true,
@@ -620,27 +864,37 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
620
864
  stdout: processResult.stdout,
621
865
  stdoutPath: processResult.stdoutSpillPath,
622
866
  });
867
+ let parseError = parsed.parseError;
623
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
+ }
624
879
  const processSucceeded = !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
625
880
  const plainTextInspection = executionPlan.plainTextInspection && processSucceeded;
626
- const parseSucceeded = plainTextInspection || parsed.parseError === undefined;
627
- const envelopeSuccess = plainTextInspection ? true : parsed.envelope?.success !== false;
881
+ const parseSucceeded = plainTextInspection || parseError === undefined;
882
+ const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
628
883
  const succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
629
884
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
630
885
 
631
- let navigationSummary: NavigationSummary | undefined;
632
- if (succeeded && shouldCaptureNavigationSummary(executionPlan.commandInfo.command, parsed.envelope?.data)) {
886
+ if (succeeded && !navigationSummary && shouldCaptureNavigationSummary(executionPlan.commandInfo.command, presentationEnvelope?.data)) {
633
887
  navigationSummary = await collectNavigationSummary({
634
888
  cwd: ctx.cwd,
635
889
  sessionName: executionPlan.sessionName,
636
890
  signal,
637
891
  });
638
- if (navigationSummary && presentationEnvelope) {
639
- presentationEnvelope = {
640
- ...presentationEnvelope,
641
- data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
642
- };
643
- }
892
+ }
893
+ if (navigationSummary && presentationEnvelope) {
894
+ presentationEnvelope = {
895
+ ...presentationEnvelope,
896
+ data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
897
+ };
644
898
  }
645
899
 
646
900
  let openResultTabCorrection: OpenResultTabCorrection | undefined;
@@ -652,8 +906,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
652
906
  executionPlan.commandInfo.command === "navigate" ||
653
907
  executionPlan.commandInfo.command === "open")
654
908
  ) {
655
- const targetTitle = extractStringResultField(parsed.envelope?.data, "title");
656
- const targetUrl = extractStringResultField(parsed.envelope?.data, "url");
909
+ const targetTitle = extractStringResultField(presentationEnvelope?.data, "title");
910
+ const targetUrl = extractStringResultField(presentationEnvelope?.data, "url");
657
911
  const plannedTabCorrection = await collectOpenResultTabCorrection({
658
912
  cwd: ctx.cwd,
659
913
  sessionName: executionPlan.sessionName,
@@ -671,6 +925,20 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
671
925
  }
672
926
  }
673
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);
939
+ }
940
+ }
941
+
674
942
  const priorManagedSessionCwd = managedSessionCwd;
675
943
  const managedSessionState = resolveManagedSessionState({
676
944
  command: executionPlan.commandInfo.command,
@@ -686,6 +954,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
686
954
  managedSessionCwd = ctx.cwd;
687
955
  }
688
956
  if (replacedManagedSessionName) {
957
+ sessionTabTargets.delete(replacedManagedSessionName);
689
958
  await closeManagedSession({
690
959
  cwd: priorManagedSessionCwd,
691
960
  sessionName: replacedManagedSessionName,
@@ -695,9 +964,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
695
964
 
696
965
  const errorText = getAgentBrowserErrorText({
697
966
  aborted: processResult.aborted,
698
- envelope: parsed.envelope,
967
+ envelope: presentationEnvelope,
699
968
  exitCode: processResult.exitCode,
700
- parseError: parsed.parseError,
969
+ parseError,
701
970
  plainTextInspection,
702
971
  spawnError: processResult.spawnError,
703
972
  stderr: processResult.stderr,
@@ -736,18 +1005,20 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
736
1005
  compatibilityWorkaround,
737
1006
  subcommand: executionPlan.commandInfo.subcommand,
738
1007
  data: redactSensitiveValue(presentation.data),
739
- error: plainTextInspection ? undefined : redactSensitiveValue(parsed.envelope?.error),
1008
+ error: plainTextInspection ? undefined : redactSensitiveValue(presentationEnvelope?.error),
740
1009
  inspection: plainTextInspection || undefined,
741
1010
  navigationSummary: redactSensitiveValue(navigationSummary),
742
1011
  openResultTabCorrection: redactSensitiveValue(openResultTabCorrection),
743
- effectiveArgs: redactedEffectiveArgs,
1012
+ effectiveArgs: redactedProcessArgs,
744
1013
  exitCode: processResult.exitCode,
745
1014
  fullOutputPath: presentation.fullOutputPath,
746
1015
  fullOutputPaths: presentation.fullOutputPaths,
747
1016
  imagePath: presentation.imagePath,
748
1017
  imagePaths: presentation.imagePaths,
749
- parseError: plainTextInspection ? undefined : parsed.parseError,
1018
+ parseError: plainTextInspection ? undefined : parseError,
750
1019
  sessionMode,
1020
+ sessionTabCorrection: redactSensitiveValue(sessionTabCorrection),
1021
+ sessionTabTarget: redactSensitiveValue(currentSessionTabTarget),
751
1022
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
752
1023
  sessionRecoveryHint: redactedRecoveryHint,
753
1024
  startupScopedFlags: executionPlan.startupScopedFlags,
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  import { spawn } from "node:child_process";
10
- import { env as processEnv } from "node:process";
10
+ import { chmod, mkdir } from "node:fs/promises";
11
+ import { env as processEnv, platform as processPlatform } from "node:process";
11
12
 
12
13
  import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
13
14
 
@@ -15,6 +16,8 @@ const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
15
16
  const MAX_BUFFERED_STDERR_CHARS = 32_000;
16
17
  const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
17
18
  const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
19
+ const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
20
+ const DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX = "/tmp/piab";
18
21
  const httpProxyEnvName = "http_proxy";
19
22
  const httpsProxyEnvName = "https_proxy";
20
23
  const allProxyEnvName = "all_proxy";
@@ -81,6 +84,26 @@ function appendTail(text: string, addition: string, maxChars: number): string {
81
84
  return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
82
85
  }
83
86
 
87
+ export function getAgentBrowserSocketDir(
88
+ platform: NodeJS.Platform = processPlatform,
89
+ uid: number | undefined = typeof process.getuid === "function" ? process.getuid() : undefined,
90
+ ): string | undefined {
91
+ if (platform === "win32") {
92
+ return undefined;
93
+ }
94
+ return `${DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX}${typeof uid === "number" ? `-${uid}` : ""}`;
95
+ }
96
+
97
+ async function ensureAgentBrowserSocketDir(socketDir: string): Promise<boolean> {
98
+ try {
99
+ await mkdir(socketDir, { recursive: true, mode: 0o700 });
100
+ await chmod(socketDir, 0o700).catch(() => undefined);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
84
107
  export function buildAgentBrowserProcessEnv(
85
108
  baseEnv: NodeJS.ProcessEnv = processEnv,
86
109
  overrides: NodeJS.ProcessEnv | undefined = undefined,
@@ -117,6 +140,11 @@ export async function runAgentBrowserProcess(options: {
117
140
  stdin?: string;
118
141
  }): Promise<ProcessRunResult> {
119
142
  const { args, cwd, env, signal, stdin } = options;
143
+ let effectiveEnv = env;
144
+ const requestedSocketDir = env?.[AGENT_BROWSER_SOCKET_DIR_ENV] ?? getAgentBrowserSocketDir();
145
+ if (requestedSocketDir && (await ensureAgentBrowserSocketDir(requestedSocketDir))) {
146
+ effectiveEnv = { ...env, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
147
+ }
120
148
 
121
149
  return await new Promise<ProcessRunResult>((resolve) => {
122
150
  let aborted = false;
@@ -191,7 +219,7 @@ export async function runAgentBrowserProcess(options: {
191
219
 
192
220
  const child = spawn("agent-browser", args, {
193
221
  cwd,
194
- env: buildAgentBrowserProcessEnv(processEnv, env),
222
+ env: buildAgentBrowserProcessEnv(processEnv, effectiveEnv),
195
223
  stdio: ["pipe", "pipe", "pipe"],
196
224
  });
197
225
 
@@ -768,8 +768,11 @@ export function chooseOpenResultTabCorrection(options: {
768
768
  }
769
769
 
770
770
  export function parseCommandInfo(args: string[]): CommandInfo {
771
- const commands: string[] = [];
771
+ const commandTokens = extractCommandTokens(args);
772
+ return { command: commandTokens[0], subcommand: commandTokens[1] };
773
+ }
772
774
 
775
+ export function extractCommandTokens(args: string[]): string[] {
773
776
  for (let index = 0; index < args.length; index += 1) {
774
777
  const token = args[index];
775
778
  if (token.startsWith("--session=")) {
@@ -782,11 +785,7 @@ export function parseCommandInfo(args: string[]): CommandInfo {
782
785
  }
783
786
  continue;
784
787
  }
785
- commands.push(token);
786
- if (commands.length === 2) {
787
- break;
788
- }
788
+ return args.slice(index);
789
789
  }
790
-
791
- return { command: commands[0], subcommand: commands[1] };
790
+ return [];
792
791
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "pi extension that exposes agent-browser as a native tool for browser automation",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
@@ -46,9 +46,9 @@
46
46
  "@sinclair/typebox": "*"
47
47
  },
48
48
  "devDependencies": {
49
- "@mariozechner/pi-coding-agent": "^0.66.1",
49
+ "@mariozechner/pi-coding-agent": "^0.67.2",
50
50
  "@sinclair/typebox": "^0.34.49",
51
- "@types/node": "^25.5.2",
51
+ "@types/node": "^25.6.0",
52
52
  "tsx": "^4.21.0",
53
53
  "typescript": "^6.0.2"
54
54
  },