pi-agent-browser-native 0.2.25 → 0.2.26

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.
@@ -17,10 +17,10 @@ export const QUICK_START_GUIDELINES = [
17
17
  "Quick start mental model: use exactly one of args (exact agent-browser CLI args after the binary), semanticAction (a thin find-locator shorthand compiled to find argv), job (a constrained short-workflow schema compiled to batch), qa (a lightweight QA preset built on job/batch), or the experimental sourceLookup / networkSourceLookup helpers (each compiled to batch); stdin is only for batch, eval --stdin, auth save --password-stdin, and wrapper-generated batch stdin from job, qa, sourceLookup, or networkSourceLookup, and other command/stdin combinations are rejected before launch; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new --profile, --session-name, --cdp, --state, --auto-connect, --init-script, --enable, -p/--provider, or iOS --device state.",
18
18
  "There is no first-class reusable named browser recipe runtime above top-level job, the qa preset, and raw batch stdin; keep recurring flows in documentation examples or those inputs (closed RQ-0068; see docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet).",
19
19
  "Common first calls: { args: [\"open\", \"https://example.com\"] } then { args: [\"snapshot\", \"-i\"] }; after navigation, use { args: [\"click\", \"@e2\"] } then { args: [\"snapshot\", \"-i\"] }.",
20
- "Locator-first clicks and fills without hand-building find argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } } or { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }; details.compiledSemanticAction shows the derived find command, and stale-ref failures for compiled semantic targets can return a retry-semantic-action-after-stale-ref next action when retry safety is provable.",
20
+ "Locator-first clicks and fills without hand-building find argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } } or { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }; add semanticAction.session when targeting a named upstream browser session; details.compiledSemanticAction shows the derived find command; selector-not-found failures may append bounded try-*-candidate next actions (and an Agent-browser candidate fallbacks prose block) for specific placeholder/text/label shapes, and stale-ref failures can return retry-semantic-action-after-stale-ref when retry safety is provable.",
21
21
  "Common advanced calls: { args: [\"batch\"], stdin: \"[[\\\"open\\\",\\\"https://example.com\\\"],[\\\"snapshot\\\",\\\"-i\\\"]]\" }, { job: { steps: [{ action: \"open\", url: \"https://example.com\" }, { action: \"assertText\", text: \"Example Domain\" }, { action: \"screenshot\", path: \".dogfood/example.png\" }] } }, { qa: { url: \"https://example.com\", expectedText: \"Example Domain\", screenshotPath: \".dogfood/qa-example.png\" } }, { args: [\"eval\", \"--stdin\"], stdin: \"document.title\" }, { args: [\"auth\", \"save\", \"name\", \"--password-stdin\"], stdin: \"<password from user-approved secret source>\" }, { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }, and { args: [\"open\", \"--enable\", \"react-devtools\", \"https://example.com\"], sessionMode: \"fresh\" }.",
22
22
  "High-value command reference: download <selector> <path> saves a file triggered by a click; get title/url/text/html/value/attr/count reads page state; screenshot [path] captures an image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree/inspect/renders/suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation.",
23
- "For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step.",
23
+ "For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata. Browser close does not delete explicit saved files; if close reports details.artifactCleanup, use host file tools to remove paths listed in explicitArtifactPaths (when non-empty) after inspection. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step.",
24
24
  "When details.nextActions is present, prefer those exact native agent_browser follow-up payloads over prose guidance; they may include args, stdin, sessionMode, safety notes, or artifactPath for saved files.",
25
25
  ] as const;
26
26
 
@@ -28,7 +28,8 @@ export const BRAVE_SEARCH_PROMPT_GUIDELINE =
28
28
  "When a non-empty BRAVE_API_KEY is available in the current environment, prefer the Brave Search API via bash/curl to discover specific destination URLs, then open the chosen URL with agent_browser instead of browsing a search engine results page just to find the target.";
29
29
 
30
30
  export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
31
- "Standard workflow: open the page, snapshot -i, interact using current @refs from that snapshot, and re-snapshot after navigation, scrolling, rerendering, or other major DOM changes because refs can become stale.",
31
+ "Standard workflow: open the page, snapshot -i, interact using current @refs from that snapshot, and re-snapshot after navigation, scrolling, rerendering, or other major DOM changes because refs are page-scoped; the wrapper fails mutation-prone stale/recycled refs before upstream can silently target a different current-page element.",
32
+ "When snapshot -i compacts because the tree is oversized, scan visible output for Omitted high-value controls and optional details.data.highValueControlRefIds before opening the spill file: those list bounded searchboxes, textboxes, comboboxes, buttons, tabs, checkboxes, radios, options, and menuitems that did not fit the key/other ref previews.",
32
33
  "When a visible text or accessible-name target should survive ref churn, prefer find locators such as role, text, label, placeholder, alt, title, or testid with the intended action instead of guessing a CSS selector.",
33
34
  "Do not assume Playwright selector dialects such as text=Close or button:has-text('Close') are supported wrapper syntax unless current upstream agent-browser behavior has been verified.",
34
35
  "For authenticated or user-specific content like feeds, inboxes, dashboards, and accounts, prefer --profile Default on the first browser call and let the implicit session carry continuity. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser.",
@@ -47,8 +48,8 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
47
48
  "For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or eval --stdin on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
48
49
  "For downloads, prefer download <selector> <path> when an element click should save a file. Do not rely on click alone when you need the downloaded file on disk.",
49
50
  "When using eval --stdin, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.",
50
- "When using eval --stdin for extraction, return the value you want instead of relying on console.log as the primary result channel.",
51
- "When details.pageChangeSummary is present, use changeType and summary as a compact signal for navigation, DOM mutation, confirmations, or artifacts; when nextActionIds is set, match those ids to entries in details.nextActions (or per-step nextActions inside batch) for concrete follow-up payloads instead of inferring from prose alone.",
51
+ "When using eval --stdin for extraction, return the value you want instead of relying on console.log as the primary result channel. Prefer plain expressions like ({ title: document.title }) or explicitly invoked functions like (() => ({ title: document.title }))(); if a function-shaped snippet returns {}, details.evalStdinHint may warn that the function was serialized instead of called. If get text on a CSS selector surfaces details.selectorTextVisibility or selectorTextVisibilityAll, prefer a visible @ref, a more specific selector, or the inspect-visible-text-candidates nextAction over hidden tab content.",
52
+ "When details.pageChangeSummary is present, use changeType and summary as a compact signal for navigation, DOM mutation, confirmations, or artifacts; when nextActionIds is set, match those ids to entries in details.nextActions (or per-step nextActions inside batch) for concrete follow-up payloads instead of inferring from prose alone. If a no-navigation click surfaces details.overlayBlockers, inspect the fresh snapshot evidence before using a close/dismiss candidate nextAction.",
52
53
  "When commands save or spill files (screenshots, downloads, PDFs, traces, recordings, HAR, large snapshot spills), treat paths as provisional until details.artifactVerification shows every row verified: branch on missingCount, pendingCount, unverifiedCount, per-entry state, and optional limitation before downstream file use.",
53
54
  "Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
54
55
  ] as const;
@@ -22,6 +22,7 @@ import { buildSnapshotPresentation, formatRawSnapshotText, formatSnapshotSummary
22
22
  import {
23
23
  type AgentBrowserBatchResult,
24
24
  type AgentBrowserEnvelope,
25
+ type AgentBrowserNextAction,
25
26
  type AgentBrowserPageChangeSummary,
26
27
  type ArtifactVerificationEntry,
27
28
  type ArtifactVerificationSummary,
@@ -29,6 +30,7 @@ import {
29
30
  buildAgentBrowserResultCategoryDetails,
30
31
  classifyAgentBrowserFailureCategory,
31
32
  classifyAgentBrowserSuccessCategory,
33
+ classifyNetworkRequestFailure,
32
34
  type BatchFailurePresentationDetails,
33
35
  type BatchStepPresentationDetails,
34
36
  type ArtifactStorageScope,
@@ -43,6 +45,7 @@ import {
43
45
  formatSessionArtifactRetentionSummary,
44
46
  mergeSessionArtifactManifest,
45
47
  stringifyUnknown,
48
+ summarizeNetworkFailures,
46
49
  truncateText,
47
50
  } from "./shared.js";
48
51
 
@@ -589,7 +592,9 @@ function formatNetworkRequestLine(item: Record<string, unknown>, index: number):
589
592
  const url = getStringField(item, "url") ?? "(no url)";
590
593
  const requestId = getStringField(item, "requestId") ?? getStringField(item, "id");
591
594
  const idText = requestId ? ` [${redactSensitiveText(requestId)}]` : "";
592
- const lines = [`${index + 1}. ${status} ${method} ${truncateText(redactSensitiveText(url), 180)}${type ? ` (${type})` : ""}${idText}`];
595
+ const failureClassification = classifyNetworkRequestFailure(item);
596
+ const impactText = failureClassification ? ` [${failureClassification.impact}: ${failureClassification.reason}]` : "";
597
+ const lines = [`${index + 1}. ${status} ${method} ${truncateText(redactSensitiveText(url), 180)}${type ? ` (${type})` : ""}${idText}${impactText}`];
593
598
  appendNetworkPreview(lines, "Payload", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.request), NETWORK_BODY_PREVIEW_MAX_CHARS);
594
599
  appendNetworkPreview(lines, "Response", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.response), NETWORK_BODY_PREVIEW_MAX_CHARS);
595
600
  appendNetworkPreview(lines, "Error", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.error), NETWORK_ERROR_PREVIEW_MAX_CHARS);
@@ -600,10 +605,14 @@ function formatNetworkRequestsText(data: Record<string, unknown>): string | unde
600
605
  const requests = getArrayField(data, "requests");
601
606
  if (!requests) return undefined;
602
607
  if (requests.length === 0) return "No network requests captured.";
603
- const shown = requests.slice(0, DIAGNOSTIC_REQUEST_PREVIEW_LIMIT).flatMap((item, index) => {
608
+ const networkFailureSummary = summarizeNetworkFailures(requests);
609
+ const shown = networkFailureSummary.totalCount > 0
610
+ ? [`Network failure summary: ${networkFailureSummary.actionableCount} actionable, ${networkFailureSummary.benignCount} benign low-impact (${networkFailureSummary.totalCount} total).`]
611
+ : [];
612
+ shown.push(...requests.slice(0, DIAGNOSTIC_REQUEST_PREVIEW_LIMIT).flatMap((item, index) => {
604
613
  if (!isRecord(item)) return [`${index + 1}. ${stringifyModelFacing(item)}`];
605
614
  return formatNetworkRequestLine(item, index);
606
- });
615
+ }));
607
616
  if (requests.length > DIAGNOSTIC_REQUEST_PREVIEW_LIMIT) {
608
617
  shown.push(`... (${requests.length - DIAGNOSTIC_REQUEST_PREVIEW_LIMIT} additional requests omitted from preview)`);
609
618
  }
@@ -1478,6 +1487,65 @@ function getSelectorRecoveryHint(errorText: string): string | undefined {
1478
1487
  return undefined;
1479
1488
  }
1480
1489
 
1490
+ interface CommandSuggestion {
1491
+ args?: string[];
1492
+ description: string;
1493
+ id?: string;
1494
+ }
1495
+
1496
+ const UNKNOWN_COMMAND_SUGGESTIONS: Record<string, CommandSuggestion[]> = {
1497
+ attr: [
1498
+ { description: "Use `get attr <selector> <name>` to read an attribute from a selector or current `@ref`." },
1499
+ ],
1500
+ count: [
1501
+ { description: "Use `get count <selector>` to count matching elements." },
1502
+ ],
1503
+ html: [
1504
+ { description: "Use `get html <selector>` to read element HTML, or `get html` for the page when upstream supports it." },
1505
+ ],
1506
+ text: [
1507
+ { description: "Use `get text <selector>` to read text from a selector or current `@ref`; run `snapshot -i` first when you need a safe `@ref`." },
1508
+ ],
1509
+ title: [
1510
+ { args: ["get", "title"], description: "Use `get title` to read the current page title.", id: "use-get-title" },
1511
+ ],
1512
+ url: [
1513
+ { args: ["get", "url"], description: "Use `get url` to read the current page URL.", id: "use-get-url" },
1514
+ ],
1515
+ value: [
1516
+ { description: "Use `get value <selector>` to read form control value from a selector or current `@ref`." },
1517
+ ],
1518
+ };
1519
+
1520
+ function getUnknownCommandSuggestions(command: string | undefined, errorText: string): CommandSuggestion[] {
1521
+ if (!command) return [];
1522
+ const normalizedCommand = command.trim().toLowerCase();
1523
+ if (!/\bunknown\s+command\b|\bunknown\s+subcommand\b|\bunrecognized\s+command\b/i.test(errorText)) return [];
1524
+ return UNKNOWN_COMMAND_SUGGESTIONS[normalizedCommand] ?? [];
1525
+ }
1526
+
1527
+ function formatUnknownCommandSuggestionText(suggestions: CommandSuggestion[]): string | undefined {
1528
+ if (suggestions.length === 0) return undefined;
1529
+ return ["Agent-browser hint: This looks like a getter shortcut, but upstream getter commands are grouped under `get`.", ...suggestions.map((suggestion) => suggestion.description)].join(" ");
1530
+ }
1531
+
1532
+ function withSessionPrefix(sessionName: string | undefined, args: string[]): string[] {
1533
+ return sessionName ? ["--session", sessionName, ...args] : args;
1534
+ }
1535
+
1536
+ function buildUnknownCommandSuggestionActions(suggestions: CommandSuggestion[], sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
1537
+ const actions = suggestions
1538
+ .filter((suggestion): suggestion is CommandSuggestion & { args: string[]; id: string } => suggestion.args !== undefined && suggestion.id !== undefined)
1539
+ .map((suggestion) => ({
1540
+ id: suggestion.id,
1541
+ params: { args: withSessionPrefix(sessionName, suggestion.args) },
1542
+ reason: suggestion.description,
1543
+ safety: "Read-only getter command; safe to retry when you intended to inspect page state.",
1544
+ tool: "agent_browser" as const,
1545
+ }));
1546
+ return actions.length > 0 ? actions : undefined;
1547
+ }
1548
+
1481
1549
  function appendSelectorRecoveryHint(errorText: string): string {
1482
1550
  const hint = getSelectorRecoveryHint(errorText);
1483
1551
  if (!hint || errorText.includes("Agent-browser hint:")) {
@@ -2137,12 +2205,22 @@ export async function buildToolPresentation(options: {
2137
2205
  }): Promise<ToolPresentation> {
2138
2206
  const { args, artifactManifest, artifactRequest, commandInfo, cwd, envelope, errorText, persistentArtifactStore, sessionName } = options;
2139
2207
  if (errorText) {
2140
- const hintedErrorText = appendSelectorRecoveryHint(redactModelFacingText(errorText));
2208
+ const safeErrorText = redactModelFacingText(errorText);
2209
+ const selectorHintedErrorText = appendSelectorRecoveryHint(safeErrorText);
2210
+ const unknownCommandSuggestions = getUnknownCommandSuggestions(commandInfo.command, safeErrorText);
2211
+ const unknownCommandSuggestionText = formatUnknownCommandSuggestionText(unknownCommandSuggestions);
2212
+ const hintedErrorText = unknownCommandSuggestionText && !selectorHintedErrorText.includes("Agent-browser hint:")
2213
+ ? `${selectorHintedErrorText}\n\n${unknownCommandSuggestionText}`
2214
+ : selectorHintedErrorText;
2141
2215
  const categoryDetails = buildAgentBrowserResultCategoryDetails({ args: [commandInfo.command, commandInfo.subcommand].filter((item): item is string => item !== undefined), command: commandInfo.command, errorText: hintedErrorText, succeeded: false });
2216
+ const nextActions = [
2217
+ ...(buildUnknownCommandSuggestionActions(unknownCommandSuggestions, sessionName) ?? []),
2218
+ ...(buildAgentBrowserNextActions({ args, command: commandInfo.command, failureCategory: categoryDetails.failureCategory, resultCategory: "failure" }) ?? []),
2219
+ ];
2142
2220
  return {
2143
2221
  ...categoryDetails,
2144
2222
  content: [{ type: "text", text: hintedErrorText }],
2145
- nextActions: buildAgentBrowserNextActions({ args, command: commandInfo.command, failureCategory: categoryDetails.failureCategory, resultCategory: "failure" }),
2223
+ nextActions: nextActions.length > 0 ? nextActions : undefined,
2146
2224
  summary: hintedErrorText,
2147
2225
  };
2148
2226
  }
@@ -368,6 +368,7 @@ export function classifyAgentBrowserFailureCategory(options: {
368
368
  ) {
369
369
  return "selector-unsupported";
370
370
  }
371
+ if (command === "find" && /could not locate element|element not found|no elements? found|unable to find/i.test(text)) return "selector-not-found";
371
372
  if (reportsSelectorMatchFailure) return "selector-not-found";
372
373
  if ((command === "download" || text.includes("wait --download") || /\bdownload\b/i.test(text)) && /missing|not verified|not found|failed|timeout|timed out/i.test(text)) {
373
374
  return "download-not-verified";
@@ -592,6 +593,77 @@ export function buildAgentBrowserResultCategoryDetails(options: {
592
593
  };
593
594
  }
594
595
 
596
+ export type NetworkFailureImpact = "actionable" | "benign";
597
+
598
+ export interface NetworkFailureClassification {
599
+ impact: NetworkFailureImpact;
600
+ reason: string;
601
+ resourceType?: string;
602
+ status?: number;
603
+ url?: string;
604
+ }
605
+
606
+ export interface NetworkFailureSummary {
607
+ actionableCount: number;
608
+ benignCount: number;
609
+ failures: NetworkFailureClassification[];
610
+ totalCount: number;
611
+ }
612
+
613
+ function getStringRecordField(value: Record<string, unknown>, key: string): string | undefined {
614
+ const field = value[key];
615
+ return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined;
616
+ }
617
+
618
+ function getNetworkRequestUrlPath(url: string | undefined): string | undefined {
619
+ if (!url) return undefined;
620
+ try {
621
+ return new URL(url).pathname;
622
+ } catch {
623
+ const withoutQuery = url.split(/[?#]/, 1)[0];
624
+ return withoutQuery.length > 0 ? withoutQuery : undefined;
625
+ }
626
+ }
627
+
628
+ function isFailedNetworkRequest(request: Record<string, unknown>): boolean {
629
+ return (typeof request.status === "number" && request.status >= 400) || request.failed === true || typeof request.error === "string";
630
+ }
631
+
632
+ function isBenignAssetFailure(request: Record<string, unknown>, url: string | undefined, resourceType: string | undefined): boolean {
633
+ const path = getNetworkRequestUrlPath(url);
634
+ if (!path) return false;
635
+ const normalizedResourceType = resourceType?.toLowerCase();
636
+ return /(?:^|\/)(?:favicon(?:[-.\w]*)?\.(?:ico|png|svg)|apple-touch-icon(?:[-.\w]*)?\.png)$/i.test(path)
637
+ && (request.status === 404 || request.failed === true || typeof request.error === "string")
638
+ && (!normalizedResourceType || ["image", "img", "other"].includes(normalizedResourceType) || normalizedResourceType.startsWith("image/"));
639
+ }
640
+
641
+ export function classifyNetworkRequestFailure(request: Record<string, unknown>): NetworkFailureClassification | undefined {
642
+ if (!isFailedNetworkRequest(request)) return undefined;
643
+ const url = getStringRecordField(request, "url");
644
+ const resourceType = getStringRecordField(request, "resourceType") ?? getStringRecordField(request, "mimeType");
645
+ const status = typeof request.status === "number" ? request.status : undefined;
646
+ if (isBenignAssetFailure(request, url, resourceType)) {
647
+ return { impact: "benign", reason: "low-impact browser icon asset", resourceType, status, url };
648
+ }
649
+ return { impact: "actionable", reason: "document, script, API, or non-benign request failure", resourceType, status, url };
650
+ }
651
+
652
+ export function summarizeNetworkFailures(requests: unknown[]): NetworkFailureSummary {
653
+ const failures = requests.flatMap((request) => {
654
+ if (!isRecord(request)) return [];
655
+ const classification = classifyNetworkRequestFailure(request);
656
+ return classification ? [classification] : [];
657
+ });
658
+ const benignCount = failures.filter((failure) => failure.impact === "benign").length;
659
+ return {
660
+ actionableCount: failures.length - benignCount,
661
+ benignCount,
662
+ failures,
663
+ totalCount: failures.length,
664
+ };
665
+ }
666
+
595
667
  export function stringifyUnknown(value: unknown): string {
596
668
  if (typeof value === "string") return value;
597
669
  if (typeof value === "number" || typeof value === "boolean") return String(value);
@@ -34,6 +34,7 @@ const SNAPSHOT_SECTION_PREVIEW_LINES = 2;
34
34
  const SNAPSHOT_MAX_ADDITIONAL_SECTIONS = 2;
35
35
  const SNAPSHOT_KEY_REF_MAX_LINES = 8;
36
36
  const SNAPSHOT_OTHER_REF_MAX_LINES = 4;
37
+ const SNAPSHOT_HIGH_VALUE_REF_MAX_LINES = 10;
37
38
  const SNAPSHOT_ROLE_COUNT_MAX_ENTRIES = 4;
38
39
  const SNAPSHOT_FALLBACK_PREVIEW_MAX_LINES = 12;
39
40
  const SNAPSHOT_NAME_MAX_CHARS = 96;
@@ -58,6 +59,7 @@ const SNAPSHOT_SIGNAL_ROLES = new Set([
58
59
  "radio",
59
60
  "region",
60
61
  "row",
62
+ "searchbox",
61
63
  "tab",
62
64
  "textbox",
63
65
  ]);
@@ -69,14 +71,15 @@ const SNAPSHOT_ROLE_PRIORITY: Record<string, number> = {
69
71
  menu: 3,
70
72
  region: 4,
71
73
  heading: 5,
72
- button: 6,
74
+ searchbox: 6,
73
75
  textbox: 7,
74
76
  combobox: 8,
75
- checkbox: 9,
76
- radio: 10,
77
- tab: 11,
78
- option: 12,
79
- link: 13,
77
+ button: 9,
78
+ checkbox: 10,
79
+ radio: 11,
80
+ tab: 12,
81
+ option: 13,
82
+ link: 14,
80
83
  listitem: 14,
81
84
  row: 15,
82
85
  gridcell: 16,
@@ -103,6 +106,28 @@ const SNAPSHOT_CHROME_SECTION_PATTERNS = [
103
106
  /\brecommended\b/i,
104
107
  /\bsuggested\b/i,
105
108
  ];
109
+ const SNAPSHOT_HIGH_VALUE_CONTROL_ROLES = new Set([
110
+ "button",
111
+ "checkbox",
112
+ "combobox",
113
+ "menuitem",
114
+ "option",
115
+ "radio",
116
+ "searchbox",
117
+ "tab",
118
+ "textbox",
119
+ ]);
120
+ const SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY: Record<string, number> = {
121
+ searchbox: 0,
122
+ textbox: 1,
123
+ combobox: 2,
124
+ button: 3,
125
+ tab: 4,
126
+ checkbox: 5,
127
+ radio: 6,
128
+ option: 7,
129
+ menuitem: 8,
130
+ };
106
131
 
107
132
  interface SnapshotRefEntry {
108
133
  id: string;
@@ -458,6 +483,25 @@ function formatCompactRef(entry: SnapshotRefEntry): string {
458
483
  return `- ${entry.id} ${entry.role}${suffix}`;
459
484
  }
460
485
 
486
+ function isHighValueControlRef(entry: SnapshotRefEntry): boolean {
487
+ if (!SNAPSHOT_HIGH_VALUE_CONTROL_ROLES.has(entry.role)) return false;
488
+ if (isNoiseName(entry.name) || isChromeSectionName(entry.name)) return false;
489
+ return entry.name.length > 0 || entry.role === "searchbox" || entry.role === "textbox" || entry.role === "combobox";
490
+ }
491
+
492
+ function rankHighValueControlRefs(left: SnapshotRefEntry, right: SnapshotRefEntry): number {
493
+ const rolePriority =
494
+ (SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY[left.role] ?? 50) -
495
+ (SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY[right.role] ?? 50);
496
+ if (rolePriority !== 0) return rolePriority;
497
+
498
+ const leftHasName = left.name.length > 0 ? 0 : 1;
499
+ const rightHasName = right.name.length > 0 ? 0 : 1;
500
+ if (leftHasName !== rightHasName) return leftHasName - rightHasName;
501
+
502
+ return compareRefIds(left.id, right.id);
503
+ }
504
+
461
505
  function shouldCompactSnapshot(rawText: string, data: Record<string, unknown>): boolean {
462
506
  const snapshot = getSnapshotText(data) ?? "";
463
507
  const refEntries = getSnapshotRefEntries(data);
@@ -611,7 +655,16 @@ export async function buildSnapshotPresentation(
611
655
  const otherRefEntries = visibleRankedRefEntries
612
656
  .filter((entry) => !keyRefIdSet.has(entry.id))
613
657
  .slice(0, SNAPSHOT_OTHER_REF_MAX_LINES);
614
- const omittedOtherRefs = Math.max(0, visibleRankedRefEntries.length - keyRefEntries.length - otherRefEntries.length);
658
+ const displayedRefIdSet = new Set([...keyRefEntries, ...otherRefEntries].map((entry) => entry.id));
659
+ const omittedHighValueControlEntries = visibleRankedRefEntries
660
+ .filter((entry) => !displayedRefIdSet.has(entry.id) && isHighValueControlRef(entry))
661
+ .sort(rankHighValueControlRefs);
662
+ const visibleHighValueControlEntries = omittedHighValueControlEntries.slice(0, SNAPSHOT_HIGH_VALUE_REF_MAX_LINES);
663
+ const omittedHighValueControls = Math.max(0, omittedHighValueControlEntries.length - visibleHighValueControlEntries.length);
664
+ const omittedNonHighlightedRefs = Math.max(
665
+ 0,
666
+ visibleRankedRefEntries.length - keyRefEntries.length - otherRefEntries.length - omittedHighValueControlEntries.length,
667
+ );
615
668
  const origin = getSnapshotOrigin(data);
616
669
 
617
670
  const lines: string[] = [
@@ -658,8 +711,14 @@ export async function buildSnapshotPresentation(
658
711
  if (otherRefEntries.length > 0) {
659
712
  lines.push("", "Other refs:", ...otherRefEntries.map(formatCompactRef));
660
713
  }
661
- if (omittedOtherRefs > 0) {
662
- lines.push(`- ... (${omittedOtherRefs} additional refs omitted)`);
714
+ if (omittedNonHighlightedRefs > 0) {
715
+ lines.push(`- ... (${omittedNonHighlightedRefs} additional refs omitted)`);
716
+ }
717
+ if (visibleHighValueControlEntries.length > 0) {
718
+ lines.push("", "Omitted high-value controls:", ...visibleHighValueControlEntries.map(formatCompactRef));
719
+ if (omittedHighValueControls > 0) {
720
+ lines.push(`- ... (${omittedHighValueControls} additional high-value controls omitted)`);
721
+ }
663
722
  }
664
723
 
665
724
  lines.push(
@@ -689,6 +748,7 @@ export async function buildSnapshotPresentation(
689
748
  previewMode: fallbackPreview ? "outline" : "structured",
690
749
  spillError: spillErrorText,
691
750
  previewRefIds: [...previewRefIds],
751
+ highValueControlRefIds: visibleHighValueControlEntries.map((entry) => entry.id),
692
752
  additionalSectionsOmitted: omittedAdditionalSectionCount,
693
753
  previewSections: [
694
754
  ...(primarySegment
@@ -649,9 +649,14 @@ export function restoreManagedSessionStateFromBranch(
649
649
 
650
650
  const messageIsError = typeof message.isError === "boolean" ? message.isError : undefined;
651
651
  const exitCode = typeof details.exitCode === "number" ? details.exitCode : undefined;
652
- const succeeded = messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
652
+ const outcome = typeof details.managedSessionOutcome === "object" && details.managedSessionOutcome !== null ? details.managedSessionOutcome as Record<string, unknown> : undefined;
653
+ const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
654
+ const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
655
+ const outcomeActiveAfter = outcome?.activeAfter === true;
656
+ const outcomeRepresentsActiveCurrentSession = outcomeActiveAfter && outcomeCurrentSessionName === managedSessionName && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged");
657
+ const succeeded = outcomeRepresentsActiveCurrentSession ? true : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
653
658
  const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
654
- if (succeeded && sessionMode === "fresh") {
659
+ if ((succeeded || outcomeRepresentsActiveCurrentSession) && sessionMode === "fresh") {
655
660
  freshSessionOrdinal += 1;
656
661
  }
657
662
  const staleCompletion = succeeded && command !== "close" && restoreRank < activeRestoreRank;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
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)",