@tea-agent/loop-agent 0.43.0-next.13 → 0.43.0-next.15
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 +5 -0
- package/dist/build-stamp.json +3 -3
- package/dist/worker/console/chat/browser-automation.js +363 -0
- package/dist/worker/console/chat/browser-routes.js +36 -14
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +89 -0
- package/dist/worker/console/chat/pi-runtime.js +9 -1
- package/dist/worker/console/chat/routes.js +6 -1
- package/dist/worker/console/chat/session-mode.js +1 -0
- package/dist/worker/console/chat/tools.js +4 -0
- package/dist/worker/console/console-update-and-init.js +1 -19
- package/dist/worker/console/console-update-runtime.js +5 -20
- package/dist/worker/console/operator-actions.js +13 -1
- package/dist/worker/console/routes.js +8 -0
- package/dist/worker/console/server.js +9 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BEg5712w.js → abnfDiagram-N423BO3Z-CQE0d0CU.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CkQvaBGZ.js → arc-BFYYG0Wz.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BrdwFNJ7.js → architectureDiagram-T3A2C74G-B0YlJL9r.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-M_LjP2kx.js → blockDiagram-VBNYF7ZC-DN9kD3TO.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bwr5Dtjc.js → c4Diagram-5PPSVZJV-ChK60XBH.js} +1 -1
- package/dist/worker/console/static/assets/channel-BF5AdCat.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CDc44stW.js → chunk-2GRJ4B5K-D4AjoNa-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DZELwsTt.js → chunk-2Q5K7J3B-B2HEK_D9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-D7YI6SWX.js → chunk-5RXB4S5H-DXfZkTa0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-Bc46bEyj.js → chunk-5VM5RSS4-DC1FyC_o.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bj4vc-Dp.js → chunk-6Q2QTUOP-U2E9RJkG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DRi-0nDX.js → chunk-GF5L2VYU-BeMabzLg.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Dr-wFY60.js → chunk-JWPE2WC7-CWvf3gKN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CqIHE_D7.js → chunk-KBJHAD2P-BQE1P4so.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C-Dl8wzk.js → chunk-RYQCIY6F-DobDWQXh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CV4Mydz3.js → chunk-XXDRQBXY-IQe7Y7EH.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BFdnxKyb.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BFdnxKyb.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C3MamSMJ.js → cose-bilkent-JH36ORCC-CyDcJU1F.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-zo_Jy2sf.js → cynefin-VYW2F7L2-CDt8Uivk.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BB2PN1AG.js → cynefinDiagram-MW4NZA55-BmFT7svy.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BimYueoi.js → dagre-VZM6K2ZE-hRf4KDfM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Di1ObJRS.js → diagram-7IWD3JNH-CaDSdUs-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-C83l-IJx.js → diagram-B4RE2ZJO-Dw13MbVk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B3S3fOpm.js → diagram-LBJQPF4R-TLaO5F4A.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-urW_Zhe7.js → diagram-Q27KOJAE-B-YdjvlA.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-nvIbCwF6.js → diagram-UB23O5K3-DHR7IqG6.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DG6O3KpP.js → ebnfDiagram-BXEA7PRR-C5v-LKCD.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DpXdtdsj.js → erDiagram-JOGREHBK-7tPJ992C.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DvXWHrND.js → flowDiagram-UKHOOZJN-CdeV10q2.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6F7SbvUS.js → ganttDiagram-PKOTCBZU-Nuod51KB.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D1tZnFec.js → gitGraphDiagram-DS77QQ5N-CiHmujkA.js} +1 -1
- package/dist/worker/console/static/assets/{index-SAANIK8g.css → index-BXrp_jW4.css} +1 -1
- package/dist/worker/console/static/assets/{index-CWarwRqc.js → index-BnZTkOuj.js} +96 -96
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HN4qwJ2M.js → infoDiagram-6WML65LV-BsbKAEaZ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-OJ4qwDRo.js → ishikawaDiagram-WSZJBQD7-BMg44jAM.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CJZ6giQk.js → journeyDiagram-NVQOT4AX-B2HydpC5.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-co60m19S.js → kanban-definition-27J2QSJJ-Dym8tKBH.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D1KeFqZY.js → linear-C2XLUUy6.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-i7dDGqov.js → mermaid.core-CWKyfsl6.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-1SkQxBh1.js → mindmap-definition-FAOFIHXS-D1kp3mJa.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B10dhI-Y.js → pegDiagram-VL7TDLO6-yChJn9Q6.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CiUKUo6j.js → pieDiagram-7S7Q4E2Y-BL0gor4I.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C5vH4LXZ.js → quadrantDiagram-CIZ2JOQS-BQlU54QR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-5nlWOCEj.js → railroadDiagram-AXF67PYL-CFaiPP19.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CILd6QS3.js → requirementDiagram-LRYGKXZP-3ZmKxDyQ.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Dfp18ZO2.js → sankeyDiagram-W5VNT64P-C6ZDqbFT.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B9pkswBi.js → sequenceDiagram-SI44F4Z6-DdB0hg2W.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C1-eSgx4.js → sizeCapture-X5ZJPWSS-BdaLiAnv.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-COJsOoAF.js → stateDiagram-OKZ733FA-D75bWtSk.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBtfTPWx.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Dv5NXJUM.js → swimlanes-SLNWSIFB-BPhv358R.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cso4j_rK.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DRdrZ7eR.js → timeline-definition-Z64GVDOM-hrXh6Jie.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-0-2TlwCj.js → vennDiagram-T6HMQDX7-CxjAfZu8.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3-o3q92P.js → wardleyDiagram-T6FBY63Y-C4cHKlnQ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DSgOgdUi.js → xychartDiagram-ELKLHX3M-388ITGyq.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +24 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +14 -3
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +36 -50
- package/dist/worker/console/workspace-context.js +6 -0
- package/dist/worker/console/workspace-initialization.js +174 -0
- package/package.json +1 -3
- package/dist/commands/new-task.js +0 -5
- package/dist/commands/status.js +0 -57
- package/dist/commands/task-contract.js +0 -269
- package/dist/shared/prompts.js +0 -26
- package/dist/task/config.js +0 -2
- package/dist/task/contract/validate-draft.js +0 -106
- package/dist/task/goal.js +0 -3
- package/dist/task/index.js +0 -12
- package/dist/task/lifecycle.js +0 -1
- package/dist/task/operator/capabilities.js +0 -6
- package/dist/task/operator/envelope.js +0 -2
- package/dist/task/operator/index.js +0 -5
- package/dist/task/operator/registry.js +0 -2
- package/dist/task/operator/types.js +0 -1
- package/dist/task/paths.js +0 -1
- package/dist/task/source-state.js +0 -1
- package/dist/task/subagent-guidance.js +0 -1
- package/dist/worker/console/chat/sift-bridge.js +0 -1
- package/dist/worker/console/index.js +0 -20
- package/dist/worker/console/static/assets/channel-CKmyKgY-.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D8Wc3jJP.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D8Wc3jJP.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bpfb0q5s.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BEwGGabW.js +0 -8
- package/dist/worker/scheduler/index.js +0 -25
- package/dist/workflows/dag/facts.js +0 -4
- package/dist/workflows/dag/index.js +0 -6
- package/dist/workflows/dynamic/index.js +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
- Operator Chat 新增对话驱动的侧边栏浏览器自动化:模型可在工作流 / Pi 会话调用 `browser_navigate` / `browser_search` / `browser_inspect` / `browser_act`,把百度等页面打开到右侧浏览器面板,执行静态搜索、GET 表单填充提交、链接点击并返回结构化页面元素;命令通道按工作区隔离,自动化不读取或修改仓库文件,也不执行页面脚本。
|
|
6
|
+
|
|
5
7
|
- 修复前端 Plan 无法提交实现步骤的问题:finalize_plan 支持按 AC 绑定实施动作,缺漏在节点内返回修正,避免空计划反复进入设计审查。
|
|
6
8
|
|
|
7
9
|
- 修复前端最终审查反复猜错范围 ID:明确 checkpoint 参数和补交步骤,收尾正确识别 checkpoint 之后的审批结论。
|
|
@@ -10,6 +12,8 @@
|
|
|
10
12
|
|
|
11
13
|
### 改进
|
|
12
14
|
|
|
15
|
+
- Console 将全局「更新程序」与当前工作区初始化分开:添加或切换工作区后只读检查并提示,设置中展示仓库路径、初始化状态、待处理文件和操作进度;首次初始化显式选择可用模型,安全更新仅作用于当前仓库,冲突保留并提示处理。
|
|
16
|
+
|
|
13
17
|
- Operator Chat 内置内部 provider 瞬态异常恢复:即使目标仓库未执行 `loop-agent init`,也能识别完整或被裁剪的 `UnknownError` / `TypeValidationError` / `LLMRequestError` / 502 / 中文「网络波动、模型处理超时」响应,将其规范化后交给 Pi 原生有界重试;Console 缺省使用 loop-agent 推荐重试参数,项目显式字段优先于用户全局字段,用户配置优先于推荐默认值。共享纯分类器同时生成 OpenCode 插件谓词,overflow、认证、权限、配额、取消和业务错误继续 fail-fast。
|
|
14
18
|
|
|
15
19
|
- Operator Chat 浏览器面板默认以完整权限运行(去掉 iframe 沙箱与「临时解锁」状态条),并放开本机回环地址限制——`localhost` / `127.0.0.1` / `::1` 等本地开发服务器可直接在面板内浏览;地址栏仍仅放行 http/https,`javascript:` / `data:` / `file:` 等危险协议继续拦截,拒绝嵌入站点(X-Frame-Options / frame-ancestors)的解释面板与出口保持不变。
|
|
@@ -22,6 +26,7 @@
|
|
|
22
26
|
|
|
23
27
|
### 修复
|
|
24
28
|
|
|
29
|
+
- 对话页修复用户消息在流式回复中漏显示或重复分组、加载历史后的滚动跳动,以及删除会话后旧请求恢复条目的问题;移除工作区后隐藏非选中的空白会话,并正确恢复删除失败的列表状态。
|
|
25
30
|
- 修复 Operator Chat 的跨会话输入污染与快速重复提交:切换会话时立即撤销旧会话发送权,草稿、附件和 ↑ 输入历史按会话独立保存;发送、排队、撤回、编辑、插话、文件建议和图片读取的迟到结果只回写原会话。提交改用带快照的有界 attempt,相同内容的快速双 Enter/双击只产生一次请求,失败恢复或排队响应不再覆盖后来输入;流式更新和会话切换保持同一个 composer DOM,中文 IME 更稳定。同步修复 durable 用户消息确认后时间重排导致闪烁光标跑到用户消息上方的问题。
|
|
26
31
|
|
|
27
32
|
- 前端设计审查的写入门禁只统计已提交的审批终态,避免审查范围、发现记录导致合法审批被误判为缺失或歧义;要求修改时保留具体审查意见,缺失、重复及冲突终态仍阻断写入。
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.43.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-09-
|
|
3
|
+
"version": "0.43.0-next.15",
|
|
4
|
+
"gitSha": "216714a351e09e72203f04c7143d880cc22e17ca",
|
|
5
|
+
"builtAt": "2026-09-11T05:20:15.894Z"
|
|
6
6
|
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side implementation for the model-callable sidebar browser tools.
|
|
3
|
+
*
|
|
4
|
+
* Navigation is delivered to the visible BrowserPanel through a small pending
|
|
5
|
+
* command queue. Page inspection uses the same http(s) URL loaded by that
|
|
6
|
+
* panel and parses the server-visible HTML. Static GET forms are submitted as
|
|
7
|
+
* real navigations; dynamic scripts and POST forms are intentionally refused.
|
|
8
|
+
*/
|
|
9
|
+
import { normalizeBrowserUrl } from "./browser-policy.js";
|
|
10
|
+
export class BrowserAutomationService {
|
|
11
|
+
fetchImpl;
|
|
12
|
+
pending = [];
|
|
13
|
+
sequence = 0;
|
|
14
|
+
constructor(fetchImpl = fetch) {
|
|
15
|
+
this.fetchImpl = fetchImpl;
|
|
16
|
+
}
|
|
17
|
+
async navigate(rawUrl, options) {
|
|
18
|
+
const normalized = normalizeBrowserUrl(rawUrl);
|
|
19
|
+
if (normalized.kind !== "ok") {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
error: normalized.kind === "invalid"
|
|
23
|
+
? "invalid browser url"
|
|
24
|
+
: "browser urls must use http(s)",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
let snapshot;
|
|
28
|
+
const snapshotSource = rewriteBaiduSearchUrlForSnapshot(normalized.url);
|
|
29
|
+
if (options?.includeSnapshot) {
|
|
30
|
+
try {
|
|
31
|
+
snapshot = await this.inspect(snapshotSource);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Navigation itself can still succeed when the panel cannot
|
|
35
|
+
// fetch the page server-side; the iframe remains the fallback.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const displayUrl = snapshotSource === normalized.url ? normalized.url : snapshotSource;
|
|
39
|
+
const command = {
|
|
40
|
+
kind: "navigate",
|
|
41
|
+
id: this.nextId(),
|
|
42
|
+
url: displayUrl,
|
|
43
|
+
...(snapshot ? { snapshot } : {}),
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
this.pending = [command];
|
|
47
|
+
return { ok: true, command };
|
|
48
|
+
}
|
|
49
|
+
reload() {
|
|
50
|
+
const command = {
|
|
51
|
+
kind: "reload",
|
|
52
|
+
id: this.nextId(),
|
|
53
|
+
createdAt: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
this.pending = [command];
|
|
56
|
+
return { ok: true, command };
|
|
57
|
+
}
|
|
58
|
+
take() {
|
|
59
|
+
const commands = this.pending;
|
|
60
|
+
this.pending = [];
|
|
61
|
+
return commands;
|
|
62
|
+
}
|
|
63
|
+
async inspect(rawUrl) {
|
|
64
|
+
const normalized = normalizeBrowserUrl(rawUrl);
|
|
65
|
+
if (normalized.kind !== "ok")
|
|
66
|
+
throw new BrowserAutomationError("INVALID_URL", "invalid browser url");
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const timer = setTimeout(() => controller.abort(), 12_000);
|
|
69
|
+
try {
|
|
70
|
+
const response = await this.fetchImpl(normalized.url, {
|
|
71
|
+
redirect: "follow",
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
headers: { accept: "text/html,application/xhtml+xml" },
|
|
74
|
+
});
|
|
75
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
76
|
+
if (!contentType.includes("html")) {
|
|
77
|
+
throw new BrowserAutomationError("NOT_HTML", "the browser page did not return an HTML document");
|
|
78
|
+
}
|
|
79
|
+
const html = await response.text();
|
|
80
|
+
if (html.length > 3_000_000) {
|
|
81
|
+
throw new BrowserAutomationError("PAGE_TOO_LARGE", "page exceeds 3MB");
|
|
82
|
+
}
|
|
83
|
+
return parseBrowserPage(html, response.url || normalized.url);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof BrowserAutomationError)
|
|
87
|
+
throw error;
|
|
88
|
+
throw new BrowserAutomationError("FETCH_FAILED", "could not load the page", {
|
|
89
|
+
cause: error,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async search(rawQuery, rawUrl) {
|
|
97
|
+
const query = rawQuery.trim();
|
|
98
|
+
if (!query)
|
|
99
|
+
throw new BrowserAutomationError("EMPTY_QUERY", "search query is empty");
|
|
100
|
+
let target = rawUrl?.trim() ? rawUrl.trim() : "https://m.baidu.com/s?wd=";
|
|
101
|
+
const normalized = normalizeBrowserUrl(target);
|
|
102
|
+
if (normalized.kind !== "ok") {
|
|
103
|
+
throw new BrowserAutomationError("INVALID_URL", "search engine url must use http(s)");
|
|
104
|
+
}
|
|
105
|
+
const url = new URL(normalized.url);
|
|
106
|
+
url.searchParams.set(url.searchParams.has("q") ? "q" : "wd", query);
|
|
107
|
+
const result = await this.navigate(url.href, { includeSnapshot: true });
|
|
108
|
+
if (!result.ok)
|
|
109
|
+
throw new BrowserAutomationError("INVALID_URL", result.error);
|
|
110
|
+
return result.command.kind === "navigate" && result.command.snapshot
|
|
111
|
+
? result.command.snapshot
|
|
112
|
+
: await this.inspect(url.href);
|
|
113
|
+
}
|
|
114
|
+
async act(rawUrl, request) {
|
|
115
|
+
const snapshot = await this.inspect(rawUrl);
|
|
116
|
+
const element = selectBrowserElement(snapshot, request);
|
|
117
|
+
if (!element) {
|
|
118
|
+
throw new BrowserAutomationError("ELEMENT_NOT_FOUND", "selector did not match a clickable element or form field");
|
|
119
|
+
}
|
|
120
|
+
if (request.type === "fill") {
|
|
121
|
+
if (element.tag !== "input" && element.tag !== "textarea") {
|
|
122
|
+
throw new BrowserAutomationError("NOT_FILLABLE", "matched element is not fillable");
|
|
123
|
+
}
|
|
124
|
+
const submitted = this.submitForm(snapshot, element, request.value);
|
|
125
|
+
const submittedSnapshot = await this.inspect(submitted.url);
|
|
126
|
+
const navigation = await this.navigate(submitted.url, { includeSnapshot: true });
|
|
127
|
+
if (!navigation.ok)
|
|
128
|
+
throw new BrowserAutomationError("INVALID_URL", navigation.error);
|
|
129
|
+
return {
|
|
130
|
+
...submittedSnapshot,
|
|
131
|
+
action: { type: "fill", selector: request.selector, value: request.value, url: submitted.url },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const formSubmit = element.clickable && element.formIndex !== undefined && !element.href
|
|
135
|
+
? this.submitForm(snapshot, element)
|
|
136
|
+
: undefined;
|
|
137
|
+
const targetUrl = formSubmit?.url ?? element.href;
|
|
138
|
+
if (targetUrl === undefined) {
|
|
139
|
+
throw new BrowserAutomationError("NOT_CLICKABLE", "matched element has no safe click target");
|
|
140
|
+
}
|
|
141
|
+
const navigation = await this.navigate(targetUrl, { includeSnapshot: true });
|
|
142
|
+
if (!navigation.ok)
|
|
143
|
+
throw new BrowserAutomationError("INVALID_URL", navigation.error);
|
|
144
|
+
return {
|
|
145
|
+
...snapshot,
|
|
146
|
+
action: {
|
|
147
|
+
type: "click",
|
|
148
|
+
selector: request.selector,
|
|
149
|
+
url: targetUrl,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
submitForm(snapshot, target, valueOverride) {
|
|
154
|
+
if (target.formIndex === undefined) {
|
|
155
|
+
throw new BrowserAutomationError("FORM_NOT_FOUND", "matched field is not inside a form");
|
|
156
|
+
}
|
|
157
|
+
if ((target.formMethod ?? "get") !== "get") {
|
|
158
|
+
throw new BrowserAutomationError("FORM_METHOD_NOT_SUPPORTED", "only GET forms can be automated without browser scripts");
|
|
159
|
+
}
|
|
160
|
+
const url = new URL(target.formAction ?? snapshot.url);
|
|
161
|
+
for (const element of snapshot.elements) {
|
|
162
|
+
if (element.formIndex !== target.formIndex || !element.name)
|
|
163
|
+
continue;
|
|
164
|
+
if (element.tag === "button")
|
|
165
|
+
continue;
|
|
166
|
+
if (element.tag === "input" && ["submit", "button", "reset", "image"].includes(element.type ?? "")) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const value = element.selector === target.selector && valueOverride !== undefined
|
|
170
|
+
? valueOverride
|
|
171
|
+
: element.value ?? "";
|
|
172
|
+
url.searchParams.set(element.name, value);
|
|
173
|
+
}
|
|
174
|
+
return { url: url.href };
|
|
175
|
+
}
|
|
176
|
+
nextId() {
|
|
177
|
+
this.sequence += 1;
|
|
178
|
+
return `browser-${Date.now().toString(36)}-${this.sequence}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export class BrowserAutomationError extends Error {
|
|
182
|
+
code;
|
|
183
|
+
constructor(code, message, options) {
|
|
184
|
+
super(message, options);
|
|
185
|
+
this.code = code;
|
|
186
|
+
this.name = "BrowserAutomationError";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function rewriteBaiduSearchUrlForSnapshot(rawUrl) {
|
|
190
|
+
try {
|
|
191
|
+
const url = new URL(rawUrl);
|
|
192
|
+
if (url.protocol !== "https:" || url.hostname !== "www.baidu.com")
|
|
193
|
+
return rawUrl;
|
|
194
|
+
if (url.pathname.replace(/\/+$/, "") !== "/s")
|
|
195
|
+
return rawUrl;
|
|
196
|
+
const query = url.searchParams.get("wd") ?? url.searchParams.get("word");
|
|
197
|
+
if (query === null)
|
|
198
|
+
return rawUrl;
|
|
199
|
+
const mobile = new URL("https://m.baidu.com/s");
|
|
200
|
+
mobile.searchParams.set("wd", query);
|
|
201
|
+
return mobile.href;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return rawUrl;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const SKIP_TAGS = new Set(["script", "style", "template", "noscript"]);
|
|
208
|
+
const INTERACTIVE_TAGS = new Set(["a", "button", "input", "select", "textarea"]);
|
|
209
|
+
export function parseBrowserPage(html, baseUrl) {
|
|
210
|
+
const state = { snapshot: { url: baseUrl, title: "", elements: [] }, nextFormIndex: 0 };
|
|
211
|
+
const tokenizer = /<!--[\s\S]*?-->|<\/?([a-zA-Z][a-zA-Z0-9:-]*)([^>]*)>/g;
|
|
212
|
+
const openTags = [];
|
|
213
|
+
let cursor = 0;
|
|
214
|
+
let match;
|
|
215
|
+
while ((match = tokenizer.exec(html)) !== null) {
|
|
216
|
+
const text = decodeEntities(html.slice(cursor, match.index)).replace(/\s+/g, " ").trim();
|
|
217
|
+
const parent = openTags[openTags.length - 1];
|
|
218
|
+
if (parent && !SKIP_TAGS.has(parent.tag))
|
|
219
|
+
appendText(parent, text);
|
|
220
|
+
cursor = match.index + match[0].length;
|
|
221
|
+
if (match[0].startsWith("<!--"))
|
|
222
|
+
continue;
|
|
223
|
+
const tag = match[1]?.toLowerCase() ?? "";
|
|
224
|
+
const attrs = parseAttributes(match[2] ?? "");
|
|
225
|
+
if (match[0].startsWith("</")) {
|
|
226
|
+
const index = findOpenTag(openTags, tag);
|
|
227
|
+
if (index >= 0) {
|
|
228
|
+
const closed = openTags[index];
|
|
229
|
+
openTags.length = index;
|
|
230
|
+
closeElement(state, closed, baseUrl);
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const parentForForm = openTags[openTags.length - 1];
|
|
235
|
+
let form = parentForForm?.form;
|
|
236
|
+
if (tag === "form" && !form) {
|
|
237
|
+
form = {
|
|
238
|
+
index: state.nextFormIndex,
|
|
239
|
+
action: attrs.action,
|
|
240
|
+
method: attrs.method?.toLowerCase(),
|
|
241
|
+
};
|
|
242
|
+
state.nextFormIndex += 1;
|
|
243
|
+
}
|
|
244
|
+
const element = { tag, attrs, text: "", form };
|
|
245
|
+
if (/\/>$/.test(match[0]) || isVoidTag(tag)) {
|
|
246
|
+
closeElement(state, element, baseUrl);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
openTags.push(element);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (state.snapshot.elements.length > 300)
|
|
253
|
+
state.snapshot.elements.length = 300;
|
|
254
|
+
return state.snapshot;
|
|
255
|
+
}
|
|
256
|
+
function parseAttributes(raw) {
|
|
257
|
+
const attrs = {};
|
|
258
|
+
const pattern = /([:@a-zA-Z_#][^\s"'<>`=]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
|
|
259
|
+
let match;
|
|
260
|
+
while ((match = pattern.exec(raw)) !== null) {
|
|
261
|
+
attrs[match[1]?.toLowerCase() ?? ""] = decodeEntities(match[2] ?? match[3] ?? match[4] ?? "");
|
|
262
|
+
}
|
|
263
|
+
return attrs;
|
|
264
|
+
}
|
|
265
|
+
function isVoidTag(tag) {
|
|
266
|
+
return tag === "input" || tag === "img" || tag === "br" || tag === "hr" || tag === "meta" || tag === "link";
|
|
267
|
+
}
|
|
268
|
+
function findOpenTag(stack, tag) {
|
|
269
|
+
for (let index = stack.length - 1; index >= 0; index -= 1) {
|
|
270
|
+
if (stack[index]?.tag === tag)
|
|
271
|
+
return index;
|
|
272
|
+
}
|
|
273
|
+
return -1;
|
|
274
|
+
}
|
|
275
|
+
function appendText(target, text) {
|
|
276
|
+
target.text = target.text ? `${target.text} ${text}` : text;
|
|
277
|
+
}
|
|
278
|
+
function closeElement(state, element, baseUrl) {
|
|
279
|
+
const { tag, attrs, text } = element;
|
|
280
|
+
if (tag === "title") {
|
|
281
|
+
state.snapshot.title = text.slice(0, 300);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (!INTERACTIVE_TAGS.has(tag))
|
|
285
|
+
return;
|
|
286
|
+
let href;
|
|
287
|
+
if (tag === "a" && attrs.href && !/^(javascript|mailto|tel):/i.test(attrs.href.trim())) {
|
|
288
|
+
try {
|
|
289
|
+
href = new URL(attrs.href, baseUrl).href;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
href = undefined;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const id = attrs.id;
|
|
296
|
+
const name = attrs.name;
|
|
297
|
+
const selector = buildSelector(tag, id, name, attrs.class, state.snapshot.elements.length);
|
|
298
|
+
const rawText = text || attrs.value || attrs.placeholder || attrs["aria-label"] || attrs.title || "";
|
|
299
|
+
state.snapshot.elements.push({
|
|
300
|
+
tag,
|
|
301
|
+
...(id ? { id } : {}),
|
|
302
|
+
text: rawText.replace(/\s+/g, " ").trim().slice(0, 240),
|
|
303
|
+
...(href ? { href } : {}),
|
|
304
|
+
...(name ? { name } : {}),
|
|
305
|
+
...(attrs.type ? { type: attrs.type } : {}),
|
|
306
|
+
...(attrs.value ? { value: attrs.value } : {}),
|
|
307
|
+
...(attrs.placeholder ? { placeholder: attrs.placeholder } : {}),
|
|
308
|
+
...(element.form ? { formIndex: element.form.index } : {}),
|
|
309
|
+
...(element.form ? {
|
|
310
|
+
formAction: element.form.action === undefined
|
|
311
|
+
? baseUrl
|
|
312
|
+
: new URL(element.form.action, baseUrl).href,
|
|
313
|
+
formMethod: element.form.method ?? "get",
|
|
314
|
+
} : {}),
|
|
315
|
+
selector,
|
|
316
|
+
clickable: tag === "a" || tag === "button" || (tag === "input" && ["submit", "button", "radio", "checkbox"].includes(attrs.type ?? "")),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function buildSelector(tag, id, name, className, index) {
|
|
320
|
+
if (id)
|
|
321
|
+
return `#${escapeSelectorValue(id)}`;
|
|
322
|
+
if (name)
|
|
323
|
+
return `${tag}[name="${escapeSelectorValue(name)}"]`;
|
|
324
|
+
if (className?.trim()) {
|
|
325
|
+
const firstClass = className.trim().split(/\s+/)[0];
|
|
326
|
+
if (firstClass)
|
|
327
|
+
return `${tag}.${escapeSelectorValue(firstClass)}`;
|
|
328
|
+
}
|
|
329
|
+
return `${tag}:nth-of-type(${index + 1})`;
|
|
330
|
+
}
|
|
331
|
+
function escapeSelectorValue(value) {
|
|
332
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
333
|
+
}
|
|
334
|
+
export function selectBrowserElement(snapshot, request) {
|
|
335
|
+
const selector = request.selector.trim();
|
|
336
|
+
if (!selector)
|
|
337
|
+
return undefined;
|
|
338
|
+
return snapshot.elements.find((element) => element.selector === selector)
|
|
339
|
+
?? snapshot.elements.find((element) => element.id === selector.slice(1))
|
|
340
|
+
?? snapshot.elements.find((element) => element.name === selector)
|
|
341
|
+
?? snapshot.elements.find((element) => element.text.toLowerCase().includes(selector.toLowerCase()) && selector.length >= 2);
|
|
342
|
+
}
|
|
343
|
+
const ENTITY_MAP = {
|
|
344
|
+
amp: "&",
|
|
345
|
+
lt: "<",
|
|
346
|
+
gt: ">",
|
|
347
|
+
quot: "\"",
|
|
348
|
+
apos: "'",
|
|
349
|
+
nbsp: " ",
|
|
350
|
+
};
|
|
351
|
+
function decodeEntities(value) {
|
|
352
|
+
return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (whole, entity) => {
|
|
353
|
+
if (entity.startsWith("#x") || entity.startsWith("#X")) {
|
|
354
|
+
const code = Number.parseInt(entity.slice(2), 16);
|
|
355
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
356
|
+
}
|
|
357
|
+
if (entity.startsWith("#")) {
|
|
358
|
+
const code = Number.parseInt(entity.slice(1), 10);
|
|
359
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
360
|
+
}
|
|
361
|
+
return ENTITY_MAP[entity.toLowerCase()] ?? whole;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* caller), restricted to http(s) URLs, with a hard timeout. Local addresses
|
|
7
7
|
* are allowed — the panel routinely browses local dev servers.
|
|
8
8
|
*/
|
|
9
|
-
import { embeddabilityOf, extractFrameAncestors, } from "./browser-policy.js";
|
|
9
|
+
import { embeddabilityOf, extractFrameAncestors, normalizeBrowserUrl, } from "./browser-policy.js";
|
|
10
10
|
function workspaceIdOf(req) {
|
|
11
11
|
try {
|
|
12
12
|
return new URL(req.url ?? "/", "http://localhost").searchParams.get("workspaceId") ?? undefined;
|
|
@@ -52,24 +52,20 @@ export async function handleBrowserRequest(req, res, deps, pathname) {
|
|
|
52
52
|
}
|
|
53
53
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
54
54
|
const raw = url.searchParams.get("url") ?? "";
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
parsed = new URL(raw);
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
sendJson(res, 400, {
|
|
61
|
-
ok: false,
|
|
62
|
-
error: { code: "INVALID_URL", message: "invalid url" },
|
|
63
|
-
});
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
55
|
+
const normalized = normalizeBrowserUrl(raw);
|
|
56
|
+
if (normalized.kind !== "ok") {
|
|
67
57
|
sendJson(res, 400, {
|
|
68
58
|
ok: false,
|
|
69
|
-
error: {
|
|
59
|
+
error: {
|
|
60
|
+
code: normalized.kind === "blocked" ? "BAD_SCHEME" : "INVALID_URL",
|
|
61
|
+
message: normalized.kind === "blocked"
|
|
62
|
+
? "only http/https urls can be probed"
|
|
63
|
+
: "invalid url",
|
|
64
|
+
},
|
|
70
65
|
});
|
|
71
66
|
return true;
|
|
72
67
|
}
|
|
68
|
+
const parsed = new URL(normalized.url);
|
|
73
69
|
const controller = new AbortController();
|
|
74
70
|
const timer = setTimeout(() => controller.abort(), 8000);
|
|
75
71
|
try {
|
|
@@ -116,6 +112,32 @@ export async function handleBrowserRequest(req, res, deps, pathname) {
|
|
|
116
112
|
clearTimeout(timer);
|
|
117
113
|
}
|
|
118
114
|
}
|
|
115
|
+
if (method === "GET" && sub === "commands" && parts.length === 1) {
|
|
116
|
+
const ws = workspaceIdOf(req);
|
|
117
|
+
if (!ws) {
|
|
118
|
+
sendJson(res, 400, {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: { code: "WORKSPACE_REQUIRED", message: "workspaceId is required" },
|
|
121
|
+
});
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (deps.requireWorkspace) {
|
|
125
|
+
const scope = deps.requireWorkspace(req);
|
|
126
|
+
if (!scope.ok) {
|
|
127
|
+
sendJson(res, 403, {
|
|
128
|
+
ok: false,
|
|
129
|
+
error: { code: "WORKSPACE_DENIED", message: "workspace scope mismatch" },
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!deps.automation) {
|
|
135
|
+
sendJson(res, 200, { ok: true, data: { commands: [] } });
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
sendJson(res, 200, { ok: true, data: { commands: deps.automation.take() } });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
119
141
|
sendJson(res, 404, {
|
|
120
142
|
ok: false,
|
|
121
143
|
error: { code: "NOT_FOUND", message: `unknown browser route: ${method} ${pathname}` },
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-independent browser tools shared by workflow and Pi chat modes.
|
|
3
|
+
* Navigation is visible in the Console sidebar; inspection is server-side and
|
|
4
|
+
* bounded to avoid exposing raw credentials or arbitrary page scripts.
|
|
5
|
+
*/
|
|
6
|
+
const BROWSER_TOOLS_DESCRIPTION = "Operate the Console sidebar browser. `browser_navigate` shows a URL in the panel; `browser_search` opens a search URL and returns elements; `browser_inspect` returns title/links/form controls; `browser_act` fills/submits a static GET form or clicks a static link. For natural-language search requests, prefer `browser_search` over navigating to a homepage and simulating its form. It never edits the workspace and cannot execute browser scripts. Dynamic-page and POST-form operations are out of scope.";
|
|
7
|
+
export async function buildBrowserTools(options) {
|
|
8
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
9
|
+
import("typebox"),
|
|
10
|
+
import("@earendil-works/pi-coding-agent"),
|
|
11
|
+
]);
|
|
12
|
+
const automation = options.automation;
|
|
13
|
+
const urlParameter = Type.String({
|
|
14
|
+
description: "Absolute http(s) URL. Prefer URLs/element selectors returned by prior browser tools.",
|
|
15
|
+
});
|
|
16
|
+
async function execute(operation) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await operation();
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
21
|
+
details: result,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const payload = {
|
|
26
|
+
error: error instanceof Error ? error.message : String(error),
|
|
27
|
+
code: error instanceof Error && "code" in error
|
|
28
|
+
? String(error.code)
|
|
29
|
+
: "BROWSER_OPERATION_FAILED",
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
33
|
+
details: payload,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
defineTool({
|
|
39
|
+
name: "browser_navigate",
|
|
40
|
+
label: "Sidebar browser navigate",
|
|
41
|
+
description: BROWSER_TOOLS_DESCRIPTION,
|
|
42
|
+
promptSnippet: BROWSER_TOOLS_DESCRIPTION,
|
|
43
|
+
parameters: Type.Object({ url: urlParameter }, { additionalProperties: false }),
|
|
44
|
+
async execute(_toolCallId, params) {
|
|
45
|
+
return execute(() => automation.navigate(params.url, { includeSnapshot: true }));
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
defineTool({
|
|
49
|
+
name: "browser_search",
|
|
50
|
+
label: "Sidebar browser search",
|
|
51
|
+
description: BROWSER_TOOLS_DESCRIPTION,
|
|
52
|
+
promptSnippet: BROWSER_TOOLS_DESCRIPTION,
|
|
53
|
+
parameters: Type.Object({
|
|
54
|
+
query: Type.String({ description: "Search query." }),
|
|
55
|
+
engineUrl: Type.Optional(urlParameter),
|
|
56
|
+
}, { additionalProperties: false }),
|
|
57
|
+
async execute(_toolCallId, params) {
|
|
58
|
+
return execute(() => automation.search(params.query, params.engineUrl));
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
defineTool({
|
|
62
|
+
name: "browser_inspect",
|
|
63
|
+
label: "Sidebar browser inspect",
|
|
64
|
+
description: BROWSER_TOOLS_DESCRIPTION,
|
|
65
|
+
promptSnippet: BROWSER_TOOLS_DESCRIPTION,
|
|
66
|
+
parameters: Type.Object({ url: urlParameter }, { additionalProperties: false }),
|
|
67
|
+
async execute(_toolCallId, params) {
|
|
68
|
+
return execute(() => automation.inspect(params.url));
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
defineTool({
|
|
72
|
+
name: "browser_act",
|
|
73
|
+
label: "Sidebar browser act",
|
|
74
|
+
description: BROWSER_TOOLS_DESCRIPTION,
|
|
75
|
+
promptSnippet: BROWSER_TOOLS_DESCRIPTION,
|
|
76
|
+
parameters: Type.Object({
|
|
77
|
+
url: urlParameter,
|
|
78
|
+
type: Type.Union([Type.Literal("click"), Type.Literal("fill")]),
|
|
79
|
+
selector: Type.String({ description: "Selector returned by browser_inspect." }),
|
|
80
|
+
value: Type.Optional(Type.String()),
|
|
81
|
+
}, { additionalProperties: false }),
|
|
82
|
+
async execute(_toolCallId, params) {
|
|
83
|
+
return execute(() => automation.act(params.url, params.type === "click"
|
|
84
|
+
? { type: "click", selector: params.selector }
|
|
85
|
+
: { type: "fill", selector: params.selector, value: params.value ?? "" }));
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
@@ -43,12 +43,14 @@ import { incrementCounter as incrementPressureCounter } from "../../../shared/pi
|
|
|
43
43
|
import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
|
|
44
44
|
import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
45
45
|
import { redactRuntimeText } from "./runtime-context.js";
|
|
46
|
+
import { BrowserAutomationService } from "./browser-automation.js";
|
|
46
47
|
import { buildTodoContinuationContext, } from "./todos.js";
|
|
47
48
|
import { buildPersistentTerminalTools } from "./terminal-tools.js";
|
|
48
49
|
import { buildScmTools } from "./scm-tools.js";
|
|
49
50
|
import { buildGoalContinuationContext, } from "./goals.js";
|
|
50
51
|
import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
|
|
51
52
|
import { buildCustomOperatorTools } from "./pi-runtime/custom-tools/operator-tools.js";
|
|
53
|
+
import { buildBrowserTools } from "./pi-runtime/custom-tools/browser-tools.js";
|
|
52
54
|
import { buildAskUserQuestionTool } from "./pi-runtime/custom-tools/ask-user-question.js";
|
|
53
55
|
import { buildTodoWriteTool } from "./pi-runtime/custom-tools/todo-write.js";
|
|
54
56
|
import { buildGoalTools } from "./pi-runtime/custom-tools/goal-tools.js";
|
|
@@ -78,7 +80,7 @@ import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-b
|
|
|
78
80
|
*/
|
|
79
81
|
export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
80
82
|
"You are General Operator Chat.",
|
|
81
|
-
"Use operator tools first (dagReport, dagRerunPlan, dagRerun). Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for probes.",
|
|
83
|
+
"Use operator tools first (dagReport, dagRerunPlan, dagRerun). Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for probes. Browser_* drives the visible Browser panel only (static http(s), no workspace edits/scripts).",
|
|
82
84
|
"No harness and asked to init: `loop-agent init instructions --repo-root .`, then `loop-agent init --repo-root . --profile full --merge`; verify with doctor, inspect and docs audit. Updates: `loop-agent init upgrade --repo-root . --json` until stable; `loop-agent init check-update` is read-only.",
|
|
83
85
|
"Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait at 60s → 90s → 120s → 180s; reset to 60s on state change; 30-60s re-checks when stall suspected. operationWait MUST use afterSeq=previous nextSeq (recommendedNextCall). Heartbeats: not meaningful progress. Run id ≠ task id.",
|
|
84
86
|
"Failure: terminal runSummary first. With progress wait; else dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun: reruns the failed subgraph, keeps prior successes. Ineligible: Worker → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). Do not call dagResume; do not re-advance a run-failed task. Pre-run identical state twice: stop and report; never loop identical bash/read/grep. Approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
|
|
@@ -415,10 +417,12 @@ export class ConsolePiRuntime {
|
|
|
415
417
|
exploreCustomToolsPromise;
|
|
416
418
|
exploreOrchestrator;
|
|
417
419
|
bindings;
|
|
420
|
+
browserAutomation;
|
|
418
421
|
constructor(options) {
|
|
419
422
|
this.options = options;
|
|
420
423
|
this.loader = createOperatorChatResourceLoader();
|
|
421
424
|
this.bindings = options.bindings ?? createDefaultPiSdkBindings();
|
|
425
|
+
this.browserAutomation = options.browserAutomation ?? new BrowserAutomationService();
|
|
422
426
|
this.exploreOrchestrator = new ExploreOrchestrator({
|
|
423
427
|
cwd: this.options.cwd,
|
|
424
428
|
bindings: this.bindings,
|
|
@@ -972,6 +976,9 @@ export class ConsolePiRuntime {
|
|
|
972
976
|
: Promise.resolve([]),
|
|
973
977
|
this.loadExploreCustomTools(),
|
|
974
978
|
]);
|
|
979
|
+
const browserTools = await buildBrowserTools({
|
|
980
|
+
automation: this.browserAutomation,
|
|
981
|
+
});
|
|
975
982
|
const questionTool = this.options.questionRegistry
|
|
976
983
|
? await buildAskUserQuestionTool({
|
|
977
984
|
sessionId,
|
|
@@ -1030,6 +1037,7 @@ export class ConsolePiRuntime {
|
|
|
1030
1037
|
...goalTools,
|
|
1031
1038
|
...terminalTools,
|
|
1032
1039
|
...scmTools,
|
|
1040
|
+
...browserTools,
|
|
1033
1041
|
...(agentTool ? [agentTool] : []),
|
|
1034
1042
|
];
|
|
1035
1043
|
}
|
|
@@ -4280,7 +4280,12 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
|
|
|
4280
4280
|
}
|
|
4281
4281
|
}
|
|
4282
4282
|
// Browser probe API (read-only; explains iframe refusals to the panel).
|
|
4283
|
-
if (await handleBrowserRequest(req, res, {
|
|
4283
|
+
if (await handleBrowserRequest(req, res, {
|
|
4284
|
+
automation: deps.browserAutomation,
|
|
4285
|
+
requireWorkspace: () => workspaceScope
|
|
4286
|
+
? { ok: true, workspaceId: workspaceScope }
|
|
4287
|
+
: { ok: false },
|
|
4288
|
+
}, pathname)) {
|
|
4284
4289
|
return true;
|
|
4285
4290
|
}
|
|
4286
4291
|
if (!pathname.startsWith("/api/operator/v1/chat/") &&
|
|
@@ -74,6 +74,7 @@ export const PI_CHAT_SYSTEM_PROMPT_BASE = [
|
|
|
74
74
|
"Use read/write/edit/bash/grep/find/ls to inspect and change the workspace. apply_patch, full-tools, shell and coding-chat are denied.",
|
|
75
75
|
"Agent (Explore) is a built-in read-only subagent for path-uncertain codebase search. Do not use it for a known single-file read. Prefer safe-read/safe-grep for secret-safe probes.",
|
|
76
76
|
"ask_user_question pauses for a real user answer. todo_write tracks the current work and is restored after compression; it is not a workflow auto-continue signal. The persistent terminal tool is available.",
|
|
77
|
+
"Sidebar browser tools (browser_navigate/search/inspect/act) drive the visible Browser panel for user-requested web research and static automation; they never edit workspace files or execute page scripts.",
|
|
77
78
|
"Goal tools (get_goal / create_goal / update_goal) track the session's durable cross-turn objective. Create a Goal only when the user explicitly asks to start one; iterate in bounded rounds until complete or blocked.",
|
|
78
79
|
"This session keeps Console compression (L0/L1/L2, sift, strict checkpoint) and existing Aimax / SDD access. Do not create tasks, run DAGs, or start requirement interviews from this session.",
|
|
79
80
|
"Obey repository AGENTS.md, skills and extensions when they apply. Choosing Pi mode is not extra write authorization and does not bypass Console access control or output scrubbing.",
|
|
@@ -25,6 +25,10 @@ export const OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS = Object.freeze([
|
|
|
25
25
|
OPERATOR_CHAT_AGENT_TOOL_NAME,
|
|
26
26
|
"terminal",
|
|
27
27
|
"scm",
|
|
28
|
+
"browser_navigate",
|
|
29
|
+
"browser_search",
|
|
30
|
+
"browser_inspect",
|
|
31
|
+
"browser_act",
|
|
28
32
|
]);
|
|
29
33
|
/**
|
|
30
34
|
* Actions the model MAY invoke. Derived from the registry but filtered by the
|