dsh-inner-browser-use 0.18.0 → 0.34.28
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/client.js +103 -8
- package/docs/ARTIFACTS.md +19 -0
- package/docs/BENCH.md +485 -0
- package/docs/ROADMAP.md +146 -0
- package/docs/VERIFY.md +14 -0
- package/index.js +320 -29
- package/package.json +9 -3
- package/read.js +403 -39
- package/review.js +213 -0
- package/sidecar.js +717 -40
- package/tools.js +764 -115
package/docs/BENCH.md
CHANGED
|
@@ -1,3 +1,238 @@
|
|
|
1
|
+
## Phase 44 — 真站页游场地,0.34.28
|
|
2
|
+
|
|
3
|
+
`node scripts/check-game.mjs <url>`,真 Chrome、自己的 profile、跑完自己关掉。两个真站:
|
|
4
|
+
|
|
5
|
+
| 站点 | 空闲时页面刷新 | 全屏截图(`sidecar_shot` 同类)| 按住 `d` 200 ms 到达页面 |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| 奥拉星 `http://aoqi.100bt.com/` | **27 fps** | 中位数 **68 ms** | 是 |
|
|
8
|
+
| 4399 `https://www.4399.com/` | **59.8 fps** | 中位数 **148 ms** | 是 |
|
|
9
|
+
|
|
10
|
+
两个落地页都**没有 canvas**:游戏本体在链接后面,要么登录要么再点进去一层,所以这组数字说明的是「真站能不能实时跑」,不是「游戏画面已经能读」。
|
|
11
|
+
|
|
12
|
+
诚实的一层:按键到达页面这件事在真站已经证实(页面自己记录的 `keydown`/`keyup`),但 4399 这种整屏重绘的页面,一张全屏图 1.8 MB、148 ms —— 做实时画面要压、要裁、要降缩放,这一层还没做,别拿它当「已经能玩」。
|
|
13
|
+
|
|
14
|
+
## Phase 43 — 页游级输入:按住、组合键、按住鼠标,0.34.26
|
|
15
|
+
|
|
16
|
+
本地画布页(每帧重绘,页面自己记录 `keydown`/`keyup`/`pointerdown`/`pointerup` 时间戳),真 Chrome,`node scripts/bench.mjs`:
|
|
17
|
+
|
|
18
|
+
| 项 | 数字 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `sidecar_key` 带 `hold_ms: 300` 整次调用 | **305 ms**(含 300 ms 按住,收尾 5 ms) |
|
|
21
|
+
| 页面测到的按住时长 | **301 ms** |
|
|
22
|
+
| `ArrowLeft+ArrowUp` | 两键都收到 `keydown`,都收到 `keyup`,顺序为倒序松开 |
|
|
23
|
+
| `sidecar_click` 带 `hold_ms: 200` 页面测到 | **211 ms** |
|
|
24
|
+
|
|
25
|
+
对照 0.34.25 之前:同样的按住被 `settle` 拖到 727 ms,等于 0.7 s 才走一步——页游里这就是掉帧。
|
|
26
|
+
|
|
27
|
+
## Phase 42 — the reload path, proved by two instances, 0.34.25
|
|
28
|
+
|
|
29
|
+
`npm run adopt`(真 Chrome,临时 profile `adopt-check`,临时状态目录):
|
|
30
|
+
|
|
31
|
+
| 项 | 数字 |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| 第一个实例冷启动到页面可用 | 675 ms |
|
|
34
|
+
| 第二个实例(模拟重载后的半边)接管 | 162 ms |
|
|
35
|
+
| 第二个实例自己起的 Chrome | `null`(没有起第二个) |
|
|
36
|
+
| 浏览器进程 | 同一个 pid,接管后仍活着 |
|
|
37
|
+
| 页签与标题 | `title: "kept"`,内容原样 |
|
|
38
|
+
|
|
39
|
+
这就是重载后「接回同一个浏览器、不重启、不丢页签和登录」的机器证据;之前只有手工重载才能走到这条路。
|
|
40
|
+
|
|
41
|
+
## Phase 41 — the read tool that had never run, 0.34.23
|
|
42
|
+
|
|
43
|
+
`node scripts/bench.mjs`(本机 headless 页面,Chrome 复用 `bench` profile):
|
|
44
|
+
|
|
45
|
+
| 项 | 数字 |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| 首次画面(复用已在跑的浏览器) | 133 ms |
|
|
48
|
+
| 首帧字节 | 76 KB |
|
|
49
|
+
| 帧率 | 60 fps |
|
|
50
|
+
| 镜像延迟 | 中位 8 ms / 最大 8 ms |
|
|
51
|
+
| 遮挡检测 | 中位 3 ms / 最大 15 ms |
|
|
52
|
+
| 点一下(普通页 / 有遮罩,含读回) | 439 / 590 ms,遮罩那次 `cleared: true` |
|
|
53
|
+
| 一步 `sidecar_do` | 440 / 590 ms |
|
|
54
|
+
| 截图 | 73 ms / 115 KB |
|
|
55
|
+
| 读 dense 页 text 档 | 6 ms / 1484 字 / ≈436 token |
|
|
56
|
+
| 读 dense 页 forms 档 | 1 ms / 432 字 / ≈127 token |
|
|
57
|
+
| 再读同一页 | 1–2 ms,给出「没有变化」一行 |
|
|
58
|
+
| `changed_only` | 58 字 / ≈17 token(整页 1505 字) |
|
|
59
|
+
|
|
60
|
+
这轮 bench 的作用不是数字,而是**它第一次跑到 `sidecar_view` 就报 `ReferenceError` 退出**:那个工具在真实会话里同样会失败,而单测只测了它的纯函数部分。修完之后 bench 一次性跑完,数字在上面。
|
|
61
|
+
|
|
62
|
+
## Phase 40 — the poll that names its chat, 0.34.19
|
|
63
|
+
|
|
64
|
+
The adoption rule from 0.34.16 never fired for the column itself: the pane
|
|
65
|
+
polled `/sidecar/state` without a session, the host treated that as anonymous,
|
|
66
|
+
and a reload could still leave the mirror empty over a live browser. The pane now
|
|
67
|
+
names its chat, and asks again the moment a claim succeeds.
|
|
68
|
+
|
|
69
|
+
No performance change was expected; the full bench was re-run anyway so the
|
|
70
|
+
numbers under it are current (commit 0.34.19, local pages):
|
|
71
|
+
|
|
72
|
+
| Label | Value |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| launch → first frame | 1029 ms |
|
|
75
|
+
| steady stream | 60 fps, 76 KB per frame |
|
|
76
|
+
| mirror delay (12 samples) | median 9 ms, max 13 ms |
|
|
77
|
+
| dense page, text | 6 ms, 1484 chars, ≈436 tokens |
|
|
78
|
+
| dense page, forms | 2 ms, 432 chars, ≈127 tokens |
|
|
79
|
+
|
|
80
|
+
## Phase 39 — count what is on the screen, not what is in the DOM, 0.34.17
|
|
81
|
+
|
|
82
|
+
`readFrames` counts child frames; the page's own count filters to frames that
|
|
83
|
+
occupy the screen. Taking the larger made a hidden 1×1 frame claim a blind spot.
|
|
84
|
+
|
|
85
|
+
- `blindSpots` now lets the page's visible count win; the reader's count is the
|
|
86
|
+
fallback for when the page could not count.
|
|
87
|
+
- Live: two frames (400×300 and 1×1) → `blind.frames` 1, exactly one line.
|
|
88
|
+
|
|
89
|
+
## Phase 38 — a reload must not blank the mirror, 0.34.16
|
|
90
|
+
|
|
91
|
+
`/sidecar/state` answered from the new plugin instance, which owns nothing until
|
|
92
|
+
something calls `open`: after a reload the owning chat's own poll said
|
|
93
|
+
`alive:false`, hence "no page open" in the column while Chrome was fine.
|
|
94
|
+
|
|
95
|
+
- The owning chat's state poll adopts the running browser and the tab remembered
|
|
96
|
+
for it before answering.
|
|
97
|
+
- Live: `/dsh-unlag/reload` 200 → state `alive True`, url `https://www.doubao.com/chat/`,
|
|
98
|
+
Chrome pid 12833 with 11 m uptime, i.e. attached rather than relaunched.
|
|
99
|
+
- Non-owning chats and empty `session` polls still answer without spawning anything.
|
|
100
|
+
|
|
101
|
+
## Phase 37 — an observation that admits its blind spot, 0.34.15
|
|
102
|
+
|
|
103
|
+
A text read and a screenshot claim to show the page. Both are lossy, and the loss
|
|
104
|
+
was silent: a chart wired as `<canvas>`, a generated image with no alt text, a
|
|
105
|
+
video, a cross-origin frame — nothing in the text said "there is something here
|
|
106
|
+
you cannot read". A model then planned as if the page were fully described.
|
|
107
|
+
|
|
108
|
+
- `readPage` counts the unreadable visible things on the page and in frames, and `renderPage` prints exactly one line when the count is non-zero; zero adds no line (measured token cost of the feature is 0 on pages that have nothing to hide).
|
|
109
|
+
- `chrome-error://` is no longer rendered as an empty page: it says the page did not load and names `sidecar_nav`.
|
|
110
|
+
- `navigate` retries that error page once (300 ms + 8 s cap). Cold launch on a fresh profile: first navigation recovered on the retry; an unreachable host still returns `bad` in well under the cap.
|
|
111
|
+
- `sidecar_shot` without an image channel appends what the result is missing and which tool fills it; file path alone is no longer the whole answer.
|
|
112
|
+
- Suite: 101 tests, 0 failures.
|
|
113
|
+
|
|
114
|
+
## Phase 36 — a reload must not cost the browser, 0.34.14
|
|
115
|
+
|
|
116
|
+
- 根因:交接看门狗 `while kill -0 ${process.pid}` 盯的是插件实例 pid,`dsh-unlag` 重载换 pid 后看门狗立刻 kill 共享 Chrome —— 镜像白屏、会话掉回 about:blank。
|
|
117
|
+
- 改为盯应用 pid(`ps -o ppid=` 向上走到顶)+ `watcher.json` 去重;实测 node 14185 的应用 pid 是 13663。
|
|
118
|
+
- `ensure()` 无活页面时回落到 `rememberedTab(id)`,首开即回原地址。
|
|
119
|
+
- 测试 97 条全绿。
|
|
120
|
+
|
|
121
|
+
## Phase 35 — a review round that can be checked off, 0.34.13## Phase 35 — a review round that can be checked off, 0.34.13
|
|
122
|
+
|
|
123
|
+
- `markRound()` 追加 verdict 行,`lastRound()` 折回 `marks`;`brief({already})` 只保留 open 条目,实测提问里出现「Already handled from your previous review」,已处理点不再重复。
|
|
124
|
+
- 工具面新增 `sidecar_review_verdict`(`mark: [{point,status,evidence}]`,状态枚举 fixed/verified/invalid/open);越界编号丢弃、未知状态记为 open。
|
|
125
|
+
- 测试 92 → 96 条全绿。
|
|
126
|
+
|
|
127
|
+
## Phase 34 — one gate in front of every write, 0.34.12## Phase 34 — one gate in front of every write, 0.34.12
|
|
128
|
+
|
|
129
|
+
- `acting()` 统一 400(未命名会话)与 403(非持有者);实测匿名 `POST /sidecar/nav` → 400 `which chat asked for it`,非持有者 `POST /sidecar/stop` → 403,持有者 `forget` → 200。
|
|
130
|
+
- 测试 90 → 92 条,全绿;文案与样式 lint 无 error。
|
|
131
|
+
|
|
132
|
+
## Phase 33 — a lost picture comes back in one click, 0.34.11
|
|
133
|
+
|
|
134
|
+
- 实测「切走 / 重载后画面没了」的下一步:`/sidecar/state?session=…` 与画面流 hello 都给出 `chat.remembered`(该会话最后地址)。
|
|
135
|
+
- 右栏在没有画面且 `remembered` 有值时显示「恢复上次的页面」,点击 POST `/sidecar/open` 并重连画面流。
|
|
136
|
+
- 同一会话第二次 open 命中已有标签页:107 ms(三次实测 2.08 s / 0.107 s / 0.108 s,首次含导航)。
|
|
137
|
+
|
|
138
|
+
## Phase 32 — a call log belongs to its chat, 0.34.10
|
|
139
|
+
|
|
140
|
+
`GET /sidecar/state` returned the last 40 mirror calls of every chat, each with
|
|
141
|
+
an eight-character session id: one conversation could read where another had
|
|
142
|
+
been browsing. The list is now filtered to the caller's own chat. The owner and
|
|
143
|
+
the list of listening chats stay global, because those are what the column needs
|
|
144
|
+
to claim the mirror.
|
|
145
|
+
|
|
146
|
+
## Phase 31 — a reload keeps the mirror, 0.34.9
|
|
147
|
+
|
|
148
|
+
`ctx.effect` cleanup ran `sidecar.shutdown()`, so each plugin reload closed
|
|
149
|
+
Chrome. The page disappeared from the column and the next open was a cold
|
|
150
|
+
start again. Cleanup now hands the browser to the next driver: cookies and a
|
|
151
|
+
login snapshot hit disk, the browser keeps running, and a detached watcher
|
|
152
|
+
closes it only after the app process is gone.
|
|
153
|
+
|
|
154
|
+
| after a `dsh-unlag` reload | before | now |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| browser | closed, page lost | still running, page kept |
|
|
157
|
+
| next interaction | cold launch (≈0.6 s, was 21 s) | attach, no launch |
|
|
158
|
+
|
|
159
|
+
## Phase 30 — a brand-new chat's first tab, 0.34.8
|
|
160
|
+
|
|
161
|
+
A fresh profile with no memory of a page means `openTab` asks for `about:blank`.
|
|
162
|
+
`navigate` then polled for a non-blank address, which a blank page never becomes,
|
|
163
|
+
and spent all 200 tries. Every first open of a new chat paid it.
|
|
164
|
+
|
|
165
|
+
| cold start, fresh profile (`about:blank`) | before | now |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| round 1 | 21 322 ms | 817 ms |
|
|
168
|
+
| round 2 | 20 882 ms | 607 ms |
|
|
169
|
+
| round 3 | 20 900 ms | 516 ms |
|
|
170
|
+
| page settings round trips | 4 | 1 |
|
|
171
|
+
|
|
172
|
+
Measured with three launches of `Sidecar#ensure` against a throwaway profile;
|
|
173
|
+
the first launch is now about 26× faster than before.
|
|
174
|
+
|
|
175
|
+
## Phase 29 — a stale session cookie is not written back, 0.34.7
|
|
176
|
+
|
|
177
|
+
`restoreSessionCookies` replayed every kept session cookie on launch, including
|
|
178
|
+
ones the browser already had. A rotated `sessionid` from the note would replace
|
|
179
|
+
the current one, and the site answers a retired session with a sign-in page.
|
|
180
|
+
|
|
181
|
+
| case | before | now |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| note holds sessionid, browser holds rotated one | old value written | nothing written |
|
|
184
|
+
| note holds a cookie the browser lost | written | written |
|
|
185
|
+
| cookie key | — | domain + path + name |
|
|
186
|
+
|
|
187
|
+
86 tests complete the suite in about 2 s.
|
|
188
|
+
|
|
189
|
+
## Phase 28 — one chat, one tab, even in parallel, 0.34.6
|
|
190
|
+
|
|
191
|
+
`tabFor` checked `pages`, then did async work, then wrote `pages`: two calls that
|
|
192
|
+
started together both missed the map and both opened a tab. The orphan tab kept
|
|
193
|
+
a live page nobody could see or stop.
|
|
194
|
+
|
|
195
|
+
| case | before | now |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| two parallel calls, cold chat | 2 tabs, 1 orphan | 1 tab |
|
|
198
|
+
| later call with another address | its own tab | same tab, navigated |
|
|
199
|
+
| leftover pending note | — | cleared in `finally` |
|
|
200
|
+
|
|
201
|
+
The fix is a per-chat promise in `opening`, joined by the second caller.
|
|
202
|
+
|
|
203
|
+
## Phase 27 — one gate for the mirror, 0.34.5
|
|
204
|
+
|
|
205
|
+
Three routes each carried their own copy of the ownership check. Copies drift:
|
|
206
|
+
a route added later is exactly the one nobody remembers to guard.
|
|
207
|
+
|
|
208
|
+
| case | result |
|
|
209
|
+
|---|---|
|
|
210
|
+
| rival chat → `/sidecar/input` | 403, owner named |
|
|
211
|
+
| rival chat → `/sidecar/nav` | 403, owner named |
|
|
212
|
+
| rival chat → `/sidecar/frames` | 403, no stream opened |
|
|
213
|
+
| unit tests | 84 |
|
|
214
|
+
|
|
215
|
+
The gate reads one function, so the question "may this chat act?" has one answer
|
|
216
|
+
in one place.
|
|
217
|
+
|
|
218
|
+
## Phase 26 — a screenshot the model can see, 0.34.3
|
|
219
|
+
|
|
220
|
+
Observation was structured text only: `sidecar_shot` wrote a PNG and returned
|
|
221
|
+
a path, which is a file name the model cannot look at. Codex and Claude Code
|
|
222
|
+
both hand screenshots back as image content, and that is the difference
|
|
223
|
+
between planning from the picture and guessing.
|
|
224
|
+
|
|
225
|
+
| case | before | now |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| route with image input | path only | path + image block, same call |
|
|
228
|
+
| route without image input | path | path, no image block, no error |
|
|
229
|
+
| no attachment store | path | path |
|
|
230
|
+
| read state across chats | one shared object | one per chat |
|
|
231
|
+
| unit tests | 79 | 81 |
|
|
232
|
+
|
|
233
|
+
Same-call cost: the PNG is already in memory for the file write, so the image
|
|
234
|
+
block adds one attachment save, no extra round trip and no second screenshot.
|
|
235
|
+
|
|
1
236
|
<!-- ui-copy-ignore -->
|
|
2
237
|
# Measured, not claimed
|
|
3
238
|
|
|
@@ -232,3 +467,253 @@ Measured with `/tmp/probe-unseen.mjs` (tool layer, example.com, profile
|
|
|
232
467
|
`probe-tool`): 1344 ms with nothing watching and no column asked for, 3236 ms in
|
|
233
468
|
the first version of the same wait that did not skip the case where no column was
|
|
234
469
|
ever requested — the whole difference was the wasted 1.2 s.
|
|
470
|
+
|
|
471
|
+
## Phase 9 — finding the field behind the words, 0.19.0
|
|
472
|
+
|
|
473
|
+
A read of a `/bare` form (two div labels, two bare inputs, no aria anywhere on
|
|
474
|
+
the fields) and one `{field: "Work email"}` step:
|
|
475
|
+
|
|
476
|
+
| what | result |
|
|
477
|
+
| --- | --- |
|
|
478
|
+
| the click lands on the field, not on the div | yes, value `me@x.y` in `input[name=wk]` |
|
|
479
|
+
| the second, untouched field | still empty |
|
|
480
|
+
| the tool's own words | `typed into "Work email"` |
|
|
481
|
+
| worst case when nothing matches | one extra in-page query, then the old behaviour |
|
|
482
|
+
|
|
483
|
+
The query walks the open shadow roots (capped at 400) and, per visible editable
|
|
484
|
+
element, builds a list of candidate names from real labels, `aria-labelledby`,
|
|
485
|
+
`aria-label`, `placeholder`, `title`, `name`, `label[for]`, the element right
|
|
486
|
+
before it, and its wrapper's text — then scores exact (3) / contains (2) /
|
|
487
|
+
reverse-contains (1).
|
|
488
|
+
|
|
489
|
+
## Phase 10 — typing by name, 0.20.0
|
|
490
|
+
|
|
491
|
+
| what the caller has | what it used to take | now |
|
|
492
|
+
| --- | --- | --- |
|
|
493
|
+
| a name for the field, no number | read the page, find the number, then type | one `sidecar_type` with `field`, one round trip |
|
|
494
|
+
| the field's words live in a div | click landed on the div, text went wherever focus was | resolved to the field, `typed into "Work email"` |
|
|
495
|
+
| a banner over the field | typed under the banner | banner cleared first, then typed |
|
|
496
|
+
|
|
497
|
+
Breadth check `type-by-words` on the `/bare` form: value lands in
|
|
498
|
+
`input[name=wk]`, the page does not move (no stray Enter).
|
|
499
|
+
|
|
500
|
+
## Phase 11 — choosing a dropdown by name, 0.21.0
|
|
501
|
+
|
|
502
|
+
`/bare` now carries a `select[name=seats]` whose only name on screen is a div
|
|
503
|
+
saying `Seats`. One `sidecar_select` with `field: "Seats"`, `value: "Two"`:
|
|
504
|
+
|
|
505
|
+
| what | result |
|
|
506
|
+
| --- | --- |
|
|
507
|
+
| the value after the call | `2` |
|
|
508
|
+
| the tool's own words | `chose Two in Seats` (on-screen name, not `name="seats"`) |
|
|
509
|
+
| a wrong option | answered with the first 12 options instead of a page read |
|
|
510
|
+
| round trips to do it | one; no read beforehand, no read after |
|
|
511
|
+
|
|
512
|
+
The naming rules (`labelSpot` and `selectSpot`) now live in one shared in-page
|
|
513
|
+
block, so the two steps cannot drift apart.
|
|
514
|
+
|
|
515
|
+
## Phase 12 — words split across nodes, 0.22.0
|
|
516
|
+
|
|
517
|
+
`/words` now carries `<button><span>Sign</span><span>in</span></button>`, which
|
|
518
|
+
the page reports as `Signin`.
|
|
519
|
+
|
|
520
|
+
| what | result |
|
|
521
|
+
| --- | --- |
|
|
522
|
+
| `{text: "Sign in"}` before | not found; caller had to fall back to a number |
|
|
523
|
+
| `{text: "Sign in"}` now | clicked, title changes, `ok click "Sign in"` |
|
|
524
|
+
| extra work | none: the same single control list, one comparison changed |
|
|
525
|
+
|
|
526
|
+
Exact equality still wins over the squeezed form, so `Sign in` never lands on
|
|
527
|
+
`Sign up` when both exist.
|
|
528
|
+
|
|
529
|
+
## Phase 13 — the field behind the box, 0.23.0
|
|
530
|
+
|
|
531
|
+
`/bare` gains the date-picker shape: a visible rounded box with a transparent
|
|
532
|
+
`<input>` laid over it, its words in a div two levels up.
|
|
533
|
+
|
|
534
|
+
| what | before | now |
|
|
535
|
+
| --- | --- | --- |
|
|
536
|
+
| `{field: "Birthday"}` | not found, fell back to clicking the div | found the hidden input, clicked its visible box |
|
|
537
|
+
| value after one `sidecar_do` step | whatever already had focus | `2001-02-03` in `#bd`, `typed into "Birthday"` |
|
|
538
|
+
| extra round trips | read the page, guess a number | none |
|
|
539
|
+
|
|
540
|
+
The visible-box rule: a candidate is usable when it, or its nearest visible
|
|
541
|
+
ancestor, has a real box — so a 0-opacity input still counts if it covers
|
|
542
|
+
something the person can see.
|
|
543
|
+
|
|
544
|
+
## Phase 15 — the answer is the picture, 0.25.0
|
|
545
|
+
|
|
546
|
+
`/pic` is one 140x90 PNG and nothing else.
|
|
547
|
+
|
|
548
|
+
| what | result |
|
|
549
|
+
| --- | --- |
|
|
550
|
+
| `sidecar_grab` | 6 ms, `picture-check.png`, 140x90 |
|
|
551
|
+
| before | three round trips: list the images, ask for the full address, fetch it |
|
|
552
|
+
|
|
553
|
+
A picture page has no text at all, so a read of it costs one line and no tokens;
|
|
554
|
+
the picture comes back as a file path the person can open.
|
|
555
|
+
|
|
556
|
+
## Phase 16 — say what the picture looks like, 0.26.0
|
|
557
|
+
|
|
558
|
+
Two 140x90 PNGs: one a gradient, one a single colour.
|
|
559
|
+
|
|
560
|
+
| page | what `sidecar_grab` answers | time |
|
|
561
|
+
| --- | --- | --- |
|
|
562
|
+
| `/pic` | `... 140x90 a picture with detail` | 21 ms (first, page cold) |
|
|
563
|
+
| `/flat` | `... 140x90 mostly one colour` | 4 ms |
|
|
564
|
+
|
|
565
|
+
The sampling is 32x32 inside the page, so it costs no round trip and its result
|
|
566
|
+
travels in the same line as the path.
|
|
567
|
+
|
|
568
|
+
## Phase 17 — the answer is a picture, in the read itself, 0.27.0
|
|
569
|
+
|
|
570
|
+
| page | read line | extra round trips |
|
|
571
|
+
| --- | --- | --- |
|
|
572
|
+
| `/pic` (gradient, no words) | `picture 140x90 a picture with detail "Chart" — sidecar_grab saves it as a file` | 0 |
|
|
573
|
+
| `/flat` (single colour) | `picture 140x90 mostly one colour "Chart" — …` | 0 |
|
|
574
|
+
| `/words` (text page) | no picture line | 0 |
|
|
575
|
+
|
|
576
|
+
The sampling is 32x32 inside the page, in the same call that reads the page, so a
|
|
577
|
+
blank chart is visible before anything is saved.
|
|
578
|
+
|
|
579
|
+
## Phase 18 — the picture that never arrived, 0.28.0
|
|
580
|
+
|
|
581
|
+
| page | read line | time |
|
|
582
|
+
| --- | --- | --- |
|
|
583
|
+
| `/broken` (320x240 `<img>` pointing at a 404, no words) | `picture 320x240 "Chart" did not load — the page may need a reload, or the picture is gone` | 123 ms (cold page) |
|
|
584
|
+
|
|
585
|
+
A failed image collapses to an error icon, so the declared `width`/`height` is
|
|
586
|
+
what the page meant to show; the read reports that size, not the icon's.
|
|
587
|
+
|
|
588
|
+
## Phase 19 — the page said again, 0.29.0
|
|
589
|
+
|
|
590
|
+
| read | characters | time |
|
|
591
|
+
| --- | --- | --- |
|
|
592
|
+
| first read of `/dense` (40 paragraphs) | 2057 | — |
|
|
593
|
+
| second read, nothing changed | 105 | 1 ms |
|
|
594
|
+
|
|
595
|
+
The comparison is free: both reads already happened. Only the report shrinks.
|
|
596
|
+
A changed scroll offset is still reported, so "nothing changed" is never a lie.
|
|
597
|
+
|
|
598
|
+
## Phase 20 — one call carries the question and the answer, 0.30.0
|
|
599
|
+
|
|
600
|
+
| step | before | `sidecar_ask` |
|
|
601
|
+
| --- | --- | --- |
|
|
602
|
+
| type the message | `sidecar_type` + `submit` | same, inside the call |
|
|
603
|
+
| read the page | `sidecar_view` | — |
|
|
604
|
+
| read it again until the answer stops growing | 2–6 more `sidecar_view` calls | — |
|
|
605
|
+
| **turns for a chat reply** | 3–7 | **1** |
|
|
606
|
+
| measured `sidecar_ask` on `/chat` (answer over 2 arrivals, 700 ms tail) | — | 2.5 s, one turn |
|
|
607
|
+
|
|
608
|
+
## Phase 21 — a review round that closes the loop, 0.31.0
|
|
609
|
+
|
|
610
|
+
| step | before | `sidecar_review` |
|
|
611
|
+
| --- | --- | --- |
|
|
612
|
+
| write the request (repo, commit, focus) | by hand, every round | built from three fields |
|
|
613
|
+
| send it, wait for the answer | `sidecar_ask` | same call |
|
|
614
|
+
| turn the answer into a list | read it, retype it | parsed, numbering kept |
|
|
615
|
+
| know what the last round was about | — | round log, with the commit and the time |
|
|
616
|
+
| **turns for a review round** | 3–7 + bookkeeping | **1** |
|
|
617
|
+
| measured round on `/chat` (echoed question + 2-item list) | — | 2.6 s, 2 points |
|
|
618
|
+
|
|
619
|
+
## Phase 21b — an answer that is not an answer, 0.31.1
|
|
620
|
+
|
|
621
|
+
A chat box on a page that is not signed in still accepts text, still redraws,
|
|
622
|
+
and still looks busy. Three cheap checks keep that from being reported as a
|
|
623
|
+
review.
|
|
624
|
+
|
|
625
|
+
| what the page does | before | now | cost |
|
|
626
|
+
| --- | --- | --- | --- |
|
|
627
|
+
| keeps the words in the box | waits 4 s, then reads the page | says so at 0.5 s | 1 evaluate |
|
|
628
|
+
| redraws but never shows the message | the new words were the answer | says the page did not take it | none (text compare) |
|
|
629
|
+
| shows the message, no reply | "answered in prose" | "has the message but has not answered yet" | none (text compare) |
|
|
630
|
+
| real page, signed out (Doubao) | a review-shaped paragraph of menu chips | the sign-in is named, 2.6 s | — |
|
|
631
|
+
|
|
632
|
+
## Phase 22 — the change travels with the request, 0.32.0
|
|
633
|
+
|
|
634
|
+
The reviewer inside the column cannot open a repository. Asking it to review
|
|
635
|
+
"the newest commit" of a repository it cannot see is asking for a guess.
|
|
636
|
+
|
|
637
|
+
| request | what the reviewer gets | cost |
|
|
638
|
+
| --- | --- | --- |
|
|
639
|
+
| repo + commit | an address and a hash it cannot open | 1 message |
|
|
640
|
+
| repo + commit + diff | the change itself, capped at 5000 characters | +diff characters in that one message |
|
|
641
|
+
|
|
642
|
+
Measured: `review-carries-diff` green in 2.6 s; the recorded round carries
|
|
643
|
+
`+const cap = 60` verbatim.
|
|
644
|
+
|
|
645
|
+
## Phase 22b — the mirror outlives the visit, 0.33.0
|
|
646
|
+
|
|
647
|
+
A column tab only exists while the chat is on screen. The ask used to be a
|
|
648
|
+
single frame pushed at request time: if that chat had no stream, the frame was
|
|
649
|
+
lost and the mirror never appeared on return.
|
|
650
|
+
|
|
651
|
+
| situation | before | now |
|
|
652
|
+
| --- | --- | --- |
|
|
653
|
+
| the chat is on screen | tab opens | tab opens |
|
|
654
|
+
| the chat is elsewhere, then returns | nothing | tab is waiting |
|
|
655
|
+
| the page was reloaded | nothing | tab is waiting |
|
|
656
|
+
| the ask is 3 minutes old | nothing | nothing (spent) |
|
|
657
|
+
| a different chat comes back | nothing | nothing (not its mirror) |
|
|
658
|
+
|
|
659
|
+
Data flow never needed the column: measured with zero viewers — boot 626 ms,
|
|
660
|
+
click and read back 114 ms, read 4 ms, shot 48 ms / 12.5 KB.
|
|
661
|
+
|
|
662
|
+
## Phase 22c — a duplicate cannot take the pane down, 0.33.2
|
|
663
|
+
|
|
664
|
+
A scan of the six shipped modules finds no repeated top-level name, no module
|
|
665
|
+
imported twice, no two tools with one name, one tab id and one style sheet.
|
|
666
|
+
The two places a reload can still meet itself are guarded instead of trusted:
|
|
667
|
+
|
|
668
|
+
| case | before | now |
|
|
669
|
+
| --- | --- | --- |
|
|
670
|
+
| same tool name registered again | thrown error, surface down | warning, first one kept |
|
|
671
|
+
| same tab mounted again | possible duplicate registration | no-op |
|
|
672
|
+
| style sheet re-applied | second sheet stacked | replaced by id |
|
|
673
|
+
| two mount entry points | possible double effect | one entry per half |
|
|
674
|
+
|
|
675
|
+
Guards are covered by `test/no-duplicate.test.mjs` plus a double-mount test:
|
|
676
|
+
65 unit tests, 66 breadth checks, 20 packaged files.
|
|
677
|
+
|
|
678
|
+
## Phase 25 — one chat cannot take another chat's page, 0.34.2
|
|
679
|
+
|
|
680
|
+
The audit on 0.34.0 named five things a driver written for one chat got wrong.
|
|
681
|
+
Each was reproduced against 0.34.1 first: two were already fixed (the
|
|
682
|
+
`sessionScope`/`KEYS` import, and every `ensure()` route already carrying a
|
|
683
|
+
session), three were real.
|
|
684
|
+
|
|
685
|
+
| What | Before | Now |
|
|
686
|
+
| --- | --- | --- |
|
|
687
|
+
| popup lookup | any non-main tab, then navigated or closed | only a target that is neither this page nor another chat's (`isForeign`) |
|
|
688
|
+
| focused password in a result line | echoed the `.value` (`DUMMY_SECRET_123`) | name only, value always empty |
|
|
689
|
+
| `sidecar_do` on a step that did not land | kept going, typed into the wrong page | stops at that step, says to re-plan |
|
|
690
|
+
| overlay that would not clear | clicked the control under it anyway | refuses the click/typing, names the overlay |
|
|
691
|
+
| `changed_only` twice | repeated the same change | diff becomes the new baseline |
|
|
692
|
+
| `/sidecar/state` | top-level `url` could be another chat's tab | `?session=` adds `chat.url/title/viewers` (live: doubao page, viewers 1) |
|
|
693
|
+
| suite | 77 pass | 79 pass (isolation, leak) |
|
|
694
|
+
|
|
695
|
+
## Phase 24 — a number that is checked before it is used, 0.34.1
|
|
696
|
+
|
|
697
|
+
| What | Before | Now |
|
|
698
|
+
| --- | --- | --- |
|
|
699
|
+
| ref on a control whose words changed | pressed the new control | refused, says both words (0 ms extra on an unchanged ref) |
|
|
700
|
+
| sidecar call after a hot reload | `sessionScope is not defined` | works; the Host half is checked by a test |
|
|
701
|
+
|
|
702
|
+
## Phase 23 — one tab per chat, typing that looks like hands, 0.34.0
|
|
703
|
+
|
|
704
|
+
The browser used to be one page for the whole app, so any chat could navigate
|
|
705
|
+
the page another chat was reading. Tabs are now per chat; one windowless Chrome
|
|
706
|
+
and one profile still back all of them, so a login is shared and an address is
|
|
707
|
+
not.
|
|
708
|
+
|
|
709
|
+
| what | measured |
|
|
710
|
+
| --- | --- |
|
|
711
|
+
| first chat, browser cold | 725 ms to a driven page |
|
|
712
|
+
| second chat, own tab | 1 round trip, no second Chrome |
|
|
713
|
+
| chat A navigates | chat B's URL and the first tab unchanged |
|
|
714
|
+
| frames per chat | `/sidecar/frames?session=` streams only that tab |
|
|
715
|
+
| paste, 46 chars | 5 ms |
|
|
716
|
+
| human typing, 46 chars | 1965–2238 ms (43–49 ms per char) |
|
|
717
|
+
| human typing, >160 chars | 8-char bursts, 16–58 ms gaps, ≈6 ms per char |
|
|
718
|
+
| headless UA | desktop Chrome UA, no `HeadlessChrome` |
|
|
719
|
+
| suite | 70 pass, breadth 66, ui-copy clean |
|
package/docs/ROADMAP.md
CHANGED
|
@@ -174,3 +174,149 @@ Next in this direction: the drawing half could re-attach by itself. A tab that
|
|
|
174
174
|
reloads re-subscribes on its own, but a tab that never loaded the half cannot be
|
|
175
175
|
told to — that is a load-order problem, so the fix belongs in how the panel half
|
|
176
176
|
is delivered, not in the wait.
|
|
177
|
+
|
|
178
|
+
## Phase 10 — words are not where the field is, 0.19.0
|
|
179
|
+
|
|
180
|
+
The tool used to treat "the control that says Email" as "the field called
|
|
181
|
+
Email". Real forms separate the two, so the writing step now resolves the words
|
|
182
|
+
to the element that takes text, and says which one it used.
|
|
183
|
+
|
|
184
|
+
Next in this direction: the same resolution for `sidecar_type` when the caller
|
|
185
|
+
names a field by its words instead of a ref, and for date/time pickers where the
|
|
186
|
+
visible box is a div and the real input is hidden beside it.
|
|
187
|
+
|
|
188
|
+
## Phase 11 — say where, not which number, 0.20.0
|
|
189
|
+
|
|
190
|
+
Typing by name is in. The same resolution is what other steps should be able to
|
|
191
|
+
use when the caller knows the destination but not the number — the next place
|
|
192
|
+
worth doing this is `sidecar_select` (choose an option by the field it belongs
|
|
193
|
+
to), and after that the date/time case where the visible box is a div and the
|
|
194
|
+
real input sits hidden beside it.
|
|
195
|
+
|
|
196
|
+
## Phase 12 — the same words, the same place, 0.21.0
|
|
197
|
+
|
|
198
|
+
`sidecar_select` joined `sidecar_type` and `sidecar_do` on the shared resolver.
|
|
199
|
+
Left in this direction: the date/time case where the box the user sees is a div
|
|
200
|
+
and the real input is hidden beside it, and `sidecar_click` on a control whose
|
|
201
|
+
words are split across sibling nodes (`<span>Sign</span><span>in</span>`).
|
|
202
|
+
|
|
203
|
+
## Phase 13 — the page's spelling is not the caller's, 0.22.0
|
|
204
|
+
|
|
205
|
+
Whitespace is normalised in both halves now (tool side and in-page side). Next
|
|
206
|
+
in this line: the date/time box where the visible box is a div and the real
|
|
207
|
+
input is hidden beside it, which needs the resolver to accept a hidden target
|
|
208
|
+
and click the thing in front of it.
|
|
209
|
+
|
|
210
|
+
## Phase 14 — behind the box, 0.23.0
|
|
211
|
+
|
|
212
|
+
Both halves now resolve a name to a place: visible field, hidden field behind a
|
|
213
|
+
visible box, words split over nodes, words outside the wrapper. Next: a page
|
|
214
|
+
whose visible box is a `<canvas>` or an `<iframe>` — nothing in the tree to
|
|
215
|
+
name, so the fallback has to be a picture plus a click, not a name lookup.
|
|
216
|
+
|
|
217
|
+
## Phase 16 — a file, not a path, 0.25.0
|
|
218
|
+
|
|
219
|
+
`sidecar_grab` returns a path. Next: say one line about the picture (size, and
|
|
220
|
+
whether it is mostly one colour) so a model that cannot open files still learns
|
|
221
|
+
"the chart is blank" without a full read.
|
|
222
|
+
|
|
223
|
+
## Phase 17 — a picture nobody can see, 0.26.0
|
|
224
|
+
|
|
225
|
+
The description is coarse on purpose. Next: when a page's answer is a picture,
|
|
226
|
+
let a read say so in one line ("the answer is a picture, and it is blank") so a
|
|
227
|
+
model that cannot open files does not need the tool at all to notice a failure.
|
|
228
|
+
|
|
229
|
+
## Phase 18 — a picture that never arrived, 0.27.0
|
|
230
|
+
|
|
231
|
+
A picture is only an answer if it loaded. Next: report a large picture element
|
|
232
|
+
whose source failed (`naturalWidth === 0`) as a broken picture rather than
|
|
233
|
+
staying silent, since that is the commonest way a generated image "disappears".
|
|
234
|
+
|
|
235
|
+
## Phase 19 — the same picture again, 0.28.0
|
|
236
|
+
|
|
237
|
+
Reads are stateless: the same page costs the same tokens twice. Next: remember
|
|
238
|
+
the last read of a page by address and say only what changed, with a hard reset
|
|
239
|
+
when the address or the document changes.
|
|
240
|
+
|
|
241
|
+
## Phase 20 — two browsers, one conversation, 0.29.0
|
|
242
|
+
|
|
243
|
+
The column is a real browser, so the model inside it can be a reviewer, not just
|
|
244
|
+
a tool: hand it the repository address, ask for a review, read the answer back
|
|
245
|
+
through the same mirror, and take the next round from it. Nothing opens on the
|
|
246
|
+
desktop while that happens: a site that refuses a headless build gets a window
|
|
247
|
+
at -3000,-3000, and the column shows the only picture.
|
|
248
|
+
|
|
249
|
+
## Phase 21 — a review round that closes the loop, 0.30.0
|
|
250
|
+
|
|
251
|
+
`sidecar_ask` sends and returns an answer. Next: a round trip that carries the
|
|
252
|
+
repository address there and the review back, and records in the round log which
|
|
253
|
+
commit the review was about, so a later round can tell whether the advice was
|
|
254
|
+
already taken.
|
|
255
|
+
|
|
256
|
+
## Phase 22 — the reviewer sees the code, not a description of it, 0.31.0
|
|
257
|
+
|
|
258
|
+
The round log now records what was asked and what came back. Next: paste the
|
|
259
|
+
diff the review is about into the same message (bounded, so the column does not
|
|
260
|
+
fill with code), and let a later round say which points were already acted on,
|
|
261
|
+
so the loop stops repeating advice.
|
|
262
|
+
|
|
263
|
+
## Phase 23 — the drawing half, 0.31.0
|
|
264
|
+
|
|
265
|
+
A page that never shipped an image still has a canvas or a video frame; take
|
|
266
|
+
that surface as the picture when the page has no file to grab.
|
|
267
|
+
|
|
268
|
+
## Phases 24–39, condensed
|
|
269
|
+
|
|
270
|
+
The phases above were written one at a time as the work happened; the run from
|
|
271
|
+
0.32 to 0.34.18 is dense enough that the per-phase prose lives in
|
|
272
|
+
`docs/BENCH.md` and the changelog. What each one changed, in one line:
|
|
273
|
+
|
|
274
|
+
| Version | Phase | What it moved |
|
|
275
|
+
| --- | --- | --- |
|
|
276
|
+
| 0.32.0 | 22 | the diff a review is about travels with the request |
|
|
277
|
+
| 0.33.0 | 22b | the mirror outlives the visit: claim, takeover, remembered address |
|
|
278
|
+
| 0.33.2 | 22c | a duplicate registration cannot take the pane down |
|
|
279
|
+
| 0.34.0 | 23 | one tab per chat, a UA that does not say "headless", typing that looks like hands |
|
|
280
|
+
| 0.34.1 | 24 | a ref is checked against what the control says before it is used |
|
|
281
|
+
| 0.34.2 | 25 | one chat cannot take another chat's page; a password never comes back in a result |
|
|
282
|
+
| 0.34.3 | 26 | a picture reaches the model as a picture; read state is per chat |
|
|
283
|
+
| 0.34.4 | 27 | an empty mirror says what to do; remembering an address is written, not crashed |
|
|
284
|
+
| 0.34.5 | 28 | one ownership gate for every acting route |
|
|
285
|
+
| 0.34.6 | 29 | one tab per chat even when two calls race |
|
|
286
|
+
| 0.34.7 | 30 | a live cookie is never overwritten by a stale copy |
|
|
287
|
+
| 0.34.8 | 31 | a cold first tab in 0.6 s instead of 21 s |
|
|
288
|
+
| 0.34.9 | 32 | a plugin reload no longer closes the browser |
|
|
289
|
+
| 0.34.10 | 33 | a call log never shows another chat's calls |
|
|
290
|
+
| 0.34.11 | 34 | the column offers "restore the last page" in one click |
|
|
291
|
+
| 0.34.12 | 35 | every write needs the chat that asked for it |
|
|
292
|
+
| 0.34.13 | 36 | a review round is verifiable: each point marked fixed / verified / invalid / open |
|
|
293
|
+
| 0.34.14 | 36b | a reload must not cost the browser: the watchdog follows the application pid |
|
|
294
|
+
| 0.34.15 | 37 | an observation admits its blind spot: pictures, canvases, videos, frames; an error page says so |
|
|
295
|
+
| 0.34.16 | 38 | a reload does not blank the mirror: the owning chat's poll adopts the running browser |
|
|
296
|
+
| 0.34.17 | 39 | the blind-spot frame count follows the screen, not the DOM |
|
|
297
|
+
| 0.34.18 | — | the adoption rule is a named function with a test |
|
|
298
|
+
|
|
299
|
+
## 页游这一档(2026-10-28 起)
|
|
300
|
+
|
|
301
|
+
输入这一层已经到位:按住键、组合键、按住鼠标,一项 300 ms 的按住整次调用 305 ms,事件由页面自己证实。实时画面沿用同一条 CDP 帧路(60 fps、镜像延迟中位数 8 ms)。
|
|
302
|
+
|
|
303
|
+
还差的一层是「看」:4399 / 7k7k / 奥拉星这类页游把画面画在 canvas 上,文本树里没有可读的东西,判断走位、Boss 血条、按钮位置都要靠像素。本会话的模型没有视觉能力,所以这里的正确做法不是假装能玩,而是把可判读的信号做出来:canvas 局部像素采样、颜色/区域变化率、画面是否在动(`blind` 行已经在报 canvas 数量)。有视觉能力的模型接着用同一套帧路就能玩。
|
|
304
|
+
|
|
305
|
+
## Where the next round should go
|
|
306
|
+
|
|
307
|
+
Ranked by what a real task still loses:
|
|
308
|
+
|
|
309
|
+
1. **End-to-end proof of the places unit tests cannot reach.** The restore-in-one-click
|
|
310
|
+
path (0.34.11) and the mirror coming back after a reload (0.34.16) were checked by
|
|
311
|
+
HTTP and by pid, not by a person clicking. A browser-level test that opens the
|
|
312
|
+
column, kills the driver, and clicks restore is the missing evidence.
|
|
313
|
+
2. **One-click "open here" when this chat has no seat.** `state.chats` can lack this
|
|
314
|
+
chat, and the column then only tells the model to call `sidecar_open`. The button
|
|
315
|
+
belongs in the column.
|
|
316
|
+
3. **Competitor behaviours still unmatched.** Playwright MCP's accessibility refs are
|
|
317
|
+
ours, and the blind line covers the vision-less read; Stagehand v4's
|
|
318
|
+
act/observe/extract split with cached selectors is only partly ours, and its
|
|
319
|
+
self-healing of a selector between rounds has no equivalent here.
|
|
320
|
+
4. **Token numbers for a real session, not a synthetic page.** Reads are measured on
|
|
321
|
+
fixed pages; an actual 20-step task has never had its cumulative token cost
|
|
322
|
+
recorded.
|
package/docs/VERIFY.md
CHANGED
|
@@ -149,3 +149,17 @@ hand on the mirror: while the person is using it a read still works
|
|
|
149
149
|
(`hold-refuses-action`), and `sidecar_resume` hands the page back so the same
|
|
150
150
|
click goes through (`resume-lets-go`).
|
|
151
151
|
|
|
152
|
+
## Who the mirror serves
|
|
153
|
+
|
|
154
|
+
The bar that matters most cannot be reached from `node --test`, because it is
|
|
155
|
+
about a column that belongs to one conversation. With DSH running:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
node scripts/live-safety.mjs http://127.0.0.1:49552
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
It reads the state, then tries the two ways in — a frame stream and a
|
|
162
|
+
navigation — as a session that is not the owner. Both must answer 403, and the
|
|
163
|
+
owner's stream must answer 200. Three `true` lines is the bar; `open · owner
|
|
164
|
+
none` as the first line only means nothing is on screen, and the two refusals
|
|
165
|
+
still have to hold.
|