docs: client spec

This commit is contained in:
imccyu
2026-08-12 01:59:50 +08:00
parent 2c2d9f26e5
commit d91180db9a
60 changed files with 216 additions and 88 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md
2026-07-28-themed-scrollbars-and-reserved-gutter.md: 8ad5d801358823576c37d6b6824d7ce3fdcdac11
2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md: d6d2c64703df95ea2a4ff79435512eabdb3d2f81
2026-07-28-themed-scrollbars-and-reserved-gutter.md: a820a92406ce4054f16922064772d03c6ac3ab83
2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md: 80104c4aca5986c3a1f49186ac4adf3169f46da5

View File

@@ -20,7 +20,7 @@ The rules sit on `body`, not `html`. `design-platform.css` declares the `--dsw-a
The two renderings are mutually exclusive, and the exclusion is enforced rather than assumed. A non-`auto` `scrollbar-width` or `scrollbar-color` makes Chromium and Safari discard every `::-webkit-scrollbar*` rule for that element, `::-webkit-scrollbar-thumb:hover` included. Declaring both unconditionally therefore leaves the hover token rendering nowhere at all: the engines that implement the hover pseudo-element are exactly the ones the standard properties silence, and Firefox has no hover pseudo-element to fall back on. The standard properties consequently sit inside `@supports not selector(::-webkit-scrollbar)`, which is true only where the pseudo-element is unimplemented, so Firefox takes the standard path and WebKit-based engines take the pseudo-element path. The WebKit rules are not gated in turn: an engine without those pseudo-elements drops them as unknown selectors, so a gate would only restate what selector matching already does. An engine too old for the `selector()` function makes the condition invalid, which evaluates false and selects the pseudo-element path — the correct side for the pre-16.4 Safari that is the realistic case for that reading.
Both paths read one indirection pair, `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover`, bound on `body` to the l1 (base-surface) tokens. **This is the rebinding contract, and it is the part the CSS alone does not state**: an elevated surface sets `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` and `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)` on its own container, and that one rebind reaches the standard properties and the WebKit pseudo-elements together. The pair is rebound as a pair; rebinding the resting thumb alone leaves the hover state on the base-surface token. `transparent` is the pair's other legal target, added when the sidebar's bars [started following the pointer](../feature/2026-08-04-pointer-revealed-sidebar-scrollbars.md); the gate below admits those two and nothing else. The mechanically discoverable subset is owned by `packages/client/ui-theme/tests/scrollbar-styles.spec.ts`: any sheet that both scrolls and paints an elevated surface must rebind, so this note no longer maintains a complete surface inventory. Most declare the pair on the elevated card rather than on the scrolling descendant, because elevation belongs to the surface and custom properties inherit to whichever child actually scrolls.
Both paths read one indirection pair, `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover`, bound on `body` to the l1 (base-surface) tokens. **This is the rebinding contract, and it is the part the CSS alone does not state**: an elevated surface sets `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` and `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)` on its own container, and that one rebind reaches the standard properties and the WebKit pseudo-elements together. The pair is rebound as a pair; rebinding the resting thumb alone leaves the hover state on the base-surface token. `transparent` is the pair's other legal target, added when the sidebar's bars [started following the pointer](../feature/2026-08-04-pointer-revealed-sidebar-scrollbars.md); the gate below admits those two and nothing else. The mechanically discoverable subset is owned by `packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts`: any sheet that both scrolls and paints an elevated surface must rebind, so this note no longer maintains a complete surface inventory. Most declare the pair on the elevated card rather than on the scrolling descendant, because elevation belongs to the surface and custom properties inherit to whichever child actually scrolls.
Four surfaces — `Menu`, `InputBar`, `QuestionComposer`, and `TodoPanel` — were initially missed, which is why the per-sheet rebinding contract is checked mechanically rather than by inspection.

View File

@@ -20,7 +20,7 @@ Status: implemented
两种渲染互斥,而这种互斥是被强制的,不是假定的。`scrollbar-width``scrollbar-color` 只要取非 `auto`Chromium 与 Safari 就会丢弃该元素上的全部 `::-webkit-scrollbar*` 规则,`::-webkit-scrollbar-thumb:hover` 也在其中。因此无条件地同时声明会让 hover token 在任何地方都得不到渲染:实现了 hover 伪元素的引擎,恰恰就是被标准属性静音的那些,而 Firefox 没有 hover 伪元素可作退路。于是标准属性写在 `@supports not selector(::-webkit-scrollbar)` 之内,该条件只在伪元素未被实现处为真,因此 Firefox 走标准属性路径WebKit 系引擎走伪元素路径。WebKit 规则不再反向加门禁:不实现这些伪元素的引擎会把它们当作未知选择器丢弃,因此加门禁只是重述选择器匹配本身已经做的事。对于旧到不支持 `selector()` 函数的引擎,该条件无效,从而求值为假并选中伪元素路径——对于这条判断下现实存在的 16.4 之前的 Safari这正是正确的一侧。
两条路径都读取同一组间接变量 `--dsh-scrollbar-thumb``--dsh-scrollbar-thumb-hover`,它们在 `body` 上绑定到 l1基础表面token。**这就是重新绑定约定,也是单看 CSS 无法得知的部分**:抬升表面在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)``--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`,这一次重新绑定同时作用于标准属性和 WebKit 伪元素。这组变量必须成对重新绑定;只改静止态滑块会让 hover 状态仍留在基础表面的 token 上。这组变量另一个合法的目标是 `transparent`,它随侧边栏滚动条[改为跟随指针](../feature/2026-08-04-pointer-revealed-sidebar-scrollbars.md)一并引入;下文的门禁只接受这两种目标。可由机械检查发现的子集归 `packages/client/ui-theme/tests/scrollbar-styles.spec.ts` 所有:任何既滚动又绘制抬升表面的样式表都必须重新绑定,因此本 note 不再维护完整的表面清单。多数把这组变量声明在抬升卡片上而非滚动的后代元素上,因为抬升层级属于这个表面,而自定义属性会继承到真正滚动的那个子元素。
两条路径都读取同一组间接变量 `--dsh-scrollbar-thumb``--dsh-scrollbar-thumb-hover`,它们在 `body` 上绑定到 l1基础表面token。**这就是重新绑定约定,也是单看 CSS 无法得知的部分**:抬升表面在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)``--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`,这一次重新绑定同时作用于标准属性和 WebKit 伪元素。这组变量必须成对重新绑定;只改静止态滑块会让 hover 状态仍留在基础表面的 token 上。这组变量另一个合法的目标是 `transparent`,它随侧边栏滚动条[改为跟随指针](../feature/2026-08-04-pointer-revealed-sidebar-scrollbars.md)一并引入;下文的门禁只接受这两种目标。可由机械检查发现的子集归 `packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts` 所有:任何既滚动又绘制抬升表面的样式表都必须重新绑定,因此本 note 不再维护完整的表面清单。多数把这组变量声明在抬升卡片上而非滚动的后代元素上,因为抬升层级属于这个表面,而自定义属性会继承到真正滚动的那个子元素。
`Menu``InputBar``QuestionComposer``TodoPanel` 这四个表面最初被漏掉,因此逐样式表的重新绑定约定由机械检查而非人工审阅把关。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-30-hover-popup-pointer-grace.md
2026-07-30-hover-popup-pointer-grace.md: 3f60c98ec6453b633feebe408cbc0c0c49eedea1
2026-07-30-hover-popup-pointer-grace.zh.md: f53d77bae7b7f223621c4c42779e1a6a616aa485
2026-07-30-hover-popup-pointer-grace.md: 302592a2e9de8e749ef2450e8877a3ccbffd15d5
2026-07-30-hover-popup-pointer-grace.zh.md: 4020a6f1f7ab52b6340a07f4c099906a7353a8a5

View File

@@ -32,4 +32,4 @@ The hover card is now hit-testable and covers 244px of whatever it overlays whil
## Testing
`packages/client/ui-primitives/tests/hover-card.spec.tsx` and `tests/atoms.spec.tsx` pin the grace boundary, cancel-on-return, no-second-dwell, disarm-on-owner-close, and the no-arming-while-closed case. The reachability gestures themselves — hovering onto the card, and moving between an open list and its trigger — are pinned in the real browser by `apps/web/tests/workspace-management.e2e.ts`, since they depend on hit testing and layout that jsdom does not model.
`packages/client/ui-primitives/tests/hover-card.client.spec.tsx` and `tests/atoms.spec.tsx` pin the grace boundary, cancel-on-return, no-second-dwell, disarm-on-owner-close, and the no-arming-while-closed case. The reachability gestures themselves — hovering onto the card, and moving between an open list and its trigger — are pinned in the real browser by `apps/web/tests/workspace-management.e2e.ts`, since they depend on hit testing and layout that jsdom does not model.

View File

@@ -32,4 +32,4 @@ Status: implemented
## 测试
`packages/client/ui-primitives/tests/hover-card.spec.tsx``tests/atoms.spec.tsx` 固定验证宽限期边界、折返取消、不重启停留计时、所有者关闭时解除待执行关闭,以及列表关闭时不启动关闭。可抵达性手势本身——把指针移到卡片上,以及在打开的列表与其触发按钮之间移动——由 `apps/web/tests/workspace-management.e2e.ts` 在真实浏览器中固定验证,因为它们依赖 jsdom 无法建模的命中测试与布局。
`packages/client/ui-primitives/tests/hover-card.client.spec.tsx``tests/atoms.spec.tsx` 固定验证宽限期边界、折返取消、不重启停留计时、所有者关闭时解除待执行关闭,以及列表关闭时不启动关闭。可抵达性手势本身——把指针移到卡片上,以及在打开的列表与其触发按钮之间移动——由 `apps/web/tests/workspace-management.e2e.ts` 在真实浏览器中固定验证,因为它们依赖 jsdom 无法建模的命中测试与布局。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-30-web-transcript-log-ordered-projection.md
2026-07-30-web-transcript-log-ordered-projection.md: 1c65eeec0fbf0dc550043bb809ae8d7310f0602b
2026-07-30-web-transcript-log-ordered-projection.zh.md: 67bb41adde46ba7112896404ddc92a05f08f7aca
2026-07-30-web-transcript-log-ordered-projection.md: f2eda6983ab88a420a4506be7766320dc5da0fa2
2026-07-30-web-transcript-log-ordered-projection.zh.md: 4c8bb5d0b9b49762d12aba837cef9c0818740cb0

View File

@@ -39,7 +39,7 @@ const COMPACT_PLUGIN: CompactCheckpointSource['plugin'] = 'compact'
Renaming the Service Definition's plugin id is now a compile error in the client: `TS2322: Type '"compact"' is not assignable to type '"compaction"'`. The import must stay **type-only** — a value import of any `@deepseek-ai` package that is neither a platform module nor an inline-safe wire layer is rejected by the client purity gate (`packages/client/tsdown.client.ts`), whose own message records that type-only imports are erased and never reach it. A type-only leaf import needs both a `tsconfig.base.json` `paths` entry and `{"path": "../../compact/compact"}` in `packages/client/runtime/tsconfig.json` `references`: composite `rootDir` rules apply to erased imports as well, and without the reference the diagnostic is `TS6059`/`TS6307`.
`packages/client/ui-conversation/tests/conversation-node-definitions.spec.ts` is the behavioral half, driving the compaction Definition with checkpoint and provenance records and proving that an older page can fill missing summary data. The Definition's type-only leaf import keeps the client isolated from the compact package root and the host-side `Context` merges reachable through it.
`packages/client/ui-conversation/tests/conversation-node-definitions.client.spec.ts` is the behavioral half, driving the compaction Definition with checkpoint and provenance records and proving that an older page can fill missing summary data. The Definition's type-only leaf import keeps the client isolated from the compact package root and the host-side `Context` merges reachable through it.
The divergence from the terminal is therefore narrow: both frontends recognize a checkpoint from the same declaration — the terminal value-imports `isCompactCheckpointSource` host-side, where no gate applies, and the client pins the type.

View File

@@ -39,7 +39,7 @@ const COMPACT_PLUGIN: CompactCheckpointSource['plugin'] = 'compact'
重命名 Service Definition 的插件 id 现在会在客户端产生编译错误:`TS2322: Type '"compact"' is not assignable to type '"compaction"'`。该导入必须保持**仅类型**——任何既非平台模块又非 inline-safe wire 层的 `@deepseek-ai` 包值导入都会被客户端纯度门禁(`packages/client/tsdown.client.ts`)拒绝,而它自己的报错信息就记录着仅类型导入会被擦除、永不抵达该门禁。仅类型的叶子导入同时需要 `tsconfig.base.json` 的一条 `paths` 条目和 `packages/client/runtime/tsconfig.json` `references` 中的 `{"path": "../../compact/compact"}`composite 的 `rootDir` 规则同样适用于被擦除的导入,缺少该引用时的诊断是 `TS6059`/`TS6307`
`packages/client/ui-conversation/tests/conversation-node-definitions.spec.ts` 是行为侧的另一半,用检查点与溯源记录驱动压缩 Definition并证明后续加载的旧分页可以补齐缺失的摘要数据。Definition 仅类型导入该叶子路径,使客户端继续与 compact 包根及经由它可达的宿主侧 `Context` 合并隔离。
`packages/client/ui-conversation/tests/conversation-node-definitions.client.spec.ts` 是行为侧的另一半,用检查点与溯源记录驱动压缩 Definition并证明后续加载的旧分页可以补齐缺失的摘要数据。Definition 仅类型导入该叶子路径,使客户端继续与 compact 包根及经由它可达的宿主侧 `Context` 合并隔离。
因此与终端的分歧很窄:两个前端都从同一份声明识别检查点——终端在宿主侧值导入 `isCompactCheckpointSource`(那里不适用任何门禁),客户端钉住类型。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-31-composer-text-layers-share-one-scrollport.md
2026-07-31-composer-text-layers-share-one-scrollport.md: ba11384409714d6a64a964d63197705acf39d213
2026-07-31-composer-text-layers-share-one-scrollport.zh.md: 7a2500a6253c7cf50a57458b66c9b80503414ace
2026-07-31-composer-text-layers-share-one-scrollport.md: 6097779529f86e6d994296ae396f108c63f01abc
2026-07-31-composer-text-layers-share-one-scrollport.zh.md: 753d67d538d0c17512444639d60b7b5c8ff80e9a

View File

@@ -71,7 +71,7 @@ Revealing the caret is the one thing that now depends on the browser rather than
## Testing
The unit spec in [input-bar.spec.tsx](../../../../packages/client/ui-conversation/tests/input-bar.spec.tsx) asserts what jsdom can see: that one scrolling box contains both the textarea and the backdrop, that the backdrop's text is now the draft and nothing else, and that a late persisted draft reveals its caret without taking focus from another control. jsdom reports `scrollHeight === clientHeight` for every element and never scrolls one, so the geometry belongs to the browser scenario; the wheel-chaining cases stub the scrollport's metrics rather than the textarea's.
The unit spec in [input-bar.spec.tsx](../../../../packages/client/ui-conversation/tests/input-bar.client.spec.tsx) asserts what jsdom can see: that one scrolling box contains both the textarea and the backdrop, that the backdrop's text is now the draft and nothing else, and that a late persisted draft reveals its caret without taking focus from another control. jsdom reports `scrollHeight === clientHeight` for every element and never scrolls one, so the geometry belongs to the browser scenario; the wheel-chaining cases stub the scrollport's metrics rather than the textarea's.
[composer-draft-scroll.e2e.ts](../../../../apps/web/tests/composer-draft-scroll.e2e.ts) measures the rest in chromium against the built client: a 40-line draft in a fresh workspace's blank composer, zero model calls. Every metric is read in the caret's own coordinate frame — where the textarea places line n, offset included — against a DOM Range over the backdrop's text for the same line, because that difference is what a user sees. The decisive case changes the offset and re-reads that difference **before the task ends**, which is before any `scroll` listener could have run: 0 with one scrollport, and the full delta with a mirror. A vacuity guard asserts the draft overflows the capped box first, and separate cases cover the cap, one wrap width across all three layers, a wheel gesture, a trailing-newline draft, and the caret-reveal path that the textarea's own scrolling used to handle — typing after scrolling away must bring the scrollport back to the caret.

View File

@@ -71,7 +71,7 @@ composer 的文本由两层叠放绘制(见 [InputBar](../../../../packages/cl
## 测试
[input-bar.spec.tsx](../../../../packages/client/ui-conversation/tests/input-bar.spec.tsx) 中的单元用例断言 jsdom 能看见的部分:同一个滚动盒同时包含 textarea 与 backdropbackdrop 的文本现在就是草稿本身、不多不少且渲染后才到达的持久化草稿会回视其光标同时不从其他控件夺走焦点。jsdom 对任何元素都报告 `scrollHeight === clientHeight` 且从不滚动,因此几何属于浏览器场景;滚轮接力用例改为桩接滚动容器的度量,而非 textarea 的。
[input-bar.spec.tsx](../../../../packages/client/ui-conversation/tests/input-bar.client.spec.tsx) 中的单元用例断言 jsdom 能看见的部分:同一个滚动盒同时包含 textarea 与 backdropbackdrop 的文本现在就是草稿本身、不多不少且渲染后才到达的持久化草稿会回视其光标同时不从其他控件夺走焦点。jsdom 对任何元素都报告 `scrollHeight === clientHeight` 且从不滚动,因此几何属于浏览器场景;滚轮接力用例改为桩接滚动容器的度量,而非 textarea 的。
[composer-draft-scroll.e2e.ts](../../../../apps/web/tests/composer-draft-scroll.e2e.ts) 在 chromium 中针对构建产物度量其余部分:全新工作区的空白 composer 中一份 40 行草稿,零模型调用。每个度量都在光标自己的坐标系里读取——即 textarea 把第 n 行放在哪,含其自身偏移——再与 backdrop 同一行文本上的 DOM Range 相比,因为这个差值正是用户看到的东西。决定性的用例改变偏移,并**在本任务结束之前**重新读取该差值,也就是在任何 `scroll` 监听可能运行之前:单一滚动容器下为 0镜像方案下则是整个增量。空洞性保护先断言草稿确实超过了带上限的盒子其余用例分别覆盖高度上限、三层同一折行宽度、滚轮手势、以换行结尾的草稿以及过去由 textarea 自身滚动承担的光标回视路径——滚离光标后输入,必须把滚动容器带回光标处。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-05-context-meter-blind-to-compaction.md
2026-08-05-context-meter-blind-to-compaction.md: 10ded250cec9c92d90803bdf5969cc7f5aa54c50
2026-08-05-context-meter-blind-to-compaction.zh.md: ca3fe59eecde82bb97b44af4c3383546d5672753
2026-08-05-context-meter-blind-to-compaction.md: b90208eda25101aeb55130cd1b6563dbfaee8a9a
2026-08-05-context-meter-blind-to-compaction.zh.md: 85387c50ebc981fc2f7f02d07432da677f69ef90

View File

@@ -43,4 +43,4 @@ The panel's composition rows still do not sum to the header, and now for one cle
## Testing
`packages/llm/token-meter/tests/token-usage-projection.spec.ts` covers the carry-forward across surface growth and a compaction (the sample holding still while the projection shrinks) and the zero clamp when heuristic error would drive the figure negative. `packages/client/ui-conversation/tests/context-meter.spec.tsx` pins the ring reading the projected figure, and `chat-stats.spec.tsx` pins `contextOccupancy`'s preference and its fallback. The end-to-end numbers above came from driving `BasicCompactService.compactNow` through a real `AgentLoop` with the projection registry mounted.
`packages/llm/token-meter/tests/token-usage-projection.spec.ts` covers the carry-forward across surface growth and a compaction (the sample holding still while the projection shrinks) and the zero clamp when heuristic error would drive the figure negative. `packages/client/ui-conversation/tests/context-meter.client.spec.tsx` pins the ring reading the projected figure, and `chat-stats.spec.tsx` pins `contextOccupancy`'s preference and its fallback. The end-to-end numbers above came from driving `BasicCompactService.compactNow` through a real `AgentLoop` with the projection registry mounted.

View File

@@ -43,4 +43,4 @@ AFTER compact: ring=4% header=~4227/100000 rows=[system 18, tools 0, messag
## 测试
`packages/llm/token-meter/tests/token-usage-projection.spec.ts` 覆盖了投影值在表层增长与一次压缩期间的延续更新(样本保持不动而投影值缩小),以及启发式误差会把数字压到负数时的零钳制。`packages/client/ui-conversation/tests/context-meter.spec.tsx` 钉住圆环读取投影值这一点,`chat-stats.spec.tsx` 钉住 `contextOccupancy` 的优先级与回退。上面那组端到端数字来自在挂载了投影注册表的真实 `AgentLoop` 上驱动 `BasicCompactService.compactNow`
`packages/llm/token-meter/tests/token-usage-projection.spec.ts` 覆盖了投影值在表层增长与一次压缩期间的延续更新(样本保持不动而投影值缩小),以及启发式误差会把数字压到负数时的零钳制。`packages/client/ui-conversation/tests/context-meter.client.spec.tsx` 钉住圆环读取投影值这一点,`chat-stats.spec.tsx` 钉住 `contextOccupancy` 的优先级与回退。上面那组端到端数字来自在挂载了投影注册表的真实 `AgentLoop` 上驱动 `BasicCompactService.compactNow`

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-05-workspace-blank-session-reuse-membership.md
2026-08-05-workspace-blank-session-reuse-membership.md: 910a10e9ada1a835df7a38a04fb04c504b0921df
2026-08-05-workspace-blank-session-reuse-membership.zh.md: 0581b768679d78716c1bf70cfffec825c7281783
2026-08-05-workspace-blank-session-reuse-membership.md: 0d46a0ccf6924c508db2e6c0f3591468e2956722
2026-08-05-workspace-blank-session-reuse-membership.zh.md: e67810dc3a45d494f64a2b774557d04297b6fce3

View File

@@ -26,4 +26,4 @@ Stray blank sessions remain visible in Ungrouped (the user can still open them)
## Testing
`packages/client/runtime/tests/workspaces-service.spec.ts` covers the four outcomes: a member blank session is reused (no create RPC); a stray blank with matching cwd is **not** reused and a fresh accounted session is created (regression case); an archived blank is not reused; a rejected first prompt keeps a member blank eligible. The full client suite (`pnpm run test:gui`) stays green.
`packages/client/runtime/tests/workspaces-service.client.spec.ts` covers the four outcomes: a member blank session is reused (no create RPC); a stray blank with matching cwd is **not** reused and a fresh accounted session is created (regression case); an archived blank is not reused; a rejected first prompt keeps a member blank eligible. The full client suite (`pnpm run test:gui`) stays green.

View File

@@ -26,4 +26,4 @@ Status: implemented
## 测试
`packages/client/runtime/tests/workspaces-service.spec.ts` 覆盖四种结果:成员空白会话被复用(无 create RPCcwd 匹配但非成员的游离空白会话**不被**复用、改为创建全新入账会话(回归用例);已归档空白会话不被复用;首次提示词被拒后成员空白会话仍可复用。完整客户端套件(`pnpm run test:gui`)保持绿色。
`packages/client/runtime/tests/workspaces-service.client.spec.ts` 覆盖四种结果:成员空白会话被复用(无 create RPCcwd 匹配但非成员的游离空白会话**不被**复用、改为创建全新入账会话(回归用例);已归档空白会话不被复用;首次提示词被拒后成员空白会话仍可复用。完整客户端套件(`pnpm run test:gui`)保持绿色。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md
2026-08-06-onboarding-step-owned-takeover-chrome.md: 35b9d16aaba4ca9f124a9972c43b2a107ea0309f
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: 2f3331965aa94aa01bf0ce21d4bbb421397ee5bd
2026-08-06-onboarding-step-owned-takeover-chrome.md: 97f5335412f256bf7e60f36731c9e0c4475ee99e
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: 054072764a7b9a25586899a93596fdc922e71dcb

View File

@@ -32,4 +32,4 @@ A future step that registers without wrapping its visible content in `Onboarding
## Testing
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` pins the primitive: body portal around the content, mask/stage class presence, `#root` inert held for exactly the mount lifetime, and the no-`#root` composition. `packages/client/ui-settings-general/tests/settings-root.spec.tsx` pins the inverted shell contract: no takeover chrome and no inert while a mounted step renders nothing. `apps/web/tests/onboarding-deepseek-config.e2e.ts` gains the defect's assembled regression pin: a configured world reloads while every `settings.describe` response is held open at the browser's network boundary — widening the steps' deciding window from loopback-invisible to hundreds of milliseconds, which is what keeps the assertions non-vacuous — and an 8 ms in-page sampler proves the takeover chrome never mounts and `#root` never turns inert. The file's existing scenarios and the step specs (`ui-settings-general`, `ui-models`) pass unchanged — the mask selector and geometry pins survive because the stylesheet moved verbatim.
`packages/client/ui-primitives/tests/onboarding-surface.client.spec.tsx` pins the primitive: body portal around the content, mask/stage class presence, `#root` inert held for exactly the mount lifetime, and the no-`#root` composition. `packages/client/ui-settings-general/tests/settings-root.client.spec.tsx` pins the inverted shell contract: no takeover chrome and no inert while a mounted step renders nothing. `apps/web/tests/onboarding-deepseek-config.e2e.ts` gains the defect's assembled regression pin: a configured world reloads while every `settings.describe` response is held open at the browser's network boundary — widening the steps' deciding window from loopback-invisible to hundreds of milliseconds, which is what keeps the assertions non-vacuous — and an 8 ms in-page sampler proves the takeover chrome never mounts and `#root` never turns inert. The file's existing scenarios and the step specs (`ui-settings-general`, `ui-models`) pass unchanged — the mask selector and geometry pins survive because the stylesheet moved verbatim.

View File

@@ -32,4 +32,4 @@
## 测试
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` 钉住原语行为:内容外的 body portal、遮罩展示层类名存在、`#root``inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings-general/tests/settings-root.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec`ui-settings-general``ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。
`packages/client/ui-primitives/tests/onboarding-surface.client.spec.tsx` 钉住原语行为:内容外的 body portal、遮罩展示层类名存在、`#root``inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings-general/tests/settings-root.client.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec`ui-settings-general``ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-reader-scroll-attribution-observed-top-ledger.md
2026-08-06-reader-scroll-attribution-observed-top-ledger.md: ef5ddbeb9bea1393f474dfb4c809ae2e19bfea5c
2026-08-06-reader-scroll-attribution-observed-top-ledger.zh.md: 7a1142cdd793cfce46bfd3908ab28345701aec7a
2026-08-06-reader-scroll-attribution-observed-top-ledger.md: 66a1ca361cf28bf0beab95fa81da9cac3527474c
2026-08-06-reader-scroll-attribution-observed-top-ledger.zh.md: ce7cd6d0ade2702a6f717340cb7c9b354d0387c2

View File

@@ -18,7 +18,7 @@ A shrink clamp whose layout regrows within the same rendering update before the
## Testing
Unit specs in `packages/client/ui-conversation/tests/chat-view.spec.tsx` pin the ledger contract directly: a `readerScroll` helper delivers a position the component never wrote, programmatic deliveries land on the ledger, and the stream-finalization shrink clamp keeps following. Two scenarios in `apps/web/tests/chat-scroll-contract.e2e.ts` extend the [browser e2e lane](../testing/2026-07-24-web-gui-browser-e2e-lane.md): keyboard paging over a settled transcript and a touch-style momentum fling against paced streaming, both red under the wheel-only implementation and green under the ledger.
Unit specs in `packages/client/ui-conversation/tests/chat-view.client.spec.tsx` pin the ledger contract directly: a `readerScroll` helper delivers a position the component never wrote, programmatic deliveries land on the ledger, and the stream-finalization shrink clamp keeps following. Two scenarios in `apps/web/tests/chat-scroll-contract.e2e.ts` extend the [browser e2e lane](../testing/2026-07-24-web-gui-browser-e2e-lane.md): keyboard paging over a settled transcript and a touch-style momentum fling against paced streaming, both red under the wheel-only implementation and green under the ledger.
The lane's Chromium cannot synthesize any non-wheel device scrolling, which bounds what the e2e can drive for real: `Input.synthesizeScrollGesture` with a touch source and hand-rolled `Input.dispatchTouchEvent` sequences deliver DOM events but never move a scroller (headless and headed-under-Xvfb alike); the `default` gesture source synthesizes wheel events; and compositor scrollbars ignore synthetic mouse input entirely, with a gutter visible only when `--hide-scrollbars` is removed. Keyboard is the one working non-wheel primitive, so it carries the real-input-pipeline proof, and the fling scenario replays touch's signature — per-frame decaying displacements the component never authored — through the scrollport directly.

View File

@@ -18,7 +18,7 @@ ChatView 的贴底跟随此前只把滚轮/触控板手势识别为读者输
## 测试
`packages/client/ui-conversation/tests/chat-view.spec.tsx` 中的单元测试直接钉住 ledger 约定:`readerScroll` 辅助函数交付一个组件从未写入过的位置,程序化交付落在 ledger 上,流收尾阶段的收缩钳制保持跟随。`apps/web/tests/chat-scroll-contract.e2e.ts` 中的两个场景扩展了[浏览器 e2e 车道](../testing/2026-07-24-web-gui-browser-e2e-lane.md):在已停稳的 transcript 上做键盘翻页以及对着按节奏推进的流式输出做一次触控式惯性快滑momentum fling两者在仅认滚轮的实现下均为红、在 ledger 下均为绿。
`packages/client/ui-conversation/tests/chat-view.client.spec.tsx` 中的单元测试直接钉住 ledger 约定:`readerScroll` 辅助函数交付一个组件从未写入过的位置,程序化交付落在 ledger 上,流收尾阶段的收缩钳制保持跟随。`apps/web/tests/chat-scroll-contract.e2e.ts` 中的两个场景扩展了[浏览器 e2e 车道](../testing/2026-07-24-web-gui-browser-e2e-lane.md):在已停稳的 transcript 上做键盘翻页以及对着按节奏推进的流式输出做一次触控式惯性快滑momentum fling两者在仅认滚轮的实现下均为红、在 ledger 下均为绿。
该车道的 Chromium 无法合成任何非滚轮的设备滚动,这限定了 e2e 能真实驱动的范围:触控来源的 `Input.synthesizeScrollGesture` 与手工构造的 `Input.dispatchTouchEvent` 序列都能交付 DOM 事件,却从不移动滚动容器(无头模式与 Xvfb 下的有头模式皆然);`default` 手势来源合成的是滚轮事件;合成器滚动条则完全无视合成的鼠标输入,且只有移除 `--hide-scrollbars` 后才能看到滚动条槽。键盘是唯一可用的非滚轮原语,因此由它承担真实输入流水线的证明;快滑场景则把触控的特征(组件从未写入过的逐帧衰减位移)直接回放进滚动容器。