mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
refactor: 移除 context-mode,Chrome 快照改为直接返回
context-mode 的 ctx_execute 系工具以真实 shell 执行且权限为 allow,绕过了 bash 策略与 protectedWrites;其每轮注入、独立 MCP 子进程与 skills 也与组合包 提示词单一所有权冲突。大输出职责已由 pi-overflow(入口截断)与 pi-compaction(压力遮蔽)承担: - 删除 context-mode 依赖、wrapper/过滤器、skills,以及 ctx_* 的权限、 tools.jsonc、路由规则、tool-search 常驻与 4 个分组 - better-sqlite3@12 仅为 context-mode 传递依赖,随之移除根 allowScripts - pi-chrome:按用户决定移除快照隐私规则与 .pi/chrome-context 临时文件机制, 快照 JSON 直接作为工具结果返回、details 只留小型元数据,超长由 pi-overflow 截断并用 overflow_query 提取;授权生命周期不变 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
2a6bf8c929
commit
56cbc83296
+2
-3
@@ -1,9 +1,8 @@
|
||||
# my-pi 维护约定(pi-chrome)
|
||||
|
||||
- 扩展入口是 `pi-chrome/extensions/chrome-profile-bridge/index.ts`,Companion MV3 扩展位于 `browser-extension/`,Session 快照存储位于 `src/session-snapshot-store.ts`,测试保留在 `test/` 与 `test-suite/`,来源记录是 `UPSTREAM.md`。
|
||||
- 扩展入口是 `pi-chrome/extensions/chrome-profile-bridge/index.ts`,Companion MV3 扩展位于 `browser-extension/`,测试保留在 `test/` 与 `test-suite/`,来源记录是 `UPSTREAM.md`。
|
||||
- 同步上游必须从官方 `tianrendong/pi-chrome` 的明确 tag/commit 移植并保留 README、CHANGELOG、SECURITY、文档、测试、作者和 LICENSE;不得导入嵌套 `.git`、`node_modules` 或构建产物。
|
||||
- 根包必须通过 `file:./pi-chrome` 提供依赖并直接加载仓库源码,不得同时安装或加载 npm 预编译入口。
|
||||
- 控制默认锁定,每个 Pi session 必须显式 `/chrome authorize`;未授权时不注册 Chrome 工具。显式授权后出现的 21 个 `chrome_*` 工具必须由 Tool Search 分入 `chrome-navigation`、`chrome-interaction`、`chrome-debugging`,不得落入 unknown-tools 模型增强路径。授权撤销或过期时必须停用全部 21 个工具,包括 `chrome_find` / `chrome_inspect`;Tool Search 重新激活 schema 不能替代每次执行时的授权校验。
|
||||
- 完整 Chrome snapshot 不得进入普通工具文本或 `details`;只能原子写入当前项目 `.pi/chrome-context/<session>/<target>/snapshot.json`,使用哈希目录、POSIX `700` 目录 / `600` 文件,并只返回工作区相对路径和有界元数据。快照不得跨 Session 持久化,Session 启动/关闭、撤销和授权过期必须清理。
|
||||
- `chrome_snapshot` 和带 `includeSnapshot=true` 的动作必须引导模型立即使用 `ctx_execute_file`,不得使用 `read` / `cat` 把原始 JSON 重新塞入上下文;任何可能改变页面或目标的成功操作必须使旧快照失效,`chrome_find` 只可额外返回有界匹配摘要。
|
||||
- `chrome_snapshot` 与带 `includeSnapshot=true` 的动作把快照 JSON 直接放进工具结果文本返回;超长结果的展示截断由 pi-overflow 统一负责(原文落盘,模型用 `overflow_query` 的 jsonPath/search 提取)。`details` 只放元素数、匹配数等小型元数据,不得把完整快照写进 `details`(会进入会话文件),也不再写 Session 临时快照文件。`chrome_find` 只返回有界匹配摘要,不保存文件。
|
||||
- 修改 bridge、Companion 扩展、授权 lifecycle、快照存储或加载入口后至少运行包内 `npm test`、根 Tool Routing/Tool Search/扩展联合加载测试、`npm pack --dry-run --json` 和实际 packed tarball 隔离安装验证;未经明确要求不得连接或控制真实 Chrome。
|
||||
|
||||
+4
-10
@@ -6,7 +6,7 @@
|
||||
|
||||
```text
|
||||
You: "Find my open GitHub PR tab, summarize review state, and screenshot failing CI."
|
||||
Agent: chrome_tab(list) → chrome_snapshot(...) → ctx_execute_file(capture) → chrome_screenshot(...)
|
||||
Agent: chrome_tab(list) → chrome_snapshot(...) → overflow_query(jsonPath) → chrome_screenshot(...)
|
||||
✓ 3 reviewers, 1 change requested, CI red on iOS. Saved → .pi/chrome-screenshots/ci.png
|
||||
You: [keeps coding — agent never asked you to log in]
|
||||
```
|
||||
@@ -74,17 +74,11 @@ Second doctor run should show all checks passing.
|
||||
|
||||
Tool parameters and gotchas are documented inline in Pi.
|
||||
|
||||
### Session-local snapshots and Context Mode
|
||||
### Snapshots and pi-overflow
|
||||
|
||||
In this maintained build, `chrome_snapshot`, `chrome_find`, and actions with `includeSnapshot=true` keep the full structured page snapshot out of the normal tool result. They atomically save only the latest capture for each target under:
|
||||
In this maintained build, `chrome_snapshot` and actions with `includeSnapshot=true` return the full structured page snapshot as JSON directly in the tool result. `chrome_find` returns only up to 20 compact ranked matches.
|
||||
|
||||
```text
|
||||
.pi/chrome-context/<session-hash>/<target-hash>/snapshot.json
|
||||
```
|
||||
|
||||
The tool returns a workspace-relative path plus bounded metadata. Analyze that path immediately with `ctx_execute_file` and print only the uids, text, or state needed for the next browser action. Do **not** use `read` or `cat` on the capture, because that would put the raw page back into model context.
|
||||
|
||||
Captures are deliberately ephemeral: a successful page interaction invalidates the old target capture, and session shutdown, `/chrome revoke`, or authorization expiry removes the session files. `chrome_find` still returns up to 20 compact ranked matches for direct use while storing the full capture privately.
|
||||
Large snapshots are bounded by the bundle's `pi-overflow` extension: the result keeps a bounded view and the full original is stored, marked with `[pi-overflow id=…]`. Extract the uids, text, or state you need with `overflow_query` — a `jsonPath` such as `elements[0].uid` or `summary.focused`, or a regex `search` — instead of taking another snapshot. Any page interaction makes earlier snapshots stale, so take a fresh one (or pass `includeSnapshot=true` on the action) when the page has changed.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -15,12 +15,9 @@
|
||||
|
||||
导入后的本地改动:
|
||||
|
||||
- Chrome 快照以原子方式写入当前项目 `.pi/chrome-context/` 目录下的 owner-only、Session 临时文件,而不是把原始页面结构返回给模型上下文。
|
||||
- 每个 Session/目标只保留最新快照;浏览器交互会使陈旧捕获失效,Session 关闭时移除该 Session 目录。
|
||||
- `chrome_find` 返回紧凑的排序匹配,同时把完整结构化快照保留在 Session 本地捕获中,供可选的 `ctx_execute_file` 分析使用。
|
||||
- click/type/fill/key 上的 `includeSnapshot=true` 会保存动作后的快照并返回其捕获路径,而不是把快照嵌入工具结果。
|
||||
- Chrome 快照与 `includeSnapshot=true` 的动作结果把快照 JSON 直接放入工具文本返回,`details` 只保留元素数/匹配数等小型元数据;展示截断由组合包 pi-overflow 统一负责(原文落盘 + `overflow_query` 查询)。`chrome_find` 只返回紧凑排序匹配,不保存文件。早期版本曾把快照原子写入 `.pi/chrome-context/` 临时文件并引导 `ctx_execute_file` 分析;该 Session 临时文件机制已随 context-mode 一起移除,各项目中残留的 `.pi/chrome-context/` 目录不再创建,可手动删除。
|
||||
- 授权后的 `before_agent_start` 不再返回 `systemPrompt`:Chrome 使用提示写入 `event.systemPromptOptions.sections.chrome_profile_bridge` 结构化分区,由组合包单一提示词出口 `extensions/system-prompt.ts` 统一组装,顺序由 `config/prompt/sections.jsonc` 控制;未授权时不贡献任何提示词内容。
|
||||
- 移除 `PI_CHROME_BRIDGE_HOST`/`PI_CHROME_BRIDGE_PORT` 环境变量,host/port 固定为 `127.0.0.1:17318`(Companion 扩展 `service_worker.js` 同样硬编码该地址,单侧可配只会断连且非回环 host 有安全风险)。
|
||||
- 移除 `MAX_TEXT_CHARS`/`truncateText` 的 30K 字符文本截断:chrome_* 工具返回完整文本,超长结果的展示截断由 pi-overflow 统一负责;`MAX_ELEMENTS` 等条数上限保留不变。
|
||||
|
||||
同步上游时,请以导入 commit 为基准比较并选择性移植改动。不得整体覆盖这些本地生命周期、隐私和 Context Mode 集成。
|
||||
同步上游时,请以导入 commit 为基准比较并选择性移植改动。不得整体覆盖这些本地生命周期和组合包集成改动。
|
||||
|
||||
@@ -4,11 +4,6 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import {
|
||||
SessionSnapshotStore,
|
||||
type SnapshotCaptureMetadata,
|
||||
type SnapshotTarget,
|
||||
} from "../../src/session-snapshot-store.ts";
|
||||
|
||||
/**
|
||||
* Existing-profile Chrome bridge for pi.
|
||||
@@ -89,13 +84,28 @@ function snapshotFromIncludedResult(raw: unknown): unknown {
|
||||
return raw && typeof raw === "object" ? (raw as { snapshot?: unknown }).snapshot : undefined;
|
||||
}
|
||||
|
||||
export function formatSnapshotCapture(metadata: SnapshotCaptureMetadata): string {
|
||||
const kib = Math.max(1, Math.ceil(metadata.bytes / 1024));
|
||||
return [
|
||||
`Chrome snapshot saved to ${metadata.path} (${kib} KiB, ${metadata.elementCount} elements${metadata.matchCount ? `, ${metadata.matchCount} matches` : ""}).`,
|
||||
"Analyze it immediately with ctx_execute_file; FILE_CONTENT is JSON and the page data is under .snapshot.",
|
||||
"This capture is replaced by the next snapshot for the same target and becomes stale after page interaction.",
|
||||
].join("\n");
|
||||
/**
|
||||
* Small, session-file-safe metadata describing a returned snapshot. The full
|
||||
* snapshot goes to the model as text — details must stay small because Pi
|
||||
* persists them in the session file.
|
||||
*/
|
||||
export function snapshotStats(snapshot: unknown): { elementCount: number; matchCount: number } {
|
||||
const object = snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)
|
||||
? snapshot as { elements?: unknown; matches?: unknown }
|
||||
: {};
|
||||
return {
|
||||
elementCount: Array.isArray(object.elements) ? object.elements.length : 0,
|
||||
matchCount: Array.isArray(object.matches) ? object.matches.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action summary followed by the inline snapshot JSON. Oversized results are
|
||||
* bounded by pi-overflow and recoverable through overflow_query, so no local
|
||||
* capture file is written.
|
||||
*/
|
||||
export function formatActionResultText(text: string, snapshot: unknown | undefined): string {
|
||||
return snapshot === undefined ? text : `${text}\n\n${safeJson(snapshot)}`;
|
||||
}
|
||||
|
||||
export function formatChromeFindMatches(snapshot: any): string {
|
||||
@@ -642,7 +652,6 @@ export default function (pi: ExtensionAPI): void {
|
||||
let countdownInterval: NodeJS.Timeout | undefined;
|
||||
// Remembered so bridge sends can tag tabs with this session's group even when ctx isn't handy.
|
||||
let sessionCtx: ExtensionContext | undefined;
|
||||
let snapshotStore: SessionSnapshotStore | undefined;
|
||||
|
||||
const clearAuthExpiryTimer = (): void => {
|
||||
if (!authExpiryTimer) return;
|
||||
@@ -716,7 +725,6 @@ export default function (pi: ExtensionAPI): void {
|
||||
persistAuth();
|
||||
// Revoking control ends pi-chrome's automation for this session; tidy up the target we own.
|
||||
cleanupAutomationTargetBestEffort();
|
||||
void snapshotStore?.clear().catch(() => undefined);
|
||||
};
|
||||
|
||||
const authSummary = (): string => {
|
||||
@@ -847,45 +855,22 @@ export default function (pi: ExtensionAPI): void {
|
||||
return { ...params, foreground: !background } as T;
|
||||
};
|
||||
|
||||
const snapshotTargetFrom = (params: Record<string, unknown>): SnapshotTarget => ({
|
||||
targetId: typeof params.targetId === "string" ? params.targetId : undefined,
|
||||
urlIncludes: typeof params.urlIncludes === "string" ? params.urlIncludes : undefined,
|
||||
titleIncludes: typeof params.titleIncludes === "string" ? params.titleIncludes : undefined,
|
||||
});
|
||||
|
||||
const saveSnapshotCapture = async (snapshot: unknown, params: Record<string, unknown>): Promise<SnapshotCaptureMetadata> => {
|
||||
if (!snapshotStore) throw new Error("Chrome snapshot storage is unavailable before session_start.");
|
||||
const stored = await snapshotStore.save(snapshot, snapshotTargetFrom(params));
|
||||
return stored.metadata;
|
||||
};
|
||||
|
||||
const invalidateSnapshotCapture = async (params?: Record<string, unknown>): Promise<void> => {
|
||||
if (!snapshotStore) return;
|
||||
if (!params) await snapshotStore.clear();
|
||||
else await snapshotStore.invalidate(snapshotTargetFrom(params));
|
||||
};
|
||||
|
||||
const formatActionWithCapture = async (raw: unknown, params: Record<string, unknown>, text: string): Promise<ToolTextResult> => {
|
||||
const formatActionResult = (raw: unknown, params: Record<string, unknown>, text: string): ToolTextResult => {
|
||||
const snapshot = snapshotFromIncludedResult(raw);
|
||||
const result = params.includeSnapshot && raw && typeof raw === "object"
|
||||
? (raw as { result?: unknown }).result
|
||||
: raw;
|
||||
if (!snapshot) {
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text }], details: { result: result as Json } };
|
||||
}
|
||||
const capture = await saveSnapshotCapture(snapshot, params);
|
||||
return {
|
||||
content: [{ type: "text", text: `${text}\n\n${formatSnapshotCapture(capture)}` }],
|
||||
details: { result: result as Json, capture },
|
||||
content: [{ type: "text", text: formatActionResultText(text, snapshot) }],
|
||||
details: { result: result as Json, snapshot: snapshotStats(snapshot) },
|
||||
};
|
||||
};
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
sessionCtx = ctx;
|
||||
const sessionId = ctx.sessionManager.getSessionId?.() || "unknown-session";
|
||||
snapshotStore = new SessionSnapshotStore(workspaceCwd(ctx), sessionId);
|
||||
await snapshotStore.clear();
|
||||
await bridge.start();
|
||||
// Reestablish in-memory state after a /reload restored chromeAuthorizedUntil from globalThis.
|
||||
if (chromeControlAuthorized()) {
|
||||
@@ -903,10 +888,6 @@ export default function (pi: ExtensionAPI): void {
|
||||
pi.on("session_shutdown", async (event) => {
|
||||
clearAuthExpiryTimer();
|
||||
clearCountdownInterval();
|
||||
// Snapshot files are intentionally ephemeral. A reload must take a fresh observation too,
|
||||
// because the browser page may have changed while the extension was reloading.
|
||||
await snapshotStore?.clear().catch(() => undefined);
|
||||
snapshotStore = undefined;
|
||||
// Keep the owned automation target across /reload, but clean it up on real session end.
|
||||
if (event?.reason !== "reload") cleanupAutomationTargetBestEffort();
|
||||
bridge.stop();
|
||||
@@ -933,12 +914,12 @@ Capability model (important):
|
||||
- Interactive controls (click/type/fill/key/hover/drag/scroll/tap) use Chrome's real input layer via chrome.debugger / CDP. Events satisfy normal user-activation gates.
|
||||
- Input bypasses page CSP because it is injected at browser input layer, not page JavaScript. Chrome may show the “Pi Chrome Connector started debugging this browser” banner while attached.
|
||||
- \`chrome_evaluate\` and \`chrome_snapshot\` run in MAIN world via **CDP \`Runtime.evaluate\`**, which is not subject to the page's Content-Security-Policy. They work even on strict-CSP pages (e.g. github.com, many bank/SaaS apps) that block \`'unsafe-eval'\`. \`chrome_navigate initScript\` likewise injects at document_start via CDP and bypasses CSP. \`chrome_screenshot\`, \`chrome_tab\`, and Chrome input also work under any CSP.
|
||||
- \`chrome_snapshot\` and snapshots requested with \`includeSnapshot=true\` are written to a private session-local JSON capture; raw page data is never returned inline. The tool result gives the path for immediate \`ctx_execute_file\` analysis.
|
||||
- \`chrome_snapshot\` and snapshots requested with \`includeSnapshot=true\` return the page snapshot as JSON directly in the tool result. When the result is large it carries a \`[pi-overflow id=…]\` marker — extract only the uids, text, or state you need with \`overflow_query\` (jsonPath such as \`elements[0].uid\` or \`summary.focused\`, or a \`search\`) instead of taking another snapshot.
|
||||
|
||||
Usage rules:
|
||||
1. If a chrome_* tool says Chrome control is locked, ask the user to run \`/chrome authorize\` before retrying.
|
||||
2. Call \`chrome_snapshot\`, then immediately call \`ctx_execute_file\` on the returned path and print only the uids/state needed for the next action. Never use read/cat on the full capture.
|
||||
3. Prefer \`uid\` over \`selector\`. After any page interaction, treat the old capture as stale. Use \`includeSnapshot=true\` when a fresh post-action capture is needed.
|
||||
2. Call \`chrome_snapshot\` and read the returned JSON (or query a pi-overflow marker with \`overflow_query\`); keep only the uids/state needed for the next action.
|
||||
3. Prefer \`uid\` over \`selector\`. After any page interaction, earlier snapshots are stale — take a fresh one (or pass \`includeSnapshot=true\` on the action) instead of reusing old uids.
|
||||
4. If \`chrome_evaluate\` returns null when you expected a value, the expression evaluated to null/undefined in the page; surface the value via \`JSON.stringify\` to confirm.
|
||||
5. \`chrome_navigate\` supports an optional \`initScript\` that runs at document_start in MAIN world for the next navigation (good for seeding localStorage or stubbing Date.now).
|
||||
6. By default chrome_* tools run in the background without focusing Chrome; pass \`background=false\` or run /chrome background off when the user wants to watch Chrome work.
|
||||
@@ -1331,7 +1312,6 @@ Usage rules:
|
||||
const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.group?.title ? `[${tab.group.title}] ` : ""}${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
|
||||
return { content: [{ type: "text", text }], details: { tabs } };
|
||||
}
|
||||
if (params.action !== "version") await invalidateSnapshotCapture();
|
||||
return { content: [{ type: "text", text: safeJson(result) }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1340,8 +1320,8 @@ Usage rules:
|
||||
name: "chrome_snapshot",
|
||||
label: "Chrome Snapshot",
|
||||
description:
|
||||
"Capture the current Chrome page into a private, session-local JSON file instead of placing the raw DOM snapshot in model context. The result returns a path for immediate ctx_execute_file analysis. Each target keeps only its latest capture; page interaction makes it stale. Use mode/query/nearUid to control what Chrome captures. Runs in the background by default; pass background=false so the user can watch.",
|
||||
promptSnippet: "Save the current Chrome page to a session-local JSON capture, then analyze that path with ctx_execute_file for only the required uids and state.",
|
||||
"Capture the current Chrome page and return the snapshot as JSON directly in the result. When it is large the result carries a [pi-overflow id=…] marker — extract only the uids, text, or state you need with overflow_query (jsonPath or search). Page interaction makes a snapshot stale; take a fresh one after navigating or acting. Use mode/query/nearUid to control what Chrome captures. Runs in the background by default; pass background=false so the user can watch.",
|
||||
promptSnippet: "Capture the current Chrome page and return its JSON snapshot; query large results via overflow_query.",
|
||||
parameters: Type.Object({
|
||||
targetId: Type.Optional(Type.String()),
|
||||
urlIncludes: Type.Optional(Type.String()),
|
||||
@@ -1366,8 +1346,7 @@ Usage rules:
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
signal,
|
||||
);
|
||||
const capture = await saveSnapshotCapture(snapshot, params);
|
||||
return { content: [{ type: "text", text: formatSnapshotCapture(capture) }], details: { capture } };
|
||||
return { content: [{ type: "text", text: safeJson(snapshot) }], details: { snapshot: snapshotStats(snapshot) } };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1375,8 +1354,8 @@ Usage rules:
|
||||
name: "chrome_find",
|
||||
label: "Chrome Find",
|
||||
description:
|
||||
"Find elements, regions, or text on the live Chrome page. Returns only compact ranked matches with stable uids and coordinates; the full structured snapshot is saved to the same private session-local file used by chrome_snapshot for optional ctx_execute_file analysis.",
|
||||
promptSnippet: "Find live Chrome controls/text/regions and return compact stable-uid matches while keeping the full snapshot out of model context.",
|
||||
"Find elements, regions, or text on the live Chrome page. Returns only compact ranked matches with stable uids and coordinates.",
|
||||
promptSnippet: "Find live Chrome controls/text/regions and return compact stable-uid matches.",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "What to find, e.g. 'merge button', 'email error', 'approve PR', 'search box'." }),
|
||||
mode: Type.Optional(StringEnum(snapshotModeValues)),
|
||||
@@ -1397,10 +1376,9 @@ Usage rules:
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
signal,
|
||||
);
|
||||
const capture = await saveSnapshotCapture(snapshot, params);
|
||||
return {
|
||||
content: [{ type: "text", text: `${formatChromeFindMatches(snapshot)}\n\n${formatSnapshotCapture(capture)}` }],
|
||||
details: { capture },
|
||||
content: [{ type: "text", text: formatChromeFindMatches(snapshot) }],
|
||||
details: { snapshot: snapshotStats(snapshot) },
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1446,9 +1424,8 @@ Usage rules:
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
signal,
|
||||
);
|
||||
const capture = await saveSnapshotCapture(snapshot, params);
|
||||
const text = `chrome_inspect fallback: loaded Chrome extension does not yet support page.inspect; reload it at chrome://extensions for deep inspect.\n\n${formatSnapshotCapture(capture)}`;
|
||||
return { content: [{ type: "text", text }], details: { capture, fallback: "page.snapshot" } };
|
||||
const text = `chrome_inspect fallback: loaded Chrome extension does not yet support page.inspect; reload it at chrome://extensions for deep inspect.\n\n${safeJson(snapshot)}`;
|
||||
return { content: [{ type: "text", text }], details: { snapshot: snapshotStats(snapshot), fallback: "page.snapshot" } };
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1475,7 +1452,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.navigate", withBackground(params), (params.timeoutMs ?? 15_000) + 2_000, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Navigated to ${params.url}${params.initScript ? " (with initScript)" : ""}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1500,7 +1476,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const value = await authorizedBridgeSend("page.evaluate", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
const text = value === undefined
|
||||
? "undefined"
|
||||
: typeof value === "string"
|
||||
@@ -1514,7 +1489,7 @@ Usage rules:
|
||||
name: "chrome_click",
|
||||
label: "Chrome Click",
|
||||
description:
|
||||
"Click a snapshot uid, CSS selector, or viewport coordinate using Chrome's real input layer. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-click snapshot to the session-local file and return its ctx_execute_file path, never the raw snapshot.",
|
||||
"Click a snapshot uid, CSS selector, or viewport coordinate using Chrome's real input layer. Earlier snapshots become stale after the click. Pass includeSnapshot=true to get a fresh post-click snapshot JSON inline.",
|
||||
promptSnippet: "Click page elements in Chrome by snapshot uid, selector, or viewport coordinate.",
|
||||
parameters: Type.Object({
|
||||
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot. Prefer uid over selector after taking a snapshot." })),
|
||||
@@ -1522,7 +1497,7 @@ Usage rules:
|
||||
x: Type.Optional(Type.Number({ description: "Viewport x coordinate if uid/selector is omitted." })),
|
||||
y: Type.Optional(Type.Number({ description: "Viewport y coordinate if uid/selector is omitted." })),
|
||||
domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM-dispatched click if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-click snapshot to the session-local capture file and return its path." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-click snapshot JSON inline after the action summary." })),
|
||||
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
urlIncludes: Type.Optional(Type.String()),
|
||||
@@ -1539,7 +1514,7 @@ Usage rules:
|
||||
const summary = summarizeActionResult(result);
|
||||
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
||||
const text = summary ? `Clicked ${target} — ${summary}` : `Clicked ${target}`;
|
||||
return formatActionWithCapture(raw, params, text);
|
||||
return formatActionResult(raw, params, text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1547,13 +1522,13 @@ Usage rules:
|
||||
name: "chrome_type",
|
||||
label: "Chrome Type",
|
||||
description:
|
||||
"Focus an optional snapshot uid or CSS selector, then type text using Chrome's real keyboard input. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-type snapshot and return its ctx_execute_file path.",
|
||||
"Focus an optional snapshot uid or CSS selector, then type text using Chrome's real keyboard input. Earlier snapshots become stale after typing. Pass includeSnapshot=true to get a fresh post-type snapshot JSON inline.",
|
||||
promptSnippet: "Type text into Chrome, optionally focusing a snapshot uid or selector first.",
|
||||
parameters: Type.Object({
|
||||
text: Type.String(),
|
||||
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot." })),
|
||||
selector: Type.Optional(Type.String({ description: "CSS selector to focus before typing." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-type snapshot to the session-local capture file and return its path." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-type snapshot JSON inline after the action summary." })),
|
||||
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
|
||||
pressEnter: Type.Optional(Type.Boolean()),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
@@ -1572,7 +1547,7 @@ Usage rules:
|
||||
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
||||
const base = `Typed ${params.text.length} character(s)${into}.`;
|
||||
const text = summary ? `${base} (${summary})` : base;
|
||||
return formatActionWithCapture(raw, params, text);
|
||||
return formatActionResult(raw, params, text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1580,7 +1555,7 @@ Usage rules:
|
||||
name: "chrome_fill",
|
||||
label: "Chrome Fill",
|
||||
description:
|
||||
"Set the full value of a text input, textarea, or contenteditable element using Chrome input. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-fill snapshot and return its ctx_execute_file path.",
|
||||
"Set the full value of a text input, textarea, or contenteditable element using Chrome input. Earlier snapshots become stale after filling. Pass includeSnapshot=true to get a fresh post-fill snapshot JSON inline.",
|
||||
promptSnippet: "Fill a Chrome form field by snapshot uid or selector, optionally returning a fresh snapshot.",
|
||||
parameters: Type.Object({
|
||||
text: Type.String(),
|
||||
@@ -1588,7 +1563,7 @@ Usage rules:
|
||||
selector: Type.Optional(Type.String({ description: "CSS selector to fill if uid is omitted." })),
|
||||
submit: Type.Optional(Type.Boolean({ description: "If true, press Enter after filling." })),
|
||||
domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM value-setting if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-fill snapshot to the session-local capture file and return its path." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-fill snapshot JSON inline after the action summary." })),
|
||||
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
urlIncludes: Type.Optional(Type.String()),
|
||||
@@ -1606,7 +1581,7 @@ Usage rules:
|
||||
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
||||
const base = `Filled ${params.text.length} character(s)${into}.`;
|
||||
const text = summary ? `${base} (${summary})` : base;
|
||||
return formatActionWithCapture(raw, params, text);
|
||||
return formatActionResult(raw, params, text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1614,7 +1589,7 @@ Usage rules:
|
||||
name: "chrome_key",
|
||||
label: "Chrome Key",
|
||||
description:
|
||||
"Send a keyboard key to an existing Chrome tab. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-key snapshot and return its ctx_execute_file path. Runs in the background by default; pass background=false so the user can watch.",
|
||||
"Send a keyboard key to an existing Chrome tab. Earlier snapshots become stale after the key press. Pass includeSnapshot=true to get a fresh post-key snapshot JSON inline. Runs in the background by default; pass background=false so the user can watch.",
|
||||
promptSnippet: "Press keys in Chrome through the companion extension.",
|
||||
parameters: Type.Object({
|
||||
key: Type.String(),
|
||||
@@ -1624,7 +1599,7 @@ Usage rules:
|
||||
altKey: Type.Optional(Type.Boolean()),
|
||||
metaKey: Type.Optional(Type.Boolean()),
|
||||
}, { description: "Modifier keys to hold while pressing the key (chord)." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-key snapshot to the session-local capture file and return its path." })),
|
||||
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, return a fresh post-key snapshot JSON inline after the action summary." })),
|
||||
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
|
||||
targetId: Type.Optional(Type.String()),
|
||||
urlIncludes: Type.Optional(Type.String()),
|
||||
@@ -1641,7 +1616,7 @@ Usage rules:
|
||||
const summary = summarizeActionResult(result);
|
||||
const base = `Pressed ${params.key}.`;
|
||||
const text = summary ? `${base} (${summary})` : base;
|
||||
return formatActionWithCapture(raw, params, text);
|
||||
return formatActionResult(raw, params, text);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1663,7 +1638,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.waitFor", params, (params.timeoutMs ?? 10_000) + 2_000, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Observed ${params.kind}: ${params.value}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1807,7 +1781,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Hovered ${params.uid ?? params.selector ?? `${params.x},${params.y}`}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1834,7 +1807,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Dragged from ${params.fromUid ?? params.fromSelector} to ${params.toUid ?? params.toSelector}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1857,7 +1829,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
||||
return { content: [{ type: "text", text: `Tapped ${target} (touch)` }], details: { result: result as Json } };
|
||||
},
|
||||
@@ -1881,7 +1852,6 @@ Usage rules:
|
||||
}),
|
||||
async execute(_id, params, signal): Promise<ToolTextResult> {
|
||||
const result = await authorizedBridgeSend("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Scrolled dy=${params.deltaY ?? 0} dx=${params.deltaX ?? 0}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
@@ -1904,7 +1874,6 @@ Usage rules:
|
||||
const cwd = workspaceCwd(ctx);
|
||||
const paths = params.paths.map((p) => resolve(cwd, p));
|
||||
const result = await authorizedBridgeSend("page.upload", withBackground({ ...params, paths }), DEFAULT_TIMEOUT_MS, signal);
|
||||
await invalidateSnapshotCapture(params);
|
||||
return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` }], details: { result: result as Json } };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "pi-chrome",
|
||||
"version": "0.15.46",
|
||||
"scripts": {
|
||||
"test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && jiti test/session-snapshot-store.test.ts",
|
||||
"test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && jiti test/snapshot-result.test.ts",
|
||||
"version": "node scripts/sync-manifest-version.js",
|
||||
"prepublishOnly": "node scripts/sync-manifest-version.js"
|
||||
},
|
||||
@@ -52,7 +52,6 @@
|
||||
"type": "commonjs",
|
||||
"files": [
|
||||
"extensions",
|
||||
"src",
|
||||
"test",
|
||||
"docs",
|
||||
"test-suite",
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { join, relative, resolve, sep } from "node:path";
|
||||
|
||||
type SnapshotObject = Record<string, unknown>;
|
||||
|
||||
export type SnapshotTarget = {
|
||||
targetId?: string;
|
||||
urlIncludes?: string;
|
||||
titleIncludes?: string;
|
||||
};
|
||||
|
||||
export type SnapshotCaptureMetadata = {
|
||||
version: 1;
|
||||
captureId: string;
|
||||
capturedAt: string;
|
||||
path: string;
|
||||
bytes: number;
|
||||
targetKey: string;
|
||||
targetId?: string | number;
|
||||
mode?: string;
|
||||
elementCount: number;
|
||||
matchCount: number;
|
||||
staleAfterInteraction: true;
|
||||
};
|
||||
|
||||
export type StoredSnapshotCapture = {
|
||||
metadata: SnapshotCaptureMetadata;
|
||||
absolutePath: string;
|
||||
};
|
||||
|
||||
export type SnapshotCaptureFile = {
|
||||
version: 1;
|
||||
captureId: string;
|
||||
capturedAt: string;
|
||||
targetKey: string;
|
||||
snapshot: unknown;
|
||||
};
|
||||
|
||||
function hashSegment(value: string): string {
|
||||
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
||||
}
|
||||
|
||||
function normalizeRelativePath(path: string): string {
|
||||
return path.split(sep).join("/");
|
||||
}
|
||||
|
||||
function snapshotObject(snapshot: unknown): SnapshotObject | undefined {
|
||||
return snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)
|
||||
? snapshot as SnapshotObject
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function snapshotTargetKey(target: SnapshotTarget = {}): string {
|
||||
if (target.targetId) return `target:${target.targetId}`;
|
||||
if (target.urlIncludes) return `url:${target.urlIncludes}`;
|
||||
if (target.titleIncludes) return `title:${target.titleIncludes}`;
|
||||
return "default";
|
||||
}
|
||||
|
||||
export class SessionSnapshotStore {
|
||||
readonly workspaceRoot: string;
|
||||
readonly sessionRoot: string;
|
||||
readonly sessionHash: string;
|
||||
|
||||
constructor(workspaceRoot: string, sessionId: string) {
|
||||
this.workspaceRoot = resolve(workspaceRoot);
|
||||
this.sessionHash = hashSegment(sessionId || "unknown-session");
|
||||
this.sessionRoot = join(this.workspaceRoot, ".pi", "chrome-context", this.sessionHash);
|
||||
}
|
||||
|
||||
async save(snapshot: unknown, target: SnapshotTarget = {}): Promise<StoredSnapshotCapture> {
|
||||
const targetKey = snapshotTargetKey(target);
|
||||
const targetDirectory = join(this.sessionRoot, hashSegment(targetKey));
|
||||
const absolutePath = join(targetDirectory, "snapshot.json");
|
||||
const temporaryPath = join(targetDirectory, `.snapshot.${process.pid}.${randomUUID()}.tmp`);
|
||||
const captureId = randomUUID();
|
||||
const capturedAt = new Date().toISOString();
|
||||
const capture: SnapshotCaptureFile = {
|
||||
version: 1,
|
||||
captureId,
|
||||
capturedAt,
|
||||
targetKey,
|
||||
snapshot,
|
||||
};
|
||||
const serialized = `${JSON.stringify(capture, null, 2)}\n`;
|
||||
|
||||
await mkdir(targetDirectory, { recursive: true, mode: 0o700 });
|
||||
await chmod(this.sessionRoot, 0o700).catch(() => undefined);
|
||||
await chmod(targetDirectory, 0o700).catch(() => undefined);
|
||||
try {
|
||||
await writeFile(temporaryPath, serialized, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
||||
await chmod(temporaryPath, 0o600).catch(() => undefined);
|
||||
await rename(temporaryPath, absolutePath);
|
||||
await chmod(absolutePath, 0o600).catch(() => undefined);
|
||||
} catch (error) {
|
||||
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const object = snapshotObject(snapshot);
|
||||
const elements = Array.isArray(object?.elements) ? object.elements : [];
|
||||
const matches = Array.isArray(object?.matches) ? object.matches : [];
|
||||
const targetId = typeof object?.targetId === "string" || typeof object?.targetId === "number"
|
||||
? object.targetId
|
||||
: undefined;
|
||||
const mode = typeof object?.mode === "string" ? object.mode : undefined;
|
||||
const path = normalizeRelativePath(relative(this.workspaceRoot, absolutePath));
|
||||
|
||||
return {
|
||||
absolutePath,
|
||||
metadata: {
|
||||
version: 1,
|
||||
captureId,
|
||||
capturedAt,
|
||||
path,
|
||||
bytes: Buffer.byteLength(serialized),
|
||||
targetKey,
|
||||
targetId,
|
||||
mode,
|
||||
elementCount: elements.length,
|
||||
matchCount: matches.length,
|
||||
staleAfterInteraction: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async invalidate(target?: SnapshotTarget): Promise<void> {
|
||||
if (!target) {
|
||||
await this.clear();
|
||||
return;
|
||||
}
|
||||
const targetDirectory = join(this.sessionRoot, hashSegment(snapshotTargetKey(target)));
|
||||
await rm(targetDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await rm(this.sessionRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { SessionSnapshotStore, snapshotTargetKey } from "../src/session-snapshot-store.ts";
|
||||
import { CHROME_TOOL_NAMES, formatChromeFindMatches, formatSnapshotCapture } from "../extensions/chrome-profile-bridge/index.ts";
|
||||
|
||||
test("snapshotTargetKey prefers explicit target selectors", () => {
|
||||
assert.equal(snapshotTargetKey(), "default");
|
||||
assert.equal(snapshotTargetKey({ titleIncludes: "Inbox" }), "title:Inbox");
|
||||
assert.equal(snapshotTargetKey({ urlIncludes: "example.com" }), "url:example.com");
|
||||
assert.equal(snapshotTargetKey({ targetId: "42", urlIncludes: "ignored" }), "target:42");
|
||||
});
|
||||
|
||||
test("SessionSnapshotStore atomically keeps only the latest capture per target", async () => {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "pi-chrome-snapshot-"));
|
||||
const store = new SessionSnapshotStore(workspace, "session/private/id");
|
||||
const first = await store.save({ mode: "interactive", targetId: 42, elements: [{ uid: "el-1" }], matches: [] });
|
||||
const second = await store.save({ mode: "changes", targetId: 42, elements: [{ uid: "el-2" }], matches: [{ uid: "el-2" }] });
|
||||
|
||||
assert.equal(first.metadata.path, second.metadata.path);
|
||||
assert.notEqual(first.metadata.captureId, second.metadata.captureId);
|
||||
assert.equal(second.metadata.elementCount, 1);
|
||||
assert.equal(second.metadata.matchCount, 1);
|
||||
assert.equal(second.metadata.targetId, 42);
|
||||
assert.ok(!second.metadata.path.includes("private"));
|
||||
|
||||
const stored = JSON.parse(await readFile(second.absolutePath, "utf8"));
|
||||
assert.equal(stored.version, 1);
|
||||
assert.equal(stored.captureId, second.metadata.captureId);
|
||||
assert.equal(stored.snapshot.elements[0].uid, "el-2");
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
assert.equal((await stat(store.sessionRoot)).mode & 0o777, 0o700);
|
||||
assert.equal((await stat(second.absolutePath)).mode & 0o777, 0o600);
|
||||
}
|
||||
|
||||
await store.clear();
|
||||
await assert.rejects(stat(second.absolutePath), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("target invalidation and session cleanup do not affect another session", async () => {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "pi-chrome-isolation-"));
|
||||
const firstSession = new SessionSnapshotStore(workspace, "session-a");
|
||||
const secondSession = new SessionSnapshotStore(workspace, "session-b");
|
||||
const firstTarget = { targetId: "101" };
|
||||
const secondTarget = { targetId: "202" };
|
||||
const first = await firstSession.save({ elements: [] }, firstTarget);
|
||||
const otherTarget = await firstSession.save({ elements: [] }, secondTarget);
|
||||
const otherSession = await secondSession.save({ elements: [] }, firstTarget);
|
||||
|
||||
await firstSession.invalidate(firstTarget);
|
||||
await assert.rejects(stat(first.absolutePath), { code: "ENOENT" });
|
||||
await stat(otherTarget.absolutePath);
|
||||
await stat(otherSession.absolutePath);
|
||||
|
||||
await firstSession.clear();
|
||||
await assert.rejects(stat(otherTarget.absolutePath), { code: "ENOENT" });
|
||||
await stat(otherSession.absolutePath);
|
||||
await secondSession.clear();
|
||||
});
|
||||
|
||||
test("snapshot result formatting stays bounded and points to Context Mode", () => {
|
||||
const text = formatSnapshotCapture({
|
||||
version: 1,
|
||||
captureId: "capture-1",
|
||||
capturedAt: "2026-08-28T00:00:00.000Z",
|
||||
path: ".pi/chrome-context/session/target/snapshot.json",
|
||||
bytes: 80_000,
|
||||
targetKey: "target:42",
|
||||
targetId: 42,
|
||||
mode: "interactive",
|
||||
elementCount: 80,
|
||||
matchCount: 3,
|
||||
staleAfterInteraction: true,
|
||||
});
|
||||
|
||||
assert.match(text, /ctx_execute_file/);
|
||||
assert.match(text, /\.pi\/chrome-context\/session\/target\/snapshot\.json/);
|
||||
assert.doesNotMatch(text, /raw-secret-page-content/);
|
||||
});
|
||||
|
||||
test("chrome_find formatting is capped and authorization tracks all 21 tools", () => {
|
||||
const matches = Array.from({ length: 25 }, (_, index) => ({
|
||||
kind: "element",
|
||||
uid: `el-${index}`,
|
||||
role: "button",
|
||||
label: `Button ${index}`,
|
||||
rect: { x: index, y: index, width: 10, height: 10 },
|
||||
}));
|
||||
const text = formatChromeFindMatches({ query: "button", matches });
|
||||
|
||||
assert.match(text, /el-0/);
|
||||
assert.match(text, /el-19/);
|
||||
assert.doesNotMatch(text, /el-20/);
|
||||
assert.equal(CHROME_TOOL_NAMES.length, 21);
|
||||
assert.ok(CHROME_TOOL_NAMES.includes("chrome_find"));
|
||||
assert.ok(CHROME_TOOL_NAMES.includes("chrome_inspect"));
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
CHROME_TOOL_NAMES,
|
||||
formatActionResultText,
|
||||
formatChromeFindMatches,
|
||||
snapshotStats,
|
||||
} from "../extensions/chrome-profile-bridge/index.ts";
|
||||
|
||||
test("formatActionResultText appends the inline snapshot JSON", () => {
|
||||
const snapshot = { elements: [{ uid: "el-1", role: "button", label: "Save" }], matches: [] };
|
||||
const text = formatActionResultText("Clicked el-1", snapshot);
|
||||
const [summary, json] = text.split("\n\n");
|
||||
assert.equal(summary, "Clicked el-1");
|
||||
assert.deepEqual(JSON.parse(json), snapshot);
|
||||
});
|
||||
|
||||
test("formatActionResultText returns the summary alone without a snapshot", () => {
|
||||
assert.equal(formatActionResultText("Clicked el-1", undefined), "Clicked el-1");
|
||||
});
|
||||
|
||||
test("snapshotStats stays small: only element and match counts", () => {
|
||||
assert.deepEqual(
|
||||
snapshotStats({ elements: [{ uid: "a" }, { uid: "b" }], matches: [{ uid: "a" }] }),
|
||||
{ elementCount: 2, matchCount: 1 },
|
||||
);
|
||||
assert.deepEqual(snapshotStats({ elements: "nope" }), { elementCount: 0, matchCount: 0 });
|
||||
assert.deepEqual(snapshotStats(undefined), { elementCount: 0, matchCount: 0 });
|
||||
assert.deepEqual(snapshotStats([1, 2, 3]), { elementCount: 0, matchCount: 0 });
|
||||
});
|
||||
|
||||
test("chrome_find formatting is capped and authorization tracks all 21 tools", () => {
|
||||
const matches = Array.from({ length: 25 }, (_, index) => ({
|
||||
kind: "element",
|
||||
uid: `el-${index}`,
|
||||
role: "button",
|
||||
label: `Button ${index}`,
|
||||
rect: { x: index, y: index, width: 10, height: 10 },
|
||||
}));
|
||||
const text = formatChromeFindMatches({ query: "button", matches });
|
||||
|
||||
assert.match(text, /el-0/);
|
||||
assert.match(text, /el-19/);
|
||||
assert.doesNotMatch(text, /el-20/);
|
||||
assert.equal(CHROME_TOOL_NAMES.length, 21);
|
||||
assert.ok(CHROME_TOOL_NAMES.includes("chrome_find"));
|
||||
assert.ok(CHROME_TOOL_NAMES.includes("chrome_inspect"));
|
||||
});
|
||||
Reference in New Issue
Block a user