✨ 新增 sctl Browser 扩展与浏览器控制(第 1 期:地基) - #4
Merged
Merged
Conversation
Add browser tab and window methods, browser target error codes and browser handshake contexts to protocol.json; generate Go for every peer, ScriptCat TypeScript byte-identical to the paired copy, and browser TypeScript under extension/src/protocol/generated.
Browser instances authenticate with their own per-instance key, bound to peer kind and instance ID in the handshake MAC, and are recorded in a 0600 registry (browsers.json) written through fsutil.WriteFileAtomic. Names are unique across paired instances; a taken name is answered with CONFLICT on $session.capabilities. Connections are registered before the capabilities reply, fixing the race where an immediate Call got ErrNotConnected. ScriptCat keeps pairing.key and its one-connection-replaces-the-previous behaviour.
…page data MCP tool descriptions for browser tools are static text per spec — tab titles and URLs returned from tabs.list are never included in tool descriptions. Add TestToolDescriptionsAreStatic to verify this: list tools before and after calling tabs.list with marker strings in tab data, confirm descriptions remain identical and contain no markers. Closes task 6 gap: MCP tool descriptions are static text.
…nd user docs architecture.md now documents the second extension kind, simultaneous browser instances, routing by method ownership, the extension/ directory, and the current store/ contents (browsers.json plus per-instance keys, not just K). AGENTS.md scopes "the authority always lives on the extension side" to ScriptCat's write/source-disclosure gates and calls out that browser control has no human gate by design. README.md, docs/README_zh-CN.md, and docs/mcp.md add installing and pairing the sctl Browser extension and the new browsers/tabs/windows commands and MCP tools, cross-linking to the owning docs instead of duplicating details.
- popup connected state shows browser, daemon version and the real connected-since time carried in ConnectionState, plus the instance name - pairing always registers the default name; the pairing-code draft is kept until pairing succeeds - merged list calls skip instances that answer NOT_FOUND (window IDs are per browser); forget of an unknown instance returns BROWSER_NOT_FOUND - CLI asks for --browser on BROWSER_AMBIGUOUS; offline browsers omit connectedAt in JSON - MCP browser tools describe list aggregation correctly and never report "waiting for approval"; tests cover browser argument forwarding - threat model states browser control has no human gate; Noto Sans CJK fallback; dark danger button text meets contrast
Splits the brand decorative blue (#1296DB) from a darker, contrast-safe --primary (#0E77AE) and darkens the light-mode success/warning/danger tokens (#128155/#9D6504/#D2352C) per the revised spec (2b4dd1a); dark mode is unchanged. The link panel's icons move to --brand since they carry no text. A new test parses :root/.dark from globals.css and asserts every text/background and white-on-fill pairing the popup uses is >= 4.5:1 in both modes, and pins --brand to the spec values.
Button hover states also carry text, so they fall under the spec's 4.5:1 rule. The primary button's bg-primary/80 hover washed white text down to 3.46:1 in light mode, the forget footer button's 10% red tint left its red text at 4.23:1, and the dark danger confirm hover (oklch black 12%) dropped dark text to 4.38:1. Hover fills are now --primary-hover/--bad-hover tokens that move away from the text colour in each mode, and the forget button uses the ghost muted hover. The light --destructive token (aria-invalid borders) still held the pre-revision #D6453D; it now equals the spec danger colour. The contrast test covers both hover fills and pins --destructive to --bad.
- escape control characters in page titles and URLs printed by sctl tabs list - log the cause when forgetting a browser fails to write the registry - forget resolves names before IDs, matching --browser target selection - settle a rename whose accepted name cannot be saved, and revert to the saved name - show the rejection again after a re-pairing later gets rejected - require the browser registry in bridge.NewServer and drop dead nil checks - derive MCP browser routing and the browser parameter from protocol peer/mergeField - merge control.Client Call/CallBrowser and share MCP schema compilation - drop vacuous assertions from the static MCP description test
TestBlockingProgressAndCancel slept 50ms and assumed the tools/call had reached the bridge caller by then. Under CPU load the cancellation can arrive before the server dispatches the request; go-sdk then drops the request, the bridge is never called, and the test failed with "ctx 取消未传播到桥接调用" (reproduced at the base 49d076c too). Wait for the fake caller to signal entry before cancelling.
…n to close Registering a new ScriptCat or browser-instance connection closed the connection it replaced synchronously, and the capabilities reply is only sent after registration. coder/websocket's Close waits up to 5s for the old peer's close frame whenever the daemon's read loop for that connection is not parked in a read, so the new connection's reply was held back for 5s (and, for browsers, regMu was held meanwhile). TestBrowserInstancesCoexist hit this at server_test.go:80 when a2's capabilities reply missed the 3s read deadline. The replaced connection is now closed in the background. closedByDaemon no longer counts a read deadline as a close, so a replacement that never closes the old connection fails the tests. Separately, the frame validator compiles its schemas on first use; under heavy CPU load that one-time cost fell inside the first handshake's 3s read deadline. Both test harnesses now pay it before any deadline.
…e docs - the reconnect note counted the upcoming retry as attempt+1, so the first retry after a failed connection read as the second - a pairing code is consumed by the first extension that pairs; README, README_zh-CN and the connect comment claimed the same code pairs both - README command table still described connect and mcp as ScriptCat-only - tabs_list description said it lists one window; it lists every window unless windowId is given
- ForgetInstance closed an online instance's connection synchronously while holding regMu; a peer that never answers the close frame held forget, and every other browser's registration, for up to 5s. It now closes in the background like a replaced connection (closeReplaced -> closeInBackground). - handleRPCResponse left the pending entry in place after delivery; a repeated response arriving after the caller gave up blocked the read loop forever on the full respCh. Delivery now removes the entry first. - tabs_close and tabs_activate MCP schemas dropped protocol.json's minItems and minimum constraints; a test now pins browser tool schemas to their protocol params types. - TestBrowserToolsNeverReportWaitingForApproval slept 100ms without waiting for the call to arrive, so under load it could pass vacuously; it now waits for arrival and uses an approval call's progress as its clock. - The popup address check accepted hosts no WebSocket URL can be built from (999.1.1.1, [:::]); saving one broke every later connection attempt. - The popup applied the initial getState reply even after a newer state broadcast had arrived, freezing a stale status on screen.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
sctl Browser 第 1 期(地基)。新增一个独立的 Chromium 扩展 sctl Browser,和 ScriptCat 同时配对到同一个
sctl serve。用户日常使用的一个或多个浏览器,每个都有自己起的名字,可以通过 CLI 和 MCP 列出、打开、关闭、激活标签页,也可以列出窗口。需求和设计决策见 spec:
docs/specs/2026-09-28-browser-foundation.md。主要改动
protocol.json仍是唯一的权威来源,每个方法、错误码、握手常量都标注了归属。生成器输出三套代码:Go 侧拿全部定义;ScriptCat 用的 TS 与改动前逐字节一致,CI 里针对固定版本 ScriptCat 的比对照常通过;浏览器扩展另有一套自己的 TS。browsers.json,写盘经fsutil.WriteFileAtomic。握手 MAC 绑定实例的类型和 ID;名称在所有已配对实例里唯一,冲突时返回CONFLICT。ScriptCat 的pairing.key不受影响。scripts.*发给 ScriptCat,浏览器方法发给目标实例)。支持--browser(名称或实例 ID 前缀)和SCTL_BROWSER。多个浏览器在线时,列表类命令合并结果,操作类命令要求指定目标;目标离线、不存在或 ID 前缀有歧义时,返回各自的错误码。sctl browsers [list] / forget、sctl tabs list / open / close / activate、sctl windows list;sctl status会列出浏览器实例;sctl connect的提示同时覆盖两个扩展。browsers_list、tabs_*、windows_list,除browsers_list外都带可选的browser参数。工具描述是静态文本,页面标题和 URL 只作为数据返回,不会拼进描述。extension/:pnpm、Vite、React、TypeScript、Tailwind v4、shadcn/ui,配 ESLint、Prettier、Vitest。WebSocket 由 offscreen 文档持有。MV3,最低 Chrome 116,权限只有tabs、storage、offscreen。弹窗采用「同门」风格,共 11 个状态,支持深浅色和中英文;配色满足 4.5:1 对比度。extensionCI 任务;Release 附带sctl-browser-extension-<version>.zip。browsers.json)、development、mcp 和两份 README。期间修掉的问题
TestBlockingProgressAndCancel的偶发失败在基线上就存在,根因是测试用 sleep 控制顺序,已改为基于信号的同步。验证
自动化检查(交付树
7074c44)go build、go vet、go test ./... -race、golangci-lint、make protocol-check全部 exit 0。真机验证
pairing.key不变。release.yaml的步骤打包:得到sctl-browser-extension-0.2.0-rc.1.zip,manifest 中 version 为 0.2.0,version_name 为 0.2.0-rc.1。已知未观察到的项(已接受,作为已知缺口)
OPERATION_EXPIRED、两个浏览器并发使用同一配对码。不在本期范围
第 2 期浏览器数据管理(书签、阅读列表、历史、下载、Cookie 等)、第 3 期页面自动化(CDP)、第 4 期调试、第 5 期 Playwright 端点,各自另写 spec。