pi-browser-use-lite 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  本文件记录已发布版本的用户可见变更。更细的过程见 git log。破坏性变更标 **BREAKING**。
4
4
 
5
+ ## 0.2.0 — 2026-11-09
6
+
7
+ - **BREAKING**:删掉 `prompt.guidelines`。默认三条规则搬进 `chrome_act` 自己的 `promptGuidelines`;跨工具的规则写 AGENTS.md。写了这个键的配置会以 `prompt: unknown key` 启动失败。
8
+ - `/chrome` 有参数补全:输入 `/chrome ` 列子命令,`/chrome authorize ` 列 on 与 off,每项带一句说明。
9
+ - 关掉某个工具后,系统提示里不再留下点名它的规则:此前全局规则里的三条 `chrome_act` 规则在关掉 `chrome_act` 后仍然保留。
10
+ - **BREAKING**:删掉 `policy.background` 与 `/chrome background`。点不动页面时由输入路径自己把标签与窗口带到前台;读操作照样不打扰。写了这个键的配置会以 `policy.background: unknown key` 启动失败。
11
+ - 输入前的可见性检查改成自救:不可见就激活标签、把窗口带到前台,仍不可见才报错并说清原因(标签非活动、窗口最小化、被盖住)。
12
+ - `chrome_tab new` 建的新标签在自己的窗口里是活动标签,不再出现「新标签点不动」。
13
+ - README 说清自动化窗口不聚焦地打开、第一次点击或打字会把它带到前台。
14
+
15
+ ## 0.1.1 — 2026-11-09
16
+
17
+ - 补中文 README:`README.zh-CN.md`,英文版顶部有切换链接,npm 包同时收录两份。
18
+
5
19
  ## 0.1.0 — 2026-11-08
6
20
 
7
21
  首版。
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # pi-browser-use-lite
2
2
 
3
+ [简体中文](README.zh-CN.md) | **English**
4
+
3
5
  Five `chrome_*` tools that drive your own browser: the tabs you are already logged into, with your cookies and your sessions.
4
6
 
5
7
  The package has two halves. The pi extension registers the tools and runs a local bridge on `127.0.0.1`. The companion Chrome extension polls that bridge and runs each command through `chrome.debugger`.
@@ -17,6 +19,7 @@ The package has two halves. The pi extension registers the tools and runs a loca
17
19
  Behaviour worth knowing:
18
20
 
19
21
  - Each session gets its own browser window. The tools do not touch your own tabs. The tabs the session opened are closed when the session ends.
22
+ - That window opens unfocused. The first click or keystroke raises it, because Chrome drops input to hidden pages. Reads never raise it.
20
23
  - Input goes through trusted CDP events, so pages that reject synthetic events still react.
21
24
  - Console and network capture starts at navigation, not at the first read.
22
25
  - Every failure carries a class and a `hint` that says what to do next.
@@ -39,7 +42,7 @@ Then authorize the tools for this session:
39
42
  /chrome authorize on
40
43
  ```
41
44
 
42
- Authorization ends with the session. Set `policy.authorizeOnStart` to skip this step.
45
+ Authorization ends with the session. Set `policy.authorizeOnStart` to skip this step. Type `/chrome ` and press Tab to list the subcommands.
43
46
 
44
47
  ## Configuration
45
48
 
@@ -64,8 +67,6 @@ Without the file every value is the default. A wrong type or an unknown key fail
64
67
  // Session rules
65
68
  "policy": {
66
69
  "authorizeOnStart": false, // true = the tools work without /chrome authorize on
67
- "background": true, // true = block every focusing call, chrome_tab switch included.
68
- // Turn it off when the model must bring a page to the front
69
70
  "closeOwnTabsOnExit": true // close the tabs this session opened when the session ends
70
71
  },
71
72
 
@@ -89,11 +90,6 @@ Without the file every value is the default. A wrong type or an unknown key fail
89
90
  "keepNewest": 20 // older screenshots in that directory are deleted
90
91
  },
91
92
 
92
- // Global rules for the system prompt. These replace the three defaults; they do not extend them
93
- "prompt": {
94
- "guidelines": ["chrome_act: this is the user's real browser; confirm before destructive actions."]
95
- },
96
-
97
93
  // Tool switches and names. Keep at least one enabled; names must be unique and must not shadow a host tool
98
94
  "tools": {
99
95
  "chrome_act": { "enabled": true, "name": "chrome_act" },
@@ -0,0 +1,147 @@
1
+ # pi-browser-use-lite
2
+
3
+ **[简体中文](README.zh-CN.md)** | [English](README.md)
4
+
5
+ 五个 `chrome_*` 工具,操作你自己的浏览器:你已经登录的标签页、你的 cookie 和会话。
6
+
7
+ 包分两半。pi 扩展注册这些工具,并在 `127.0.0.1` 上跑一个本地桥。配套的 Chrome 扩展轮询这个桥,用 `chrome.debugger` 执行每条命令。
8
+
9
+ ## 工具
10
+
11
+ | 工具 | 做什么 |
12
+ |---|---|
13
+ | `chrome_act` | navigate、click、type、fill、press、hover、drag、scroll、upload、eval、wait、batch |
14
+ | `chrome_see` | 快照:refs、text、changes、full;读控制台与网络日志 |
15
+ | `chrome_tab` | list、new、switch、close |
16
+ | `chrome_vision` | 截视口或整页 |
17
+ | `chrome_cdp` | 一条裸 CDP 调用,另四个盖不住的用它 |
18
+
19
+ 值得知道的行为:
20
+
21
+ - 每个会话一个独立浏览器窗口。工具不碰你自己的标签页。会话结束时关掉本会话开的标签。
22
+ - 这个窗口不聚焦地打开。第一次点击或打字会把它带到前台,因为 Chrome 丢掉不可见页面的输入。读操作不会打扰你。
23
+ - 输入走可信 CDP 事件,拒绝合成事件的页面也会响应。
24
+ - 控制台与网络抓取从导航开始,不从第一次读取开始。
25
+ - 每次失败都带类别和一句 `hint`,说明下一步做什么。
26
+
27
+ ## 安装
28
+
29
+ ```bash
30
+ pi install npm:pi-browser-use-lite
31
+ ```
32
+
33
+ 扩展就在包里。每个浏览器装一次:
34
+
35
+ 1. 在 pi 里运行 `/chrome status`。扩展没连上时,回复会打印要加载的目录。
36
+ 2. 打开 `chrome://extensions`,开开发者模式,点「加载已解压的扩展程序」,选那个目录。
37
+ 3. 再跑一次 `/chrome status`。`connected: yes` 表示桥看到了扩展。
38
+
39
+ 然后给这次会话授权:
40
+
41
+ ```
42
+ /chrome authorize on
43
+ ```
44
+
45
+ 授权随会话结束。想省这一步,把 `policy.authorizeOnStart` 设为 true。输入 `/chrome ` 再按 Tab 列出子命令。
46
+
47
+ ## 配置
48
+
49
+ 一个 JSON 文件:`<agent dir>/pi-browser-use-lite/config.json`。agent dir 是 `PI_CODING_AGENT_DIR`,没设时是 `~/.pi/agent`。
50
+
51
+ 没有这个文件时全部取默认值。类型错或未知键会让启动失败,并一次列出所有坏字段。枚举值不认识时取默认值。数字越界时夹到最近的边界。下面的注释只作解释,真正的文件必须是严格 JSON。
52
+
53
+ **最小配置**:不用写。五个工具在默认值下都能用。
54
+
55
+ ```json
56
+ {}
57
+ ```
58
+
59
+ **完整示例**:每个键都跟着默认值,以及什么时候该改。
60
+
61
+ ```jsonc
62
+ {
63
+ // 本地桥。扩展里编译了同一个端口
64
+ // (browser-extension/background/transport.js):两处一起改,然后重载扩展
65
+ "bridge": { "port": 17318 },
66
+
67
+ // 会话规则
68
+ "policy": {
69
+ "authorizeOnStart": false, // true = 不用 /chrome authorize on 就能用工具
70
+ "closeOwnTabsOnExit": true // 会话结束时关掉本会话开的标签
71
+ },
72
+
73
+ // 输入与时限
74
+ "input": {
75
+ "dragSteps": 10, // 按下与松开之间的鼠标移动次数
76
+ "scrollSteps": 5 // 每次 scroll 的滚轮事件数
77
+ },
78
+ "timing": { "navigateTimeoutMs": 15000 }, // 等 load 事件的时长;navigate 调用再多给 2 秒
79
+
80
+ // 模型可见的输出
81
+ "output": {
82
+ "maxElements": 80, // 快照元素上限
83
+ "textChars": 18000 // see 的 text 模式正文上限,其余带提示丢掉
84
+ },
85
+
86
+ // 截图
87
+ "screenshot": {
88
+ "dir": "/home/me/.pi/agent/pi-browser-use-lite/screenshots", // 第一次用时创建
89
+ "format": "png", // png | jpeg
90
+ "keepNewest": 20 // 这个目录里更旧的截图会被删掉
91
+ },
92
+
93
+ // 工具开关与名字。至少留一个启用;名字不能重复,也不能盖住宿主已有的工具
94
+ "tools": {
95
+ "chrome_act": { "enabled": true, "name": "chrome_act" },
96
+ "chrome_see": { "enabled": true, "name": "chrome_see" },
97
+ "chrome_tab": { "enabled": true, "name": "chrome_tab" },
98
+ "chrome_vision": { "enabled": true, "name": "chrome_vision" },
99
+ "chrome_cdp": { "enabled": true, "name": "chrome_cdp" }
100
+ },
101
+
102
+ // 按工具改内置提示。"" 清空 description 或 promptSnippet;[] 表示不加规则
103
+ "guidance": {
104
+ "chrome_act": {
105
+ "description": "操作用户真实的浏览器。",
106
+ "promptSnippet": "操作浏览器",
107
+ "promptGuidelines": ["chrome_act: navigate 时带上 see: refs。"]
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ 每个键都可选。范围与默认值在 `src/config/schema.ts`。
114
+
115
+ ## 文档
116
+
117
+ | 文档 | 内容 |
118
+ |---|---|
119
+ | [ARCHITECTURE](docs/ARCHITECTURE.md) | 模块边界与 seam;改结构之前先读它 |
120
+ | [GLOSSARY](docs/GLOSSARY.md) | 一个概念只用一个词 |
121
+ | [Modules](docs/modules/) | 每个模块的取舍与坑 |
122
+ | [Decisions](docs/decisions/) | 为什么是这个形状 |
123
+
124
+ ## 开发
125
+
126
+ 在仓库根执行:
127
+
128
+ ```bash
129
+ pnpm -C packages/pi-browser-use-lite lint
130
+ pnpm -C packages/pi-browser-use-lite check:imports
131
+ pnpm -C packages/pi-browser-use-lite typecheck
132
+ pnpm -C packages/pi-browser-use-lite test
133
+ ```
134
+
135
+ 端到端测试会驱动真实浏览器。把 `packages/pi-browser-use-lite/browser-extension` 加载进扩展,留一个开着的 Chromium 系浏览器,然后运行:
136
+
137
+ ```bash
138
+ pnpm -C packages/pi-browser-use-lite test:e2e
139
+ ```
140
+
141
+ 关掉其他装了本扩展的浏览器。两份扩展会抢同一个桥,命令落到先问的那一份。
142
+
143
+ 桥报出的版本与扩展不同的时候,扩展会自己重载。改了扩展代码但没升版本号时,去扩展页手动重载一次。
144
+
145
+ ## 许可证
146
+
147
+ MIT,见 [LICENSE](LICENSE)。
@@ -71,14 +71,11 @@ export const routes = new Map([
71
71
  ],
72
72
  [
73
73
  "tab.new",
74
- async ({ sessionKey, url, background } = {}) => ({
75
- id: await createTarget({ sessionKey, url, background }),
74
+ async ({ sessionKey, url } = {}) => ({
75
+ id: await createTarget({ sessionKey, url }),
76
76
  }),
77
77
  ],
78
- [
79
- "tab.activate",
80
- ({ tabId, background } = {}) => activateTab({ tabId, background }),
81
- ],
78
+ ["tab.activate", ({ tabId } = {}) => activateTab({ tabId })],
82
79
  ["tab.close", ({ tabId } = {}) => closeTab({ tabId })],
83
80
  [
84
81
  "page.navigate",
@@ -205,11 +202,7 @@ function requireTabId(tabId) {
205
202
  */
206
203
  async function tabIdOf(action, params) {
207
204
  if (params.tabId !== undefined) return requireTabId(params.tabId);
208
- return targetFor({
209
- sessionKey: params.sessionKey,
210
- action,
211
- background: true,
212
- });
205
+ return targetFor({ sessionKey: params.sessionKey, action });
213
206
  }
214
207
 
215
208
  /** 指针层的入参:ref 换成元素中心的视口坐标,其余原样。 */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 指针事件:移动、点击、悬停、拖动、滚轮。全部走 CDP,不做 DOM 假事件回退(DEC-0007)。
3
- * 目标标签不可见时直接抛错:Chrome 会丢弃不可见页面的可信输入,激活标签由调用方决定。
3
+ * Chrome 会丢弃不可见页面的可信输入,所以每个入口先调 assertVisible:不可见就把标签与窗口提到前台。
4
4
  */
5
5
 
6
6
  import { evaluate, sendCommand } from "./debugger.js";
@@ -210,14 +210,51 @@ async function viewportCenter(tabId) {
210
210
  return { x: center.x, y: center.y };
211
211
  }
212
212
 
213
+ /** 聚焦之后等页面变可见的次数与间隔:Chrome 要过一个 tick 才改 visibilityState。 */
214
+ const RAISE_TRIES = 6;
215
+ const RAISE_WAIT_MS = 50;
216
+
217
+ function sleep(ms) {
218
+ return new Promise((resolve) => setTimeout(resolve, ms));
219
+ }
220
+
221
+ function visibilityState(tabId) {
222
+ return evaluate(tabId, "document.visibilityState");
223
+ }
224
+
225
+ /** 页面为什么不可见,用来让报错指向人能动手的地方。 */
226
+ async function hiddenReason(tabId) {
227
+ const tab = await chrome.tabs.get(tabId).catch(() => undefined);
228
+ if (tab?.active === false) return "it is an inactive tab in its window";
229
+ const win =
230
+ tab?.windowId === undefined
231
+ ? undefined
232
+ : await chrome.windows.get(tab.windowId).catch(() => undefined);
233
+ if (win?.state === "minimized") return "its window is minimized";
234
+ if (win?.state === "fullscreen")
235
+ return "its window is behind a full-screen window";
236
+ return "its window is covered by another window";
237
+ }
238
+
239
+ /**
240
+ * 输入前确保页面可见。不可见就先激活标签、把窗口带到前台,仍不可见才报错。
241
+ * 激活标签不改窗口焦点,所以这一步只在真需要可信输入时打扰用户。
242
+ */
213
243
  export async function assertVisible(tabId) {
214
- const state = await evaluate(tabId, "document.visibilityState");
244
+ const state = await visibilityState(tabId);
215
245
  if (state === "visible") return;
216
- if (state === "hidden")
246
+ if (state !== "hidden")
217
247
  throw new Error(
218
- `page_error: tab ${tabId} is hidden; Chrome drops input to hidden pages, so bring it to the front with chrome_tab switch (turn background mode off first)`,
248
+ `page_error: could not read document.visibilityState for tab ${tabId}`,
219
249
  );
250
+ const reason = await hiddenReason(tabId);
251
+ const tab = await chrome.tabs.update(tabId, { active: true });
252
+ await chrome.windows.update(tab.windowId, { focused: true });
253
+ for (let attempt = 0; attempt < RAISE_TRIES; attempt++) {
254
+ await sleep(RAISE_WAIT_MS);
255
+ if ((await visibilityState(tabId)) === "visible") return;
256
+ }
220
257
  throw new Error(
221
- `page_error: could not read document.visibilityState for tab ${tabId}`,
258
+ `page_error: tab ${tabId} is hidden because ${reason}; Chrome drops trusted input to hidden pages. Bring the window to the front, then retry`,
222
259
  );
223
260
  }
@@ -23,17 +23,17 @@ export async function listTabs({ sessionKey } = {}) {
23
23
  * @param {object} params
24
24
  * @param {string} params.sessionKey 会话标识
25
25
  * @param {string} [params.url] 要打开的地址,缺省 about:blank
26
- * @param {boolean} [params.background] 为 true 时新标签不在本会话窗口里激活
27
26
  * @returns {Promise<{ id: number, active: boolean, title: string, url: string }>}
28
27
  */
29
- export async function newTab({ sessionKey, url, background = false } = {}) {
28
+ export async function newTab({ sessionKey, url } = {}) {
30
29
  requireSessionKey(sessionKey);
31
30
  const windowId = await liveWindowId(sessionKey);
32
31
  if (windowId !== undefined) {
32
+ // 新标签在自己的窗口里就是活动标签。激活标签不改变窗口焦点,不会打扰用户。
33
33
  const tab = await chrome.tabs.create({
34
34
  windowId,
35
35
  url: url ?? "about:blank",
36
- active: !background,
36
+ active: true,
37
37
  });
38
38
  return formatTab(await chrome.tabs.get(tab.id));
39
39
  }
@@ -47,14 +47,12 @@ export async function newTab({ sessionKey, url, background = false } = {}) {
47
47
  }
48
48
 
49
49
  /**
50
- * 激活标签,并把它所在的窗口带到前台。后台模式是硬策略:直接抛错,不碰 chrome.tabs.update。
50
+ * 激活标签,并把它所在的窗口带到前台。
51
51
  * @param {object} params
52
52
  * @param {number} params.tabId 标签 id
53
- * @param {boolean} [params.background] 为 true 时拒绝激活
54
53
  * @returns {Promise<{ id: number, active: boolean, title: string, url: string }>}
55
54
  */
56
- export async function activateTab({ tabId, background = false } = {}) {
57
- if (background) throw new Error("background mode blocks tab.activate");
55
+ export async function activateTab({ tabId } = {}) {
58
56
  const tab = await chrome.tabs.update(tabId, { active: true });
59
57
  await chrome.windows.update(tab.windowId, { focused: true });
60
58
  return formatTab(tab);
@@ -18,17 +18,12 @@ export const OWNED_KEY_PREFIX = "target.owned.";
18
18
  * @param {object} params
19
19
  * @param {string} params.sessionKey 会话标识
20
20
  * @param {string} [params.url] 新标签的地址
21
- * @param {boolean} [params.background] 为 true 时新标签不激活
22
21
  * @returns {Promise<number>} 新 target 的标签 id
23
22
  */
24
- export async function createTarget({
25
- sessionKey,
26
- url,
27
- background = false,
28
- } = {}) {
23
+ export async function createTarget({ sessionKey, url } = {}) {
29
24
  requireSessionKey(sessionKey);
30
25
 
31
- const tab = await newTab({ sessionKey, url, background });
26
+ const tab = await newTab({ sessionKey, url });
32
27
  await writeRecord(TARGET_KEY_PREFIX + sessionKey, tab.id);
33
28
  const owned = await readOwned(sessionKey);
34
29
  await writeRecord(OWNED_KEY_PREFIX + sessionKey, [...owned, tab.id]);
@@ -41,14 +36,9 @@ export async function createTarget({
41
36
  * @param {string} params.sessionKey 会话标识
42
37
  * @param {string} params.action 动作名,决定能不能顺手建 target
43
38
  * @param {string} [params.url] 建新标签时的地址
44
- * @param {boolean} [params.background] 为 true 时新建的标签不激活
45
39
  * @returns {Promise<number>} target 标签 id
46
40
  */
47
- export async function targetFor({
48
- sessionKey,
49
- action,
50
- background = false,
51
- } = {}) {
41
+ export async function targetFor({ sessionKey, action } = {}) {
52
42
  requireSessionKey(sessionKey);
53
43
  if (!action) throw new Error("action is required");
54
44
 
@@ -59,7 +49,7 @@ export async function targetFor({
59
49
  `no_target: no automation target for session ${sessionKey}`,
60
50
  );
61
51
 
62
- return createTarget({ sessionKey, background });
52
+ return createTarget({ sessionKey });
63
53
  }
64
54
 
65
55
  /**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Browser Use",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "Lets Pi drive tabs in your own Chrome through a local bridge at 127.0.0.1.",
6
6
  "permissions": [
7
7
  "debugger",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-browser-use-lite",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Pi extension: drive your own Chrome through a companion browser extension.",
5
5
  "keywords": ["pi-package", "pi-extension", "chrome", "browser"],
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `/chrome` 命令:authorize、background、status 三个子命令。
2
+ * `/chrome` 命令:authorize 与 status 两个子命令。
3
3
  * 命令只改会话状态与读桥的状态,不发浏览器命令;子命令文本由 {@link ChromeCommand.run} 返回。
4
4
  */
5
5
 
@@ -32,11 +32,35 @@ export const INSTALL_STEPS = [
32
32
  "3. leave the extension enabled; it polls the bridge by itself",
33
33
  ].join("\n");
34
34
 
35
- const SUBCOMMANDS = [
36
- "/chrome authorize on|off",
37
- "/chrome background on|off",
38
- "/chrome status",
39
- ].join("\n");
35
+ const SUBCOMMANDS = ["/chrome authorize on|off", "/chrome status"].join("\n");
36
+
37
+ /** 一条参数补全。形状与 pi 的 `AutocompleteItem` 一致,命令层不引 TUI 类型。 */
38
+ export interface ChromeCompletion {
39
+ value: string;
40
+ label: string;
41
+ description: string;
42
+ }
43
+
44
+ /** 整条子命令加一句说明,顺序即补全列表的顺序。 */
45
+ const COMPLETIONS: ReadonlyArray<readonly [string, string]> = [
46
+ ["authorize on", "unlock the tools for this session"],
47
+ ["authorize off", "lock the tools until the next authorize on"],
48
+ ["status", "print host, extension, authorization, bridge and target state"],
49
+ ];
50
+
51
+ /**
52
+ * `/chrome` 的参数补全:按整条子命令做前缀匹配,打到 `authorize ` 只剩 on 与 off。
53
+ * 一个都没匹配上返回 null,让 pi 用它的默认补全。
54
+ */
55
+ export function completeChromeArgument(
56
+ prefix: string,
57
+ ): ChromeCompletion[] | null {
58
+ const typed = prefix.trimStart();
59
+ const items = COMPLETIONS.filter(([value]) => value.startsWith(typed)).map(
60
+ ([value, description]) => ({ value, label: value, description }),
61
+ );
62
+ return items.length > 0 ? items : null;
63
+ }
40
64
 
41
65
  export function createChromeCommand(deps: ChromeCommandDeps): ChromeCommand {
42
66
  return {
@@ -45,7 +69,6 @@ export function createChromeCommand(deps: ChromeCommandDeps): ChromeCommand {
45
69
  if (trimmed === "") return SUBCOMMANDS;
46
70
  const [verb, value] = trimmed.split(/\s+/);
47
71
  if (verb === "authorize") return toggleAuthorize(deps, value);
48
- if (verb === "background") return toggleBackground(deps, value);
49
72
  if (verb === "status") return statusOf(deps);
50
73
  return `bad_args: unknown subcommand: ${trimmed}\nhint: run /chrome for the list`;
51
74
  },
@@ -71,17 +94,7 @@ async function toggleAuthorize(
71
94
  return "authorized: on";
72
95
  }
73
96
 
74
- function toggleBackground(
75
- deps: ChromeCommandDeps,
76
- value: string | undefined,
77
- ): string {
78
- const on = flagOf(value);
79
- if (on === undefined) return usage("background on|off");
80
- deps.policy.setBackground(on);
81
- return `background: ${on ? "on" : "off"}`;
82
- }
83
-
84
- /** 状态行:桥的四项来自 `bridge.status()`,策略两项来自会话,target 来自宿主会话状态。 */
97
+ /** 状态行:桥的四项来自 `bridge.status()`,授权来自会话,target 来自宿主会话状态。 */
85
98
  async function statusOf(deps: ChromeCommandDeps): Promise<string> {
86
99
  await deps.bridge.start();
87
100
  const bridge = await deps.bridge.describe();
@@ -90,7 +103,6 @@ async function statusOf(deps: ChromeCommandDeps): Promise<string> {
90
103
  `host: ${deps.hostVersion}`,
91
104
  `extension: ${bridge.extensionVersion ?? "not connected"}`,
92
105
  `authorized: ${deps.policy.authorized ? "on" : "off"}`,
93
- `background: ${deps.policy.background ? "on" : "off"}`,
94
106
  `bridge: ${bridge.mode}`,
95
107
  `port: ${new URL(bridge.url).port}`,
96
108
  `connected: ${bridge.connected ? "yes" : "no"}`,
@@ -68,30 +68,16 @@ export interface ResolvedConfig {
68
68
  bridge: { port: number };
69
69
  policy: {
70
70
  authorizeOnStart: boolean;
71
- background: boolean;
72
71
  closeOwnTabsOnExit: boolean;
73
72
  };
74
73
  input: { dragSteps: number; scrollSteps: number };
75
74
  timing: { navigateTimeoutMs: number };
76
75
  output: { maxElements: number; textChars: number };
77
76
  screenshot: { dir: string; format: ScreenshotFormat; keepNewest: number };
78
- prompt: { guidelines: string[] };
79
77
  tools: Record<ToolId, ToolSettings>;
80
78
  guidance: Partial<Record<ToolId, GuidanceFields>>;
81
79
  }
82
80
 
83
- /**
84
- * 跨工具的默认规则。每条以工具名开头:两层规则平铺进系统提示后按原文去重,
85
- * 不加工具前缀,只有点名才看得出是哪个工具的规则。
86
- * 全局规则只点名 chrome_act:别的工具可以关掉,点名它们的规则放在该工具的
87
- * promptGuidelines 里,才能随工具一起消失(SPEC-0001「关掉的工具……规则也一起消失」)。
88
- */
89
- export const DEFAULT_PROMPT_GUIDELINES = [
90
- "chrome_act: this is the user's real browser; confirm before destructive actions.",
91
- "chrome_act: pass see: refs on navigate, then click the refs from that snapshot; a ref from an older page fails.",
92
- "chrome_act: passing see adds a snapshot to the reply and costs extra tokens; ask for it only when the page just changed.",
93
- ] as const;
94
-
95
81
  /**
96
82
  * 13 个标量字段的默认值。工具记录与文案覆盖不在其中。
97
83
  * `screenshot.dir` 跟着 agent 目录走,由 buildConfig 补,不在这里。
@@ -105,14 +91,12 @@ export const DEFAULTS: Omit<
105
91
  bridge: { port: 17_318 },
106
92
  policy: {
107
93
  authorizeOnStart: false,
108
- background: true,
109
94
  closeOwnTabsOnExit: true,
110
95
  },
111
96
  input: { dragSteps: 10, scrollSteps: 5 },
112
97
  timing: { navigateTimeoutMs: 15_000 },
113
98
  output: { maxElements: 80, textChars: 18_000 },
114
99
  screenshot: { format: "png", keepNewest: 20 },
115
- prompt: { guidelines: [...DEFAULT_PROMPT_GUIDELINES] },
116
100
  };
117
101
 
118
102
  export type Raw = Record<string, unknown>;
@@ -29,7 +29,6 @@ const TOP_KEYS = [
29
29
  "timing",
30
30
  "output",
31
31
  "screenshot",
32
- "prompt",
33
32
  "tools",
34
33
  "guidance",
35
34
  ];
@@ -45,7 +44,7 @@ export function buildConfig(
45
44
  const policy = asObject(raw.policy, "policy", errors);
46
45
  checkUnknownKeys(
47
46
  policy,
48
- ["authorizeOnStart", "background", "closeOwnTabsOnExit"],
47
+ ["authorizeOnStart", "closeOwnTabsOnExit"],
49
48
  "policy",
50
49
  errors,
51
50
  );
@@ -79,12 +78,6 @@ export function buildConfig(
79
78
  "policy.authorizeOnStart",
80
79
  errors,
81
80
  ),
82
- background: readBoolean(
83
- policy.background,
84
- DEFAULTS.policy.background,
85
- "policy.background",
86
- errors,
87
- ),
88
81
  closeOwnTabsOnExit: readBoolean(
89
82
  policy.closeOwnTabsOnExit,
90
83
  DEFAULTS.policy.closeOwnTabsOnExit,
@@ -155,7 +148,6 @@ export function buildConfig(
155
148
  RANGES.keepNewest,
156
149
  ),
157
150
  },
158
- prompt: readPrompt(raw.prompt, errors),
159
151
  tools: readTools(raw.tools, errors),
160
152
  guidance: readGuidance(raw.guidance, errors),
161
153
  };
@@ -222,16 +214,6 @@ function checkUnknownKeys(
222
214
  }
223
215
  }
224
216
 
225
- function readPrompt(value: unknown, errors: Errors): { guidelines: string[] } {
226
- const raw = asObject(value, "prompt", errors);
227
- checkUnknownKeys(raw, ["guidelines"], "prompt", errors);
228
- const guidelines =
229
- raw.guidelines === undefined
230
- ? [...DEFAULTS.prompt.guidelines]
231
- : readStringArray(raw.guidelines, "prompt.guidelines", errors);
232
- return { guidelines };
233
- }
234
-
235
217
  function readGuidance(
236
218
  value: unknown,
237
219
  errors: Errors,
package/src/index.ts CHANGED
@@ -8,7 +8,10 @@ import type {
8
8
  ToolDefinition,
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
  import { Bridge } from "./bridge/index.ts";
11
- import { createChromeCommand } from "./commands/index.ts";
11
+ import {
12
+ completeChromeArgument,
13
+ createChromeCommand,
14
+ } from "./commands/index.ts";
12
15
  import { agentDir, HOST_VERSION, loadConfig } from "./config/index.ts";
13
16
  import { createSessionPolicy, type SessionPolicy } from "./policy/index.ts";
14
17
  import {
@@ -41,7 +44,8 @@ export default function piBrowserUseLite(pi: ExtensionAPI): void {
41
44
  }
42
45
 
43
46
  pi.registerCommand("chrome", {
44
- description: "authorize, background, or report the bridge state",
47
+ description: "authorize the tools, or report the bridge state",
48
+ getArgumentCompletions: completeChromeArgument,
45
49
  handler: async (args, ctx) => {
46
50
  ctx.ui.notify(await chrome.run(args));
47
51
  },
@@ -67,13 +71,6 @@ export default function piBrowserUseLite(pi: ExtensionAPI): void {
67
71
  policy.endSession();
68
72
  await bridge.stop();
69
73
  });
70
-
71
- pi.on("before_agent_start", (event) => {
72
- event.systemPromptOptions.promptGuidelines = [
73
- ...event.systemPromptOptions.promptGuidelines,
74
- ...config.prompt.guidelines,
75
- ];
76
- });
77
74
  }
78
75
 
79
76
  /**
@@ -88,7 +85,7 @@ function registerable(
88
85
  return {
89
86
  ...tool,
90
87
  execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
91
- const denial = policy.check(actionOf(params));
88
+ const denial = policy.check();
92
89
  if (denial) return errorResult(denial);
93
90
  return await executors.execute(
94
91
  tool.id,
@@ -119,7 +116,3 @@ function createTargetHolder(): SessionTarget {
119
116
  }
120
117
 
121
118
  /** 参数里的动作名。只有 chrome_act 与 chrome_tab 带 action,别的工具给空串。 */
122
- function actionOf(params: unknown): string {
123
- const action = (params as { action?: unknown } | null | undefined)?.action;
124
- return typeof action === "string" ? action : "";
125
- }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 策略插点:授权开关与硬背景模式。
3
- * 状态只在内存,随会话结束清回默认;判定不做 IO,也没有定时器。
2
+ * 策略插点:授权开关。
3
+ * 状态只在内存,随会话结束清回未授权;判定不做 IO,也没有定时器。
4
4
  */
5
5
 
6
6
  import type { ResolvedConfig } from "../config/index.ts";
@@ -18,27 +18,15 @@ const NOT_AUTHORIZED: PolicyDenial = {
18
18
  hint: "Run /chrome authorize on",
19
19
  };
20
20
 
21
- /** 聚焦动作名。工具名可被 config 改名,动作名不会,所以按动作名判。 */
22
- const FOCUS_ACTION = "switch";
23
-
24
- const FOCUS_BLOCKED: PolicyDenial = {
25
- class: "locked",
26
- message: "background mode blocks tab switch",
27
- hint: "Run /chrome background off",
28
- };
29
-
30
- /** 会话内的授权与背景模式状态。 */
21
+ /** 会话内的授权状态。 */
31
22
  export interface SessionPolicy {
32
23
  readonly authorized: boolean;
33
- readonly background: boolean;
34
24
  /** 授权开关。只在内存,不落盘。 */
35
25
  authorize(on: boolean): void;
36
- /** 背景模式开关。开启时不接受任何聚焦请求。 */
37
- setBackground(on: boolean): void;
38
- /** 会话结束:授权回到未授权,背景模式回到配置默认值。 */
26
+ /** 会话结束:授权回到未授权。 */
39
27
  endSession(): void;
40
28
  /** 插点:一次工具调用先过这里。放行返回 undefined,拒绝返回原因。 */
41
- check(action?: string): PolicyDenial | undefined;
29
+ check(): PolicyDenial | undefined;
42
30
  }
43
31
 
44
32
  /** 造一份会话策略。`authorizeOnStart` 为真时开局即授权。 */
@@ -46,28 +34,18 @@ export function createSessionPolicy(
46
34
  settings: ResolvedConfig["policy"],
47
35
  ): SessionPolicy {
48
36
  let authorized = settings.authorizeOnStart;
49
- let background = settings.background;
50
37
  return {
51
38
  get authorized() {
52
39
  return authorized;
53
40
  },
54
- get background() {
55
- return background;
56
- },
57
41
  authorize(on) {
58
42
  authorized = on;
59
43
  },
60
- setBackground(on) {
61
- background = on;
62
- },
63
44
  endSession() {
64
45
  authorized = false;
65
- background = settings.background;
66
46
  },
67
- check(action = "") {
68
- if (!authorized) return NOT_AUTHORIZED;
69
- if (background && action === FOCUS_ACTION) return FOCUS_BLOCKED;
70
- return undefined;
47
+ check() {
48
+ return authorized ? undefined : NOT_AUTHORIZED;
71
49
  },
72
50
  };
73
51
  }
@@ -34,7 +34,6 @@ export async function runTab(
34
34
  case "new": {
35
35
  const result = (await call(ctx, "tab.new", {
36
36
  url: params.url,
37
- background: true,
38
37
  })) as TabInfo;
39
38
  const id = targetIdOf(result);
40
39
  if (id !== undefined) ctx.deps.target.set(id);
@@ -45,10 +44,7 @@ export async function runTab(
45
44
  }
46
45
  case "switch": {
47
46
  const tabId = await requireTab(ctx, "switch", params.tab);
48
- const result = await call(ctx, "tab.activate", {
49
- tabId,
50
- background: false,
51
- });
47
+ const result = await call(ctx, "tab.activate", { tabId });
52
48
  return okResult(`ok: switch ${tabId}`, { result });
53
49
  }
54
50
  case "close": {
@@ -268,6 +268,9 @@ export const TOOL_SPECS = {
268
268
  ),
269
269
  }),
270
270
  promptGuidelines: [
271
+ "chrome_act: this is the user's real browser; confirm before destructive actions.",
272
+ "chrome_act: pass see: refs on navigate, then click the refs from that snapshot; a ref from an older page fails.",
273
+ "chrome_act: passing see adds a snapshot to the reply and costs extra tokens; ask for it only when the page just changed.",
271
274
  "chrome_act: name the action first, then send only the parameters that action reads.",
272
275
  "chrome_act: batch runs steps in order and stops at the first failure unless the step is optional.",
273
276
  ],