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 +14 -0
- package/README.md +2 -0
- package/docs/ARCHITECTURE.md +2 -0
- package/docs/REQUIREMENTS.md +2 -0
- package/docs/TOOL_CONTRACT.md +2 -0
- package/extensions/agent-browser/index.ts +294 -23
- package/extensions/agent-browser/lib/process.ts +30 -2
- package/extensions/agent-browser/lib/runtime.ts +6 -7
- package/package.json +3 -3
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
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
package/docs/REQUIREMENTS.md
CHANGED
|
@@ -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
|
|
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -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 {
|
|
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(
|
|
828
|
+
content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedProcessArgs)}` }],
|
|
587
829
|
details: {
|
|
588
830
|
compatibilityWorkaround,
|
|
589
|
-
effectiveArgs:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ||
|
|
627
|
-
const envelopeSuccess = plainTextInspection ? true :
|
|
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
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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(
|
|
656
|
-
const targetUrl = extractStringResultField(
|
|
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:
|
|
967
|
+
envelope: presentationEnvelope,
|
|
699
968
|
exitCode: processResult.exitCode,
|
|
700
|
-
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(
|
|
1008
|
+
error: plainTextInspection ? undefined : redactSensitiveValue(presentationEnvelope?.error),
|
|
740
1009
|
inspection: plainTextInspection || undefined,
|
|
741
1010
|
navigationSummary: redactSensitiveValue(navigationSummary),
|
|
742
1011
|
openResultTabCorrection: redactSensitiveValue(openResultTabCorrection),
|
|
743
|
-
effectiveArgs:
|
|
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 :
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
+
"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.
|
|
49
|
+
"@mariozechner/pi-coding-agent": "^0.67.2",
|
|
50
50
|
"@sinclair/typebox": "^0.34.49",
|
|
51
|
-
"@types/node": "^25.
|
|
51
|
+
"@types/node": "^25.6.0",
|
|
52
52
|
"tsx": "^4.21.0",
|
|
53
53
|
"typescript": "^6.0.2"
|
|
54
54
|
},
|