Merge remote-tracking branch 'origin/master' into fix/preset-ui-20260921
# Conflicts: # packages/preset/agent-preset-registry/package.json
This commit is contained in:
commit
f68d4b5aba
1386 files changed
+40545
-13468
No files matched your search
@@ -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/architecture/2026-07-08-tool-output-spill-files.md
|
||||
2026-07-08-tool-output-spill-files.md: fa6697aad63c97b0ef580fe1ca46e50541e3d682
|
||||
2026-07-08-tool-output-spill-files.zh.md: a995eafc7878b342a2164c5116da1f43ada791a5
|
||||
2026-07-08-tool-output-spill-files.md: e32aec72f423dff33a29d9012c0f71c9cfbd24fb
|
||||
2026-07-08-tool-output-spill-files.zh.md: 2941fc516723e72b5d5670e674ce612f3cf581cc
|
||||
@@ -66,36 +66,7 @@ interface SpillRef {
|
||||
|
||||
### Spill policy
|
||||
|
||||
`dsh-spill-policy` is a `tools/post-execute` result transformer with one configuration knob:
|
||||
|
||||
```ts ignore-check
|
||||
interface Config {
|
||||
/** Omitted means no automatic spill policy. Present means apply to oversized plain text tool results. */
|
||||
maxInlineBytes?: number
|
||||
}
|
||||
```
|
||||
|
||||
When `maxInlineBytes` is omitted the plugin registers nothing (a true no-op). When set, it applies a default policy to final plain-text tool results:
|
||||
|
||||
1. Let the tool run normally, delegating via `next()` so a downstream listener settles the result first.
|
||||
2. Flatten the accepted final `ContentBlock[]` only when it is entirely plain text; a result with any non-text block is left untouched.
|
||||
3. If its UTF-8 byte size is at or below `maxInlineBytes`, leave it unchanged.
|
||||
4. If it is larger, call `ctx.spillStore.saveText()` with the full final text.
|
||||
5. Replace the model-facing result with a retained head/tail preview plus the spill reference.
|
||||
|
||||
The preview is an implementation default owned by the policy: a head/tail split of `maxInlineBytes` via the retention library's `TextRetainer`. Future config can expose preview sizing only after a second deployment needs it.
|
||||
|
||||
The replacement text is intentionally generic because the policy only knows the final formatted tool result, not the tool's internal resource:
|
||||
|
||||
```text
|
||||
<retained preview>
|
||||
|
||||
(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.)
|
||||
```
|
||||
|
||||
If `ctx.spillStore.saveText()` fails (permissions, ENOSPC, backend unavailable), or the call has no session owner, or no backend is loaded, the plugin logs the reason and returns the original result unchanged. Spill failure never turns a successful tool call into an `isError` result or hides the inline result.
|
||||
|
||||
The policy skips `read` to avoid a circular `read -> spill file -> read again` loop. Additional opt-out configuration is deferred until a real second tool needs it.
|
||||
`dsh-spill-policy` retains ordered text/image ends under `maxInlineTokens` after post-execute policy accepts the result. `saveText()` stores the complete result with readable image paths; image bytes remain in attachment storage. The [multimodal retention decision](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.md) owns image projection order, atomic image omission, and route pricing. An omitted budget installs no listeners, recovery failures keep the original result, and model-facing `read` results skip retention to avoid a read/spill loop.
|
||||
|
||||
## Showcase: web_fetch
|
||||
|
||||
@@ -129,7 +100,7 @@ With `dsh-spill-policy` configured, a large formatted fetch result is automatica
|
||||
- id: spill-policy
|
||||
name: '@deepseek-ai/dsh-spill-policy'
|
||||
config:
|
||||
maxInlineBytes: 50000
|
||||
maxInlineTokens: 12500
|
||||
```
|
||||
|
||||
This separation is important. `web-fetch-http` still owns resource caps (`maxResponseBytes`, `maxBodyChars`) to protect network, memory, and decoding work. `spill-policy` owns only the model-facing context cap after the result already exists. If the provider already returned `truncated: true`, the spill file contains the full formatted result the tool returned, not the full original webpage; the policy does not claim otherwise.
|
||||
@@ -186,11 +157,11 @@ The local-backend value proposition depends on the existing `read`/`grep` tools
|
||||
|
||||
**Snapshot gap.** No ACP snapshot scenario covers the transcript-visible `web_fetch` spill notice yet. The ACP snapshot harness replays keyless and cannot hit the live web, and a `web_fetch` spill requires a real over-cap HTTP body; a deterministic scenario would need a seeded loopback fetch target the replay tree does not currently wire (the examples do not load `tool-web` at all). The behavior is covered instead by the `dsh-tool-web` integration test against a loopback server. Closing the gap is follow-up work: wire `tool-web` + a seeded fetch target into the ACP example, then record a `web-fetch-spill` scenario.
|
||||
|
||||
The policy can become too large if it starts owning tool-specific semantics. It stays narrow: plain-text final results only. Tool-owned early spill remains future work.
|
||||
The policy can become too large if it starts owning tool-specific semantics. It handles accepted text/image sequences without interpreting tool-specific text. Tool-owned early spill remains future work.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineBytes` deployment knob is enough to prove the shape.
|
||||
**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineTokens` deployment knob is enough to prove the shape.
|
||||
|
||||
**Make `tool-results` a broad tool-result platform.** Rejected: a broad package name invites retention policy, result replacement, preview wording, search, and early spill into one seam. The shared storage part is smaller: save text and return a locator plus retrieval hint.
|
||||
|
||||
|
||||
@@ -66,36 +66,7 @@ interface SpillRef {
|
||||
|
||||
### spill 策略
|
||||
|
||||
`dsh-spill-policy` 是一个 `tools/post-execute` 结果转换器,只提供一个配置项:
|
||||
|
||||
```ts ignore-check
|
||||
interface Config {
|
||||
/** Omitted means no automatic spill policy. Present means apply to oversized plain text tool results. */
|
||||
maxInlineBytes?: number
|
||||
}
|
||||
```
|
||||
|
||||
省略 `maxInlineBytes` 时,插件不会注册任何内容,是真正的无操作。设置该值后,它会对最终的纯文本工具结果应用默认策略:
|
||||
|
||||
1. 让工具正常运行,通过 `next()` 委托,使下游监听器先结算结果。
|
||||
2. 仅当已接受的最终 `ContentBlock[]` 全部是纯文本时,才将其展平;含任何非文本块的结果保持不变。
|
||||
3. 如果 UTF-8 字节大小不超过 `maxInlineBytes`,保持不变。
|
||||
4. 如果超出上限,使用完整的最终文本调用 `ctx.spillStore.saveText()`。
|
||||
5. 把模型可见结果替换为保留的首尾预览和 spill 引用。
|
||||
|
||||
预览属于策略所有的实现默认值:以 `maxInlineBytes` 为上限,使用保留库的 `TextRetainer` 进行首尾分割。只有第二个部署证明有此需求后,未来配置才会公开预览大小。
|
||||
|
||||
替换文本刻意保持通用,因为策略只知道最终格式化的工具结果,不了解工具的内部资源:
|
||||
|
||||
```text
|
||||
<retained preview>
|
||||
|
||||
(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.)
|
||||
```
|
||||
|
||||
如果 `ctx.spillStore.saveText()` 失败(权限、ENOSPC、后端不可用),或调用没有会话所有者,或未加载后端,插件会记录原因并原样返回结果。spill 失败绝不会把成功的工具调用变为 `isError` 结果,也不会隐藏内联结果。
|
||||
|
||||
策略跳过 `read`,以避免形成 `read -> spill file -> read again` 循环。额外的选择退出配置要等确实出现第二个有此需求的工具后再引入。
|
||||
`dsh-spill-policy` 在执行后策略接受结果之后,按 `maxInlineTokens` 对文字和图片共同计量并保留首尾。完整结果仍通过 `saveText()` 保存,图片本体留在附件存储,结果文件记录可读取路径。图片投影顺序、整图省略和模型计量由[图文结果保留决策](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.zh.md)负责。省略配置时不安装监听器,存储失败时保留原结果;`read` 的模型可见结果跳过自动省略,避免重复读取产生循环。
|
||||
|
||||
## 示例:web_fetch
|
||||
|
||||
@@ -129,7 +100,7 @@ ctx.tools.register(defineTool({
|
||||
- id: spill-policy
|
||||
name: '@deepseek-ai/dsh-spill-policy'
|
||||
config:
|
||||
maxInlineBytes: 50000
|
||||
maxInlineTokens: 12500
|
||||
```
|
||||
|
||||
这项分离很重要。`web-fetch-http` 仍负责资源上限(`maxResponseBytes`、`maxBodyChars`),用来保护网络、内存和解码工作。`spill-policy` 只负责结果已经存在后针对模型上下文的上限。如果提供方已经返回 `truncated: true`,spill 文件包含的是工具返回的完整格式化结果,而不是原始网页全文;策略不会做出其他承诺。
|
||||
@@ -186,11 +157,11 @@ ctx.tools.register(defineTool({
|
||||
|
||||
**快照缺口。** 目前没有 ACP 快照场景覆盖 transcript(文本记录)可见的 `web_fetch` spill 提示。ACP 快照 harness 在无密钥环境中回放,无法访问实时 web,而 `web_fetch` spill 需要一个真实的超上限 HTTP 正文;确定性场景需要一个预置的 loopback fetch 目标,但当前回放树尚未接线(示例根本没有加载 `tool-web`)。该行为改由 `dsh-tool-web` 针对 loopback server 的集成测试覆盖。弥补该缺口属于后续工作:把 `tool-web` 和预置 fetch 目标接入 ACP 示例,然后录制 `web-fetch-spill` 场景。
|
||||
|
||||
如果策略开始负责工具专用语义,就会膨胀得过大。它的范围保持狭窄:只处理纯文本最终结果。由工具负责的提前 spill 仍留作未来工作。
|
||||
如果策略开始负责工具专用语义,就会膨胀得过大。它处理已接受的图文序列,不解释工具专用的文字语义。由工具负责的提前 spill 仍留作未来工作。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**要求每个工具通过保留声明选择加入。**不予采纳,因为目标是实现类似 Claude Code 通用工具结果持久化的默认行为。只需一个 `maxInlineBytes` 部署配置项即可验证该形态。
|
||||
**要求每个工具通过保留声明选择加入。**不予采纳,因为目标是实现类似 Claude Code 通用工具结果持久化的默认行为。只需一个 `maxInlineTokens` 部署配置项即可验证该形态。
|
||||
|
||||
**把 `tool-results` 建成宽泛的工具结果平台。** 不予采纳:宽泛的包名会诱使系统把保留策略、结果替换、预览措辞、搜索和提前 spill 合并进一个 seam。可共享的存储部分更小:保存文本,并返回定位符与检索提示。
|
||||
|
||||
|
||||
+2
-2
@@ -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/architecture/2026-07-23-client-plugin-loading-model.md
|
||||
2026-07-23-client-plugin-loading-model.md: a8d016a70792ad4ac6a71083677ce5d82a30af0a
|
||||
2026-07-23-client-plugin-loading-model.zh.md: 1b94909c9611f01e5a6d7fcb50ec3f91ee88fab1
|
||||
2026-07-23-client-plugin-loading-model.md: b8c23108a7a1cf3a04ded373d8705bae626c70c2
|
||||
2026-07-23-client-plugin-loading-model.zh.md: efaea843120e5b023bbc8f59663ea47468944c71
|
||||
@@ -46,7 +46,7 @@ A `require.async("./client.<name>.js")` call requests the exact revisioned `/plu
|
||||
|
||||
The shared tsdown preset emits a map for every plugin entry and chunk and rewrites first-party source paths into the browser-resolvable repository form `/packages/<group>/<package>/src/...`. The production Client pass consumes `lib/types`; the preset supplies each tsc map to Rolldown and fills `sourcesContent` from the original files, so the final map reaches TypeScript/TSX instead of stopping at emitted JavaScript. Other workspace sources inlined into a bundle likewise resolve to their `packages/` owner, while dependency paths remain unchanged. Combo generation strips each local debug directive, records its generated-line offset, resolves every authored source against the original per-plugin map URL, and emits an Indexed Source Map v3. An authored map supplies its section; otherwise an identity section embeds the generated bundle and uses the packer's `sourceURL` as its source name when present. The absolute combo map URL mirrors the script resource list by changing every `client.js` suffix to `client.js.map`, so `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` points to `/plugins/??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`. A chunk script likewise points to its own map URL. Source-map files are read and combined only when those map URLs receive `GET`; `HEAD` materializes neither script nor map bodies. The Vite shell also emits source maps, letting shell code and externally loaded plugins map stacks and performance profiles back to TypeScript/TSX.
|
||||
|
||||
The graph retains each row's revisioned one-resource combo URL for HMR and adds a revisioned descriptor for every startup combo request; several descriptors may carry the same scheduling phase. Initial row revisions are opaque process nonces rather than content hashes; they keep the snapshotted one-resource response immutable without hashing every plugin at startup. The shared preset stamps the entry after every package output is written. After the watcher observes that completed-build stamp, `rebuilt(id)` hashes the entry bytes together with its timestamp and publishes the resulting revision; a chunk-only build therefore advances the owner revision without a Host-side sibling scan. Startup combo revisions derive from the ordered row revisions. Script bodies are assembled on their first `GET`; source-map files are read and combined separately on their first map `GET`. `HEAD` materializes neither body. Versioned scripts and maps use immutable caching, and unrelated graph recomposition retains already-materialized chunk responses at the same revision. The Host serves only exact generated URLs; stale revisions and unadvertised resource lists return 404 instead of aliasing different bytes. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin Host and build-stamped registration id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id.
|
||||
The graph retains each row's revisioned one-resource combo URL for HMR and adds a revisioned descriptor for every startup combo request; several descriptors may carry the same scheduling phase. Initial publication and `rebuilt(id)` use the same hash of entry mtime, ctime, and size; artifact contents never enter the hash. Ctime distinguishes rewrites that preserve mtime and size. A Host restart over unchanged artifacts therefore retains every row revision, so the reconnecting SSE snapshot does not trigger browser plugin replacement. The shared preset stamps the entry after every package output is written; a chunk-only build therefore advances the owner revision without a Host-side sibling scan. Startup combo revisions derive from the ordered row revisions. Script bodies are assembled on their first `GET`; source-map files are read and combined separately on their first map `GET`. `HEAD` materializes neither body. Versioned scripts and maps use immutable caching, and unrelated graph recomposition retains already-materialized chunk responses at the same revision. The Host serves only exact generated URLs; stale revisions and unadvertised resource lists return 404 instead of aliasing different bytes. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin Host and build-stamped registration id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id.
|
||||
|
||||
### The loading flow, end to end
|
||||
|
||||
@@ -56,7 +56,7 @@ What happens between `dsh web` starting and the UI appearing? Three stages: the
|
||||
|
||||
1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, including the always-mounted `client-hmr` row. `auditStartupEntries` reports failed imports, activation errors with their original stacks, and pending dependencies; optional entries warn and required entries reject startup ([startup policy](2026-09-09-consumer-owned-startup-strictness.md)).
|
||||
2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) resolves each live Loader entry through the same `name` and owning-tree `baseUrl` inputs that imported its Host face, then reads the nearest owning package.json `dsh.client` declaration and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`. The manifest package name is the browser module identity even when an overlay names a relative source or built entry file. Distinct active Loader sources resolving to one package name fail composition; after one source unloads, the surviving source supplies the row without a fiber restart. The row's three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers, rejects synchronous request cycles, and assigns every row to exactly one initial batch. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the Host audit reports either error from the FAILED fiber.
|
||||
3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per entry name and owning-tree base URL for the process lifetime and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows receive an opaque process nonce plus sequence without hashing their artifacts; startup combo revisions hash the combined script inputs plus indexed map, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows.
|
||||
3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per entry name and owning-tree base URL for the process lifetime and changed bundle publication reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows hash their pre-read filesystem metadata; startup combo revisions hash the ordered row identities and revisions, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows.
|
||||
|
||||
Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted.
|
||||
|
||||
@@ -80,7 +80,7 @@ The Host SSE adapter forwards existing graph-change notifications and sends the
|
||||
|
||||
Hot reload is a composition decision: the web bundle mounts the `client-hmr` row (a normal plugin package) unconditionally; its node half brings the bundle watch and the SSE channel, and the chain stays idle until a rebuild watcher rewrites client bundles. A composition that must not expose it disables the row.
|
||||
|
||||
How does a rebuilt bundle become a reload signal? The hmr node half observes it itself — no builder tells it. Before reading each startup snapshot, the module host captures the entry stat baseline and exposes it through `ctx.clientModules.artifactBaseline(id)`. One HMR-owned interval compares every current graph row with that baseline. An unchanged row starts watching without a content read or hash; a write after baseline capture is already a stat delta and only that row enters `rebuilt(id)`. This avoids both an initial all-row re-hash and `fs.watchFile`, whose asynchronous first baseline can silently absorb a construction-time rebuild. Watch membership follows `onGraphChanged`; vanished rows drop out, and an entry missing at poll time keeps its row dirty so reappearance forces a new revision even with identical metadata. The shared tsdown preset stamps `client.js` after every sibling output has been written; on an entry mtime or size delta, or a dirty row, `rebuilt(id)` derives the revision from the entry bytes and completed-build timestamp. A chunk-only edit therefore changes the revision, and a partial write followed by the completion stamp supplies a later stat change that self-heals. When the `rev` changes, the node half broadcasts a `rebuilt` frame on `GET /plugins/events` — a system SSE channel that sends the full graph on connect and `rebuilt` frames on change, presentation-only wire that never enters the session log. Polling is deliberate because inotify does not fire on the weka network mount, the same reason the build-side watcher needs `--poll`; one entry stat per row and interval is sufficient, the interval is a validated config field (default 500ms), and disposal clears the one timer. Rebuilding artifacts is any process using the shared Client tsdown preset — `scripts/dev-web.ts` remains the watch-build entry point, discovering its package list through `dsh.client` while scanning `packages/*/*/package.json` at startup — and builder and host share no notification protocol.
|
||||
How does a rebuilt bundle become a reload signal? The hmr node half observes it itself — no builder tells it. Before reading each startup snapshot, the module host captures the entry stat baseline and exposes it through `ctx.clientModules.artifactBaseline(id)`. One HMR-owned interval compares every current graph row with that baseline. An unchanged row starts watching without a content read or hash; a write after baseline capture is already a stat delta and only that row enters `rebuilt(id)`. This avoids redundant reads when installing the watcher and avoids `fs.watchFile`, whose asynchronous first baseline can silently absorb a construction-time rebuild. Watch membership follows `onGraphChanged`; vanished rows drop out, and an entry missing at poll time keeps its row dirty so reappearance retries publication even when mtime and size match the baseline. The shared tsdown preset stamps `client.js` after every sibling output has been written; on an entry mtime, ctime, or size delta, or a dirty row, `rebuilt(id)` derives the revision from those three metadata values. It reads the new bundle snapshot only if the revision changes. A chunk-only edit therefore changes the revision, and a partial write followed by the completion stamp supplies a later stat change that self-heals. When the `rev` changes, the node half broadcasts a `rebuilt` frame on `GET /plugins/events` — a system SSE channel that sends the full graph on connect and `rebuilt` frames on change, presentation-only wire that never enters the session log. Polling is deliberate because inotify does not fire on the weka network mount, the same reason the build-side watcher needs `--poll`; one entry stat per row and interval is sufficient, the interval is a validated config field (default 500ms), and disposal clears the one timer. Rebuilding artifacts is any process using the shared Client tsdown preset — `scripts/dev-web.ts` remains the watch-build entry point, discovering its package list through `dsh.client` while scanning `packages/*/*/package.json` at startup — and builder and host share no notification protocol.
|
||||
|
||||
On the browser side, the transport delegates code replacement to the same modules controller that owns graph reconciliation:
|
||||
|
||||
@@ -120,4 +120,4 @@ Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordi
|
||||
| Full ctx-ification now (React and libraries via services, no module table) | Static UI libraries still expose synchronous values, so removing the table would leave those imports without a shared identity |
|
||||
| Eager instantiation with a frozen table | Runs bundle side effects at script arrival; lazy registration keeps execution at Cordis import while recursive `require` materializes registered requests |
|
||||
| Fetch response text, then inject an inline `<script>` | Makes the module system buffer the complete source and maintain separate fetch/execute paths; dynamic source execution also breaks the browser-native association among the network resource, source map, and profile |
|
||||
| Builder-push rebuild channel (`POST /plugins/rebuilt` from the orchestrator's `onSuccess`) | Couples reload to one blessed builder process and a second wire protocol; the webserver already holds every bundle path, and stat polling covers the torn-write race (re-hash on every stat change) that once justified pushing |
|
||||
| Builder-push rebuild channel (`POST /plugins/rebuilt` from the orchestrator's `onSuccess`) | Couples reload to one blessed builder process and a second wire protocol; the webserver already holds every bundle path, and stat polling covers the torn-write race (publication on every stat change) that once justified pushing |
|
||||
@@ -46,7 +46,7 @@ Host 会快照每个已构建插件入口 bundle,并把每个调度阶段的
|
||||
|
||||
共享 tsdown 预设为每个插件入口与 chunk 产出 map,并把第一方源码路径重写成浏览器可识别的仓库形式 `/packages/<group>/<package>/src/...`。生产 Client 构建会消费 `lib/types`;预设把每份 tsc map 交给 Rolldown,并从原文件补齐 `sourcesContent`,使最终 map 回到 TypeScript/TSX,而不是停在编译后的 JavaScript。内联进 bundle 的其他 workspace 源码同样回到其 `packages/` 归属,依赖包路径保持原样。Combo 生成会移除每个局部调试指令、记录其生成行偏移、以原插件 map URL 解析每个自带 source,再产出 Indexed Source Map v3。插件有自带 map 时直接用于对应 section;没有时则生成 identity section,内嵌构建后 bundle,并在存在时把 packer 写入的 `sourceURL` 用作 source 名。绝对 combo map URL 会平行改写脚本资源列表中的每个 `client.js` 后缀,因此 `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` 指向 `/plugins/??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`。chunk 脚本同样指向自己的 map URL。只有在这些 map URL 收到 `GET` 后,source map 文件才会被读取和组合;`HEAD` 不会物化脚本或 map body。Vite 壳同样产出 sourcemap,使壳代码与经外部加载的插件都能从 stack 和性能 profile 回到 TypeScript/TSX。
|
||||
|
||||
图为 HMR 保留每个 row 带 revision 的单资源 combo URL,并为每个启动 combo 请求增加带 revision 的描述;多条描述可以使用同一调度阶段。初始 row revision 是进程级不透明 nonce,而不是内容哈希;它无需在启动时哈希每个插件,也能保证已快照的单资源响应不可变。共享预设会在每个包输出写完后标记入口。watcher 观察到该构建完成标记后,`rebuilt(id)` 会把入口字节与其时间戳一起哈希并发布所得 revision;因此只重建 chunk 也会推进 owner revision,无需 Host 扫描 sibling。启动 combo revision 从有序 row revision 派生。脚本 body 在首次 `GET` 时组合;source map 文件在首次 map `GET` 时单独读取并组合。`HEAD` 不会物化任一 body。版本化脚本与 map 使用 immutable 缓存,无关图重组会保留相同 revision 下已经物化的 chunk 响应。Host 只提供精确生成的 URL;陈旧 revision 与未发布资源列表返回 404,不会别名到其他字节。外部脚本的 `error` 事件不给响应状态与正文,因此失败诊断只报告 URL;同源 Host 与构建期写入的 registration id 是身份边界,`load` 后的 factory 存在性检查负责拒绝未登记预期 id 的产物。
|
||||
图为 HMR 保留每个 row 带 revision 的单资源 combo URL,并为每个启动 combo 请求增加带 revision 的描述;多条描述可以使用同一调度阶段。首次发布与 `rebuilt(id)` 使用相同的入口 mtime、ctime 和大小哈希;产物内容不参与求哈希。ctime 用于区分保留 mtime 和大小的重写。Host 重启时若产物未变,每个 row 的 revision 都保持不变,因此重连后的 SSE 快照不会触发浏览器插件替换。共享预设会在每个包输出写完后标记入口;因此只重建 chunk 也会推进 owner revision,无需 Host 扫描 sibling。启动 combo revision 从有序 row revision 派生。脚本 body 在首次 `GET` 时组合;source map 文件在首次 map `GET` 时单独读取并组合。`HEAD` 不会物化任一 body。版本化脚本与 map 使用 immutable 缓存,无关图重组会保留相同 revision 下已经物化的 chunk 响应。Host 只提供精确生成的 URL;陈旧 revision 与未发布资源列表返回 404,不会别名到其他字节。外部脚本的 `error` 事件不给响应状态与正文,因此失败诊断只报告 URL;同源 Host 与构建期写入的 registration id 是身份边界,`load` 后的 factory 存在性检查负责拒绝未登记预期 id 的产物。
|
||||
|
||||
### 装载流程,端到端
|
||||
|
||||
@@ -56,7 +56,7 @@ Host 会快照每个已构建插件入口 bundle,并把每个调度阶段的
|
||||
|
||||
1. 负责组合的 app(`apps/cli`)把名册作为普通行放进它的 `cordis.yml` 配置树——client 插件包与每个 host 插件一样是 entry 行,包括无条件挂载的 `client-hmr` 行。`auditStartupEntries` 报告 import 失败、带原始 stack 的激活错误,以及待满足的依赖;optional entry 输出 warning,required entry 则使启动失败([启动策略](2026-09-09-consumer-owned-startup-strictness.zh.md))。
|
||||
2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)使用 Host face import 时相同的 `name` 与所属 tree `baseUrl` 解析每个 live Loader entry,再读取最近归属 package.json 的 `dsh.client` 声明并组合出 `window.__DSH_BOOT__`:`{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`。即使 overlay 指向相对的 source 或 built entry 文件,manifest 包名仍是浏览器模块身份。若不同的 active Loader source 解析到同一包名,组合会失败;一个来源卸载后,仍存活的来源无需重启 fiber 即可提供该 row。Row 的三个可选字段都来自 manifest,永不人肉抄写。组合会把被请求的动态图 row 排到消费者之前、拒绝同步请求环,并把每个 row 恰好分配给一个初始批次。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下;畸形声明字段同样会让激活失败,Host 检查会从 FAILED fiber 报告这两类错误。
|
||||
3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,包元数据(含「非 client 包」的否定结论)按 entry 名与所属 tree base URL 缓存至进程结束,bundle 重哈希只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。初始 row 使用不透明的进程 nonce 加序号,不对其产物求哈希;启动 combo revision 对合并脚本输入及 indexed map 求哈希,row 与批次描述再共同哈希进 `graph.rev`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知;modules 会注册 combo 路由并贡献结构化 index 注入行。
|
||||
3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,包元数据(含「非 client 包」的否定结论)按 entry 名与所属 tree base URL 缓存至进程结束,变化后 bundle 的发布只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。初始 row 对读取前的文件系统元数据求哈希;启动 combo revision 对有序 row 的身份与 revision 求哈希,row 与批次描述再共同哈希进 `graph.rev`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知;modules 会注册 combo 路由并贡献结构化 index 注入行。
|
||||
|
||||
为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个在仓库中声明了 dsh.client 的包,不代表这次部署要挂载它,扫描发现无从替人做这个决定;node 半只扫描配置树实际挂载了的东西。
|
||||
|
||||
@@ -80,7 +80,7 @@ Host SSE 适配器转发现有图变化通知,并在连接时发送当前完
|
||||
|
||||
热重载是一项组合决策:web 组合包无条件挂载 `client-hmr` 行(一个常规的插件包),其 node 半带来 bundle 监视与 SSE(Server-Sent Events)通道;没有重建 watcher 改写客户端 bundle 时链路保持空闲。不应暴露它的组合可以禁用该行。
|
||||
|
||||
重建好的 bundle 怎么变成重载信号?hmr 的 node 半自己观察——没有构建器来通知它。模块 host 在读取每份启动快照前捕获入口 stat 基线,并通过 `ctx.clientModules.artifactBaseline(id)` 暴露它。HMR 自持的单个定时器把当前图的每个 row 与这份基线比较:未变化的 row 直接开始监视,不读取内容也不求哈希;基线捕获后的写入已经形成 stat 差异,只有该 row 会进入 `rebuilt(id)`。这同时消除了启动期的全量重哈希,并避开 `fs.watchFile` 以异步首次 stat 建立基线、可能静默吸收构造期重建的问题。监视集合的成员随 `onGraphChanged` 更新;消失的 row 撤下监视,轮询时缺失的入口则让对应 row 保持标脏状态,文件重现时即使元数据相同也强制生成新 revision。共享 tsdown 预设会在每个 sibling 输出写完后标记 `client.js`;入口 mtime 或 size 变化、或 row 处于标脏状态时,`rebuilt(id)` 从入口字节与构建完成时间戳派生 revision。只修改 chunk 因此也会更换 revision;部分写入之后的完成标记还会提供一次更晚的 stat 变化以完成自愈。`rev` 变化时,node 半在 `GET /plugins/events` 上广播 `rebuilt` 帧——这是一条系统级 SSE 通道,连接即发全量图,变更时发 `rebuilt` 帧,仅供呈现的 wire,永不进会话日志。轮询是刻意选择:inotify 在 weka 网络挂载上不触发,构建侧监视器需要 `--poll` 也是同一原因;每个 row 每个间隔只需一次入口 stat,轮询间隔是一个经校验的配置字段(默认 500ms),dispose(资源释放)会清掉那一个定时器。重建产物的进程必须使用共享 Client tsdown 预设;`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dsh.client 发现,构建器与 host 之间没有通知协议。
|
||||
重建好的 bundle 怎么变成重载信号?hmr 的 node 半自己观察——没有构建器来通知它。模块 host 在读取每份启动快照前捕获入口 stat 基线,并通过 `ctx.clientModules.artifactBaseline(id)` 暴露它。HMR 自持的单个定时器把当前图的每个 row 与这份基线比较:未变化的 row 直接开始监视,不读取内容也不求哈希;基线捕获后的写入已经形成 stat 差异,只有该 row 会进入 `rebuilt(id)`。这既避免安装 watcher 时重复读取,也避开 `fs.watchFile` 以异步首次 stat 建立基线、可能静默吸收构造期重建的问题。监视集合的成员随 `onGraphChanged` 更新;消失的 row 撤下监视,轮询时缺失的入口则让对应 row 保持标脏状态,文件重现时即使 mtime 和大小与基线相同也重试发布。共享 tsdown 预设会在每个 sibling 输出写完后标记 `client.js`;入口 mtime、ctime 或大小变化、或 row 处于标脏状态时,`rebuilt(id)` 从这三个元数据值派生 revision。只有 revision 变化时,它才读取新的 bundle 快照。只修改 chunk 因此也会更换 revision;部分写入之后的完成标记还会提供一次更晚的 stat 变化以完成自愈。`rev` 变化时,node 半在 `GET /plugins/events` 上广播 `rebuilt` 帧——这是一条系统级 SSE 通道,连接即发全量图,变更时发 `rebuilt` 帧,仅供呈现的 wire,永不进会话日志。轮询是刻意选择:inotify 在 weka 网络挂载上不触发,构建侧监视器需要 `--poll` 也是同一原因;每个 row 每个间隔只需一次入口 stat,轮询间隔是一个经校验的配置字段(默认 500ms),dispose(资源释放)会清掉那一个定时器。重建产物的进程必须使用共享 Client tsdown 预设;`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dsh.client 发现,构建器与 host 之间没有通知协议。
|
||||
|
||||
浏览器侧的传输把代码替换交给负责图对账的同一个 modules 控制器:
|
||||
|
||||
@@ -120,4 +120,4 @@ Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块
|
||||
| 现在就彻底 ctx 化(React 与库全走服务,不设模块表) | 静态 UI 库仍暴露同步实体,因此删除模块表会让这些 import 失去共享身份 |
|
||||
| 冻结表 + 到达即实例化 | 会在 script 到达时执行 bundle 副作用;惰性登记把执行推迟到 Cordis import,并由递归 `require` 物化已登记请求 |
|
||||
| fetch 响应文本后注入内联 `<script>` | 模块系统必须缓冲整份源码并维护 fetch/execute 两条路径;动态源码执行也切断浏览器网络资源、sourcemap 与 profile 的原生关联 |
|
||||
| 构建器推送重建通道(编排器在 `onSuccess` 里 POST `/plugins/rebuilt`) | 把重载耦合到一个钦定的构建器进程和第二套 wire 协议;webserver 本就握有每个 bundle 路径,stat 轮询(每次 stat 变化即重哈希)已兜住当年为推送辩护的撕裂写竞态 |
|
||||
| 构建器推送重建通道(编排器在 `onSuccess` 里 POST `/plugins/rebuilt`) | 把重载耦合到一个钦定的构建器进程和第二套 wire 协议;webserver 本就握有每个 bundle 路径,stat 轮询(每次 stat 变化即发布)已兜住当年为推送辩护的撕裂写竞态 |
|
||||
+2
-2
@@ -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/architecture/2026-08-31-released-session-format-migrations.md
|
||||
2026-08-31-released-session-format-migrations.md: e21793e0ea3d0c5376cbb3949c5d6ef599b9afb1
|
||||
2026-08-31-released-session-format-migrations.zh.md: 637d8fcf0206661df9a6f1b3b470ad8a55746720
|
||||
2026-08-31-released-session-format-migrations.md: 00672af920cf8369022b4274c1c50552f5407a7e
|
||||
2026-08-31-released-session-format-migrations.zh.md: 9404d8f454b7c17be69ae064c405c4dcc0794f88
|
||||
+2
@@ -89,6 +89,8 @@ Each released codec creates a row decoder with explicit `strict` or `recoverable
|
||||
|
||||
The v0-to-v1 edge preserves logical headers, sequence numbers, references, timestamps, and payloads except for bounded released-v0 normalizations. It translates the retired `steering/message` and `compact/*` event names, accepts a released `llm/retry` after its matching `step/end`, deterministically supplies a missing `llm/retry.retryId` per turn/step/provider/policy chain, and supplies one deterministic `compactionId` across a legacy compaction group that omitted it. The v1-to-v2 edge owns attempt folding and reference remapping, and emits only settled v2 events. It splits a legacy goal-sourced user message into `goal/change` plus the original model-visible message. It also inserts an interrupted `turn/end` for the bounded released restart in which an open turn with no open step is followed by a non-empty `next-turn` inbox splice and the next numbered `turn/start`.
|
||||
|
||||
The V3-to-V4 edge applies the same bounded restart repair to logs written directly as V3. Keeping this repair in the incoming edge preserves [native V4 lifecycle validation](2026-09-17-native-v4-read-validation.md); accepting repeated starts there would admit producer corruption in new logs. Insertions require local-reference and inherited-cut remapping, while captured generations retain their original coordinates. The [format specification](../../../../packages/session/session-format-v3-to-v4/README.md#sequence-references) owns the exact evidence and fields.
|
||||
|
||||
The catalog exposes one `createRestore()` operation for production, Worker, fixture, and replay callers. Recovery policy and final validation policy are chosen once at restore creation. Historical production uses recoverable source parsing with transformed-current validation; this validates the released current result after migration, while input that is already current receives only codec validation. Worker and fixture verification use strict parsing with full installed current restoration. A migration-stage or transformed-current validation refusal remains `SessionFormatUnsupportedMigrationError`; physical decoding failures remain corruption. Test support keeps only fixture-specific token and envelope materialization.
|
||||
|
||||
### JSONL integration
|
||||
|
||||
+2
@@ -89,6 +89,8 @@ Chain 中不存在 `flatMap`、spread expansion、中间 event array 或 schedul
|
||||
|
||||
v0-to-v1 除了有限的 released-v0 归一化外,会保留逻辑 header、seq、引用、时间戳与 payload。它转换已移除的 `steering/message` 与 `compact/*` 事件名称,接受出现在对应 `step/end` 之后的已发布 `llm/retry`,按 turn/step/provider/policy chain 为缺失的 `llm/retry.retryId` 确定性补值,并为省略 id 的旧 compaction group 确定性补充同一个 `compactionId`。v1-to-v2 负责 attempt folding 与引用重写,并且只 emit 已结算的 v2 event。它会把旧的 goal 来源 user message 拆成 `goal/change` 与原本的模型可见 message。它还会为一种有限的已发布 restart 插入 interrupted `turn/end`:一个没有 open step 的 open turn 后出现非空 `next-turn` inbox splice,随后直接开始编号连续的下一轮。
|
||||
|
||||
V3-to-V4 边对直接写成 V3 的日志应用同样的有限 restart 修复。将修复保留在迁入边,可以维持[原生 V4 生命周期校验](2026-09-17-native-v4-read-validation.zh.md);若在那里接受重复 start,新日志也会允许生产者损坏。插入事件要求重映射本地引用与继承截点,而捕获代际保留原坐标。[格式规范](../../../../packages/session/session-format-v3-to-v4/README.zh.md#sequence-references)负责定义具体证据和字段。
|
||||
|
||||
Catalog 为 production、Worker、fixture 与 replay 暴露同一个 `createRestore()`。Recovery policy 与最终 validation policy 在 restore 创建时一次确定。Historical production 使用 recoverable source parsing 与 transformed-current validation;这种策略会在迁移后校验已发布 current 结果,而已经是 current 的输入只接受 codec 校验。Worker 与 fixture verification 使用 strict parsing 与已安装 current 格式的完整 restoration。Migration stage 或 transformed-current validation 的拒绝会保持为 `SessionFormatUnsupportedMigrationError`;物理解码失败仍是 corruption。Test support 只保留 fixture 自身需要的 token 和 envelope materialization。
|
||||
|
||||
### JSONL 串联
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/architecture/2026-09-08-desktop-welcome-window-material.md
|
||||
2026-09-08-desktop-welcome-window-material.md: 4ee5f6d2934378b06754c4a8f134a4ca07a80840
|
||||
2026-09-08-desktop-welcome-window-material.zh.md: 1689af8481925c787c1d2412173fbd7d8c2f4b64
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Desktop welcome window material
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-08-desktop-welcome-window-material.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The desktop welcome design blurs the desktop behind its entire window. Chromium backdrop filters only sample content inside the renderer. Credential setup also needs an explicit startup owner so a native skip is not followed by a second Web dialog.
|
||||
|
||||
## Decision
|
||||
|
||||
The [Electron welcome window](../../../../apps/desktop/src/welcome-window.ts) owns native material and window controls. Its independently bundled React renderer supplies the entry, account sign-in states and API-key form. It shares the `StateDot` loading component with the Web UI without booting the Web plugin graph. The Desktop build emits local JavaScript and CSS under `lib/welcome`, included in packaged applications; the document keeps its network-denying content security policy. A narrow preload supplies typed shell copy, a write-only key operation, and a skip operation; IPC rejects other windows and subframes. Each sandboxed preload is bundled independently because Electron’s restricted require cannot load sibling chunks. The Electron main process authenticates through the shared Web launch URL and resolves the official provider's reference through the existing settings and credential RPC methods. The [thin Web wrapper](2026-09-10-desktop-web-wrapper.md) owns the HTTP server; onboarding adds no Host endpoint or child IPC operation. Responses contain metadata or a safe outcome, never a key or private provider diagnostics.
|
||||
|
||||
Cold startup opens the entry when neither an account credential nor a model API key is stored. Saving enters the workspace after persistence succeeds; skipping enters without saving a draft or a completion flag. The next process launch checks the credential store again. The Desktop preload marker suppresses only the Web credential step; settings and the welcome notice remain mounted. Other native shells can set `credentialOnboarding` through the Models Host plugin, which publishes that boolean through the page-injection event before dialogs register. The module graph carries package identities rather than arbitrary Host config, so a Host row alone does not configure its Client half. Startup reads account presence and API-key configuration independently. Account sign-in enters the workspace; sign-out reopens the welcome window only when no model API key remains configured.
|
||||
|
||||
The generated project follows declared workspace dependencies because pnpm’s hoist index alone can omit configured plugins.
|
||||
|
||||
Desktop reads the shared `locale.preference` before opening the welcome window. The Client awaits an isolated preload read of that preference and the same OS language order before mounting, then reports resolved locale changes to the shell. Only an explicit Settings selection writes the preference; automatic detection stays provisional. The shell ships English and Simplified Chinese dictionaries, so a Client-only language pack falls back to a supported OS language in native windows.
|
||||
|
||||
The welcome flow shares the Desktop backend controller with startup recovery. The main recovery document loads offscreen while the Host starts; a failure reveals it, and successful startup chooses the welcome window or workspace after reading credentials and locale. Recovery actions retain the controller’s serialized retries and awaited child shutdown. The application preload exposes recovery controls only to shell documents and the locale bridge only to application documents.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A CSS-only backdrop filter.** It cannot blur other application windows or the desktop, so it cannot provide the required native effect.
|
||||
|
||||
**Persisting a completed-onboarding flag.** A skip is valid only for the current process; retaining it would suppress the next cold-start choice while the user still has no credentials.
|
||||
|
||||
**Keeping both native and Web credential dialogs.** Their separate completion state would ask for a key twice after a native skip. The composition assigns credential onboarding to one owner.
|
||||
|
||||
**Drawing replacement traffic lights.** Native controls preserve platform window behavior and accessibility. Their dimensions and outer window corners follow the OS rather than reproducing Figma geometry exactly.
|
||||
|
||||
**Separate preview callbacks.** A visual-only skip cannot demonstrate entry into the workspace and can hide broken startup dependencies. The preview uses the same callbacks and Host as the product.
|
||||
|
||||
## Consequences
|
||||
|
||||
Native blur strength and font fallback vary by system. macOS uses menu vibrancy with the onboarding window tint: 40% white in light mode and 50% rgb(24 25 28) in dark mode. Native materials own the blur strength. Windows compositing needs platform QA. Owner-local text expectations cover both pages and locales, and a built-Host acceptance test proves credential persistence across restarts. Account sign-out preserves independently configured API keys. Onboarding presentation produces no Session events.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Agent Note: 桌面欢迎窗口材质
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-08-desktop-welcome-window-material.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
桌面欢迎界面的设计要求整个窗口模糊其背后的桌面。Chromium 背景滤镜只能采样渲染器内部的内容。凭证配置还需要明确的启动归属,避免跳过原生引导后又出现 Web 弹窗。
|
||||
|
||||
## 决策
|
||||
|
||||
[Electron 欢迎窗口](../../../../apps/desktop/src/welcome-window.ts)负责原生材质和窗口控件。独立打包的 React 渲染器提供入口、账号登录状态和 API Key 表单。它与 Web UI 共用 `StateDot` 加载组件,不启动 Web 插件图。Desktop 构建将本地 JavaScript 和 CSS 输出到 `lib/welcome`,并纳入应用安装包;文档继续使用禁止网络访问的内容安全策略。窄接口 preload 提供强类型桌面文案、仅写密钥操作和跳过操作;IPC 拒绝其他窗口和子 frame。每个沙箱 preload 都独立打包,因为 Electron 受限的 require 无法加载同目录中的拆分模块。Electron 主进程通过共享 Web 启动 URL 认证,并通过现有设置和凭证 RPC 方法解析官方提供方的引用。[Web 薄壳](2026-09-10-desktop-web-wrapper.zh.md)负责 HTTP 服务;引导不增加 Host 端点或子进程 IPC 操作。响应只包含元数据或安全结果,不包含密钥或提供方的私有诊断。
|
||||
|
||||
冷启动在既无已存账号凭证、也无已配置的模型 API Key 时打开入口。保存会在持久化成功后进入工作区;跳过会直接进入,不保存草稿或完成标记。下次进程启动时会重新检查凭证存储。Desktop preload 标记只抑制 Web 凭证步骤;设置和欢迎须知仍然挂载。其他原生壳可以通过 Models Host 插件设置 `credentialOnboarding`,由页面注入事件在注册弹窗前发布该布尔值。模块图传递的是包标识,而不是任意 Host 配置,因此单独配置 Host 行不会配置其 Client 半部。启动分别读取账号凭证是否存在和 API Key 配置状态。账号登录后进入工作区;退登后仅在没有已配置的模型 API Key 时重新打开欢迎窗口。
|
||||
|
||||
生成的开发项目沿已声明的 workspace 依赖建立链接,因为单独依赖 pnpm 的提升索引会漏掉配置插件。
|
||||
|
||||
Desktop 在打开欢迎窗口前读取共享的 `locale.preference`。Client 在挂载前等待隔离 preload 返回该偏好和同一系统语言顺序,之后将解析后的语言变更通知桌面壳。只有设置中的明确选择会写入偏好;自动检测保持临时状态。桌面壳自带英文和简体中文词典,因此仅在 Client 中提供的语言包会在原生窗口回退到受支持的系统语言。
|
||||
|
||||
欢迎流程与启动恢复共用 Desktop 后端控制器。Host 启动期间,主窗口在屏幕外加载恢复文档;失败时显示该文档,成功后读取凭证和语言,再选择欢迎窗口或工作区。恢复操作保留控制器的串行重试与等待子进程退出的行为。应用 preload 只向壳文档提供恢复控件,只向应用文档提供语言桥接。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**仅使用 CSS 背景滤镜。** 它无法模糊其他应用窗口或桌面,因此不能提供所需的原生效果。
|
||||
|
||||
**持久化已完成引导标记。** 跳过只对当前进程有效;保存该标记会在用户仍无凭证时抑制下次冷启动的选择。
|
||||
|
||||
**同时保留原生与 Web 凭证弹窗。** 两者独立的完成状态会在跳过原生引导后再次要求输入密钥。组合将凭证引导交给单一归属方。
|
||||
|
||||
**绘制替代红绿灯。** 原生控件保留平台窗口行为和无障碍能力。它们的尺寸与窗口外部圆角遵循操作系统,不严格复制 Figma 几何参数。
|
||||
|
||||
**独立的预览回调。** 纯视觉预览中的跳过无法证明能进入工作区,还会掩盖启动依赖缺失。预览使用与产品相同的回调和 Host。
|
||||
|
||||
## 后果
|
||||
|
||||
原生模糊强度和字体回退因系统而异。macOS 使用 menu vibrancy,叠加 onboarding 的窗口背景色:浅色模式为 40% 白色,深色模式为 50% rgb(24 25 28)。模糊强度由原生材质控制。Windows 合成效果需要平台验证。所属目录内的文本预期覆盖两页和两种语言,构建后 Host 的验收测试证明凭证可跨重启持久化。账号退登保留独立配置的 API Key。引导展示不会生成 Session 事件。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/architecture/2026-09-14-deepseek-account-login.md
|
||||
2026-09-14-deepseek-account-login.md: 458e020c2000fd3db5ee9806e99d4b653710cb1e
|
||||
2026-09-14-deepseek-account-login.zh.md: 2fb59009302309a67e2c3ec99050685ba39b4212
|
||||
@@ -0,0 +1,69 @@
|
||||
# Agent Note: DeepSeek account browser login
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-14-deepseek-account-login.zh.md)
|
||||
|
||||
Sign-in captures the initiating UI language per attempt so browser authorization follows the Desktop or Settings locale without depending on the Platform default.
|
||||
|
||||
## Problem
|
||||
|
||||
Desktop login must preserve local cancellation across the browser, UI, and Host and report success only after credentials are saved.
|
||||
|
||||
## Decision
|
||||
|
||||
The account Service Definition separates UI and model consumers from the platform protocol implementation. The platform provider registers an AuthorizationFlow and persists an owner-scoped GrantRecord. Account presence is derived from storage. Login attempts stay in memory and expose safe snapshots through the account Remote controller.
|
||||
|
||||
Local cancellation is authoritative. The provider stops callbacks and ignores late exchange results; auth_cancel invalidates the remote application and unexchanged code using authorize_id and the original PKCE verifier. Its background request does not delay local cancellation, and failure never restores login. session.commit admits persistence synchronously before its first await and rejects an already-cancelled flow. Cancellation after admission waits for the commit result. The callback redirects to auth_exchange.biz_data.authorized_url only after authorization settlement confirms storage.
|
||||
|
||||
Authorization registers a temporary /oauth/callback route on the Host webServer. The authenticated initiating client supplies its browser-accessible loopback HTTP origin, including any local SSH forwarding port. Non-loopback reverse proxies are unsupported. Cleanup removes only the route and preserves shared connections. Exchange failure returns Web to its login UI or focuses Desktop through account state; retry is explicit. PKCE secrets never cross UI transports. Model and file request tokens resolve only for the configured inferenceOrigin, defaulting to `https://api.deepseek.com`; redirects are refused. An explicit alternate origin requires grants from the configured Platform issuer, so changing the endpoint alone never forwards account credentials. Development loopback grants cannot authenticate production requests. API-key references remain separately stored.
|
||||
|
||||
Platform navigation and authorization share one validated platformOrigin from Cordis configuration. Private profile patches or environment expressions supply deployment-specific origins without committing development addresses.
|
||||
|
||||
The Desktop profile supplies the native platform for Host API headers. Account and update-policy clients share one mapping to avoid different client identities for the same installation. This identity does not depend on a login attempt, since stored grants also issue requests after restart. Embedded pages use the same platform identity and retain their other separately configured headers.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Putting the protocol in Electron duplicates the implementation for Web consumers and gives the shell credential ownership. Putting tokens in DEEPSEEK_API_KEY loses the distinction between account grants and user API keys. Treating a browser callback as success before persistence admits false success. Waiting for a remote cancel acknowledgment leaves local cancellation dependent on network availability. These alternatives are rejected.
|
||||
|
||||
## Consequences
|
||||
|
||||
Sign out removes the local grant before starting Platform POST /auth-api/v0/users/logout with the captured token. Remote failure never restores login. The provider retries at most five times after the initial request, with configurable exponential backoff starting at one second by default. Retry jobs retain only their old token, cannot change later login state, and end at provider shutdown without durable storage. Local credential deletion must succeed before publishing signed-out state. Sign out belongs to the sidebar account menu; Account settings owns profile, balance and sign-in. Both read one plugin-owned Host stream through framework hooks.
|
||||
|
||||
API keys and account grants remain separately stored. Account tokens have no expiry or refresh flow; exhausted logout retries can leave the removed token valid remotely. Provider initialization discards grants from a different configured Platform issuer locally, without remote revocation, so switching environments starts signed out instead of aborting Desktop startup. API keys and device identity survive this cleanup. Profile and balance query failures preserve login state; authorization-attempt deadlines apply only before token issuance. The same grant authenticates Platform current and get_user_summary queries on its configured issuer origin. Host preserves Platform-masked contact details and drops response tokens; account changes invalidate pending results. UI shows normal_wallets recharge balances and positive bonus_wallets in separate rows, so promotional credit is never presented as recharge funds.
|
||||
|
||||
The account plugin supplies choice, waiting, failure and timeout dialogs through settings.models.sign-in. The model package owns credential readiness and the existing API-key editor; the settings shell coordinates explicit reopening so login and API-key onboarding do not mount competing dialogs.
|
||||
|
||||
Platform embedding transfers a grant from Host to Electron over private Node IPC and then once to the trusted Platform main frame through a sandboxed preload. Preload performs one synchronous IPC before page scripts execute; the main process only validates the sender and returns prepared memory. Platform reads the token synchronously thereafter without a readiness API. A failed initialization retains embedded mode and a throwing getter. Account UI projections remain credential-free. The Platform document can read this credential, so its script security is part of account protection; context isolation protects native capabilities, not a token intentionally returned to the document. Replacing or removing the grant destroys the document and its temporary session.
|
||||
|
||||
Support questionnaires receive only available environment metadata through explicit prefill fields. Account UIDs, tokens and masked contact details stay out of questionnaire URLs.
|
||||
|
||||
Private proxy development can explicitly map authorization and completion URLs to platformOrigin with rewriteBrowserOrigin. Both pages retain their fixed paths and full query strings; allowing a remote origin without mapping it would send the browser outside the configured environment. Shipped configuration requires same-origin URLs.
|
||||
|
||||
DSH grants authenticate Platform, inference and Files requests with x-dsh-auth-token, without a Bearer prefix. API-key authentication remains protocol-specific.
|
||||
|
||||
Profile and recharge-wallet queries use independent getProfile and getBalance operations. The client publishes each result as it arrives, so a slow or failed balance request cannot delay the sidebar username. Account changes invalidate both pending results.
|
||||
|
||||
Deployment authentication uses explicit Host-only requestHeaders on the configured Platform origin. The provider rejects redirects and reserved-header overrides so deployment cookies cannot replace account authorization or follow a browser destination. Environment-specific authentication protocols remain outside the account provider.
|
||||
|
||||
The bundled account UI is Desktop-only: its preload bridge enables the account launcher, settings and sign-in onboarding. Plain Web retains API-key onboarding and the standard Settings launcher without account subscription or login. The Host protocol accepts login_source (desktop or web) in auth_init; the bundled UI sends desktop. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
|
||||
|
||||
The macOS development launcher registers an isolated, ad-hoc-signed application bundle for `dsh://open`. The bundle retains the workspace entry and development paths for Launch Services cold starts without copying credentials or changing the package-manager-owned Electron application. Protocol registration targets the latest launched development or packaged application.
|
||||
|
||||
Exchange user data supplies the first profile read after credential commit, avoiding a second request before displaying the username. The Host retains only projected UI fields in the active attempt, matched to its token and consumed once; later reads use current. Missing or malformed user data does not discard successful authorization.
|
||||
|
||||
The embedded Platform document stays hidden during loading because native child views cover renderer overlays. Only the current document may become visible after loading; returning or signing out invalidates pending visibility changes. Native ownership also ends when the application document reloads or is replaced, its renderer terminates, or its window closes. React effect cleanup alone is insufficient because document teardown may never execute it. Same-document and subframe navigation preserve the view.
|
||||
|
||||
Separate accountRequestHeaders route account data and embedded Platform traffic independently of authorization and logout. Cookie overrides merge by name, retaining deployment authentication. Host passes the resolved headers over private process IPC; Electron injects them only at the configured origin and omits them from bootstrap.
|
||||
|
||||
The Host binds private Platform sessions to the account provider lifetime. Removal or watch termination clears Electron’s session; replacement subscribes to the new provider, and disposed reads cannot publish old credentials.
|
||||
|
||||
The client distinguishes plugin disposal from terminal account-stream failure. RemoteStream aborts its signal after either outcome, so the plugin owns a separate disposal flag to preserve failure feedback while suppressing reports after unload.
|
||||
|
||||
## Verification
|
||||
|
||||
Provider tests exercise real loopback callbacks, invalid state, delayed exchange cancellation, credential persistence, sign-out, and official-origin restrictions. Desktop tests cover the native action bridge and localized entry. Manual development integration uses the platform dev middleware Mock and the real Electron Host, including cancellation before browser approval. Production backend credentials and installer scheme registration require release-environment validation.
|
||||
|
||||
## Related
|
||||
|
||||
[Credential records and flows](2026-08-13-credential-records-and-authorization-flows.md) remains the generic credential authority. [Desktop wrapper](2026-09-10-desktop-web-wrapper.md) owns the transport composition.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Agent Note: DeepSeek 账号浏览器登录
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-14-deepseek-account-login.md) | 中文
|
||||
|
||||
登录申请保存发起界面的语言,使浏览器授权跟随桌面或设置页的语言,不依赖平台默认值。
|
||||
|
||||
## 问题
|
||||
|
||||
桌面登录必须在浏览器、UI 和 Host 之间保留本地取消意图,并只在凭证保存后宣告成功。
|
||||
|
||||
## 决策
|
||||
|
||||
账号服务定义将 UI 和模型使用方与平台协议实现分开。平台提供者注册 AuthorizationFlow,并保存按所有者寻址的 GrantRecord。账号是否存在从存储派生。登录尝试保留在内存中,通过账号 Remote 控制器提供安全快照。
|
||||
|
||||
本地取消具有决定权。提供者停止回调并忽略迟到的兑换结果;auth_cancel 使用 authorize_id 和原始 PKCE verifier 作废远程申请及未兑换的 code。后台请求不延迟本地取消,失败也不会恢复登录。session.commit 在第一次 await 前同步准入持久化,并拒绝已取消的流程。准入后的取消等待提交结果。回调只有在授权结束确认存储后才跳转 auth_exchange.biz_data.authorized_url。
|
||||
|
||||
授权在 Host webServer 上注册临时 /oauth/callback 路由。已鉴权的发起客户端提供浏览器可访问的本机 HTTP 来源,包含 SSH 本地转发端口。不支持非本机域名反向代理。清理仅移除路由,保留共享连接。兑换失败返回 Web 登录界面,或通过账号状态聚焦 Desktop;重试由用户明确发起。PKCE 私密数据不经过 UI 传输。模型和文件请求的 token 仅为配置的 inferenceOrigin 解析,默认为 `https://api.deepseek.com`;请求拒绝重定向。显式配置其他来源时,要求授权由配置的 Platform 来源签发,因此仅修改请求地址不会转发账号凭证。本机开发授权记录不能认证生产请求。API Key 引用独立保存。
|
||||
|
||||
平台导航与授权共享 Cordis 配置中同一个经过校验的 platformOrigin。私有 profile patch 或环境表达式提供部署来源,开发地址不进入仓库。
|
||||
|
||||
Desktop profile 为 Host API 请求头提供原生平台信息。账号与更新策略客户端共用一份映射,避免同一安装实例发送不同的客户端标识。此标识不依赖登录尝试,因为重启后保存的授权仍会发起请求。内嵌页面使用同一平台标识,并保留独立配置的其他请求头。
|
||||
|
||||
## 替代方案
|
||||
|
||||
把协议放在 Electron 会为 Web 使用方重复实现,并让外壳拥有凭证。把 token 放入 DEEPSEEK_API_KEY 会丢失账号授权与用户 API Key 的区别。在持久化前将浏览器回调视为成功会产生虚假成功。等待远程取消确认会使本地取消依赖网络。这些方案均被拒绝。
|
||||
|
||||
## 影响
|
||||
|
||||
退出登录先删除本地授权,再使用捕获的 token 调用 Platform POST /auth-api/v0/users/logout。远程失败不会恢复登录态。首次请求失败后,提供者最多重试五次,使用可配置的指数退避,默认从一秒开始。重试任务只保留旧 token,不改变后续登录态,并在提供者关闭时结束,不持久化。本地凭证删除成功后才发布已退出状态。退出登录位于侧边栏账号菜单;账号设置负责资料、余额和登录。两者通过框架 hook 读取插件持有的同一条 Host 状态流。
|
||||
|
||||
API Key 与账号记录独立保存。账号 token 没有过期或刷新流程;退登重试耗尽后,已从本地删除的 token 在远端仍可能有效。提供者初始化时在本地丢弃与配置的 Platform 来源不同的授权,不调用远端撤销,使环境切换以未登录态启动,而不导致 Desktop 启动失败。API Key 和设备标识保留。资料与余额查询失败保留登录态;授权尝试的有效期仅适用于 token 签发前。同一授权 token 可认证已配置签发来源上的 Platform current 和 get_user_summary 查询。Host 原样保留 Platform 脱敏后的联系方式并丢弃响应 token;账号变化使未完成结果失效。UI 分行显示 normal_wallets 充值余额与正额 bonus_wallets 赠金余额,避免将赠金额度显示为充值资金。
|
||||
|
||||
账号插件通过 settings.models.sign-in 提供选择、等待、失败和超时对话框。模型包保留凭证就绪检查和现有 API Key 编辑器;设置外壳协调显式重开,避免登录与 API Key 引导同时挂载冲突的对话框。
|
||||
|
||||
Platform 内嵌通过私有 Node IPC 将授权从 Host 传给 Electron,再经沙箱 preload 一次性传给受信任的 Platform 主 frame。preload 在页面脚本执行前执行一次同步 IPC;主进程仅校验调用来源并返回已准备的内存数据。Platform 此后同步读取 token,无需就绪接口。初始化失败时保留内嵌模式,由 getter 抛错。账号 UI 投影仍不包含凭证。Platform 文档能够读取此凭证,因此其脚本安全也是账号保护的一部分;上下文隔离保护原生能力,不能隐藏有意返回给文档的 token。替换或移除授权会销毁文档及其临时会话。
|
||||
|
||||
支持问卷仅通过显式预填字段接收已有的环境信息,URL 不包含账号 UID、token 或脱敏联系方式。
|
||||
|
||||
私有代理开发可通过 rewriteBrowserOrigin 显式将授权页和完成页映射到 platformOrigin。两者保留固定路径和完整查询字符串;仅放宽来源而不做映射会让浏览器离开配置的环境。发布配置要求浏览器地址同源。
|
||||
|
||||
DSH 授权通过 x-dsh-auth-token 请求头鉴权 Platform、推理和 Files 请求,不加 Bearer 前缀。API Key 继续使用各协议的认证方式。
|
||||
|
||||
资料和充值钱包余额分别通过 getProfile 与 getBalance 查询。客户端在各自返回时立即更新,余额请求慢或失败不会延迟侧边栏用户名显示。账号变化使两种进行中的查询结果失效。
|
||||
|
||||
开发环境认证通过配置的平台来源上的显式 Host 请求头 requestHeaders 完成。提供者拒绝重定向和保留请求头覆盖,防止开发环境 Cookie 替换账号授权或跟随浏览器跳转地址。特定环境的认证协议不属于账号提供者。
|
||||
|
||||
内置账号界面仅用于 Desktop:preload 桥接启用账号入口、设置和登录引导。普通 Web 保留 API Key 引导及标准设置入口,不订阅账号状态或显示登录。Host 协议在 auth_init 中接受 login_source(desktop 或 web);内置界面发送 desktop。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
|
||||
|
||||
macOS 开发启动器为 `dsh://open` 注册独立、经临时签名的应用包。该应用包保留工作区入口和开发路径,使 Launch Services 能够冷启动;凭证不会被复制,也不修改包管理器安装的 Electron 应用。协议注册指向最近启动的开发版或打包版应用。
|
||||
|
||||
exchange 的 user 数据在凭证提交后供首次资料读取使用,展示用户名无需再等待一次请求。Host 仅在当前尝试中保留筛选后的 UI 字段,与 token 匹配并读取一次后移除;后续读取使用 current。缺失或格式无效的 user 不会使已成功的授权失效。
|
||||
|
||||
内嵌 Platform 文档在加载期间保持隐藏,因为原生子视图会覆盖渲染层浮层。加载完成后仅当前文档可显示;返回或退登会使待执行的显示操作失效。应用文档刷新或替换、其渲染进程终止或窗口关闭时,原生视图的所有权也随之结束。仅依赖 React effect 清理不足以保证释放,因为文档销毁时可能不执行它。同文档导航和子框架导航保留视图。
|
||||
|
||||
独立的 accountRequestHeaders 将账号资料及内嵌 Platform 流量与授权、退登分开路由。Cookie 覆盖按名称合并,保留部署认证。Host 通过私有进程 IPC 传递合并后的请求头;Electron 仅在配置来源注入,并从 bootstrap 排除。
|
||||
|
||||
Host 将私有 Platform 会话绑定到账号提供者的生命周期。提供者移除或订阅结束时清空 Electron 会话;替换后订阅新的提供者,已释放的读取不得发布旧凭据。
|
||||
|
||||
客户端区分插件卸载和账户状态流的终止错误。RemoteStream 在两种情况下都会中止其信号,因此插件独立记录卸载状态,保留错误反馈并抑制卸载后的报告。
|
||||
|
||||
## 验证
|
||||
|
||||
提供者测试覆盖真实本机回调、错误 state、延迟兑换取消、凭证持久化、退出和官方来源限制。桌面测试覆盖原生操作桥接与本地化入口。手动开发联调使用平台 dev middleware Mock 和真实 Electron Host,包括在浏览器批准前取消。生产后端凭证及安装器 scheme 注册仍需发布环境验证。
|
||||
|
||||
## 相关记录
|
||||
|
||||
[凭证记录与流程](2026-08-13-credential-records-and-authorization-flows.zh.md)仍是通用凭证依据。[桌面外壳](2026-09-10-desktop-web-wrapper.zh.md)负责传输组合。
|
||||
+2
-2
@@ -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/architecture/2026-09-15-desktop-native-fatal-recovery.md
|
||||
2026-09-15-desktop-native-fatal-recovery.md: 94ead425f5483ba3e6ed5d9d595edb4483254c27
|
||||
2026-09-15-desktop-native-fatal-recovery.zh.md: 2138fb11405d00d51775350bf60d2702621aaa19
|
||||
2026-09-15-desktop-native-fatal-recovery.md: fc1564b02e2f7b707d2b6f6d8e0947b1a78f142d
|
||||
2026-09-15-desktop-native-fatal-recovery.zh.md: ceda54b4141c5fe58e6b385c86e6c1e4958dd261
|
||||
@@ -12,7 +12,7 @@ A recovery document depends on the renderer and preload whose failure can preven
|
||||
|
||||
Electron owns one native fatal dialog per application process. Explicit main-window creation, document-load, preload, renderer, Web initialization, and backend failures enter this path. Ordinary requests retain local error handling, and the shared Web plugin manager owns package-operation failures. Expected cancellation and shutdown do not enter recovery. No elapsed-time heuristic classifies a slow startup as fatal.
|
||||
|
||||
The first report claims presentation before awaiting the dialog. Later reports remain in logs. The Electron console retains the complete reported diagnostic. The dialog bounds the first diagnostic to its final eight lines and limits the complete detail to 1,200 UTF-16 code units, including truncation notice and reinstall advice, because native dialogs cannot scroll. The dialog offers exit, restart, or disabling third-party bundles followed by a whole-application restart. Recovery calls the shared app-boot `sanitizeProfile` function under the existing profile lock after Host shutdown. The function restores caller-supplied bundles and renames the profile patch to a unique backup without parsing it, requiring runtime initialization, or deleting installed files. Callers own profile shutdown and write exclusion; Desktop is the current production caller. The home-level patch remains unchanged. An explicit recovery-operation failure is presented separately and does not count as another automatic fatal report.
|
||||
The first report claims presentation before awaiting the dialog. Later reports remain in logs. A crash report file under the application logs directory retains the complete reported diagnostic, and the dialog names it on its own line ([fatal diagnostics and crash reports](2026-09-22-fatal-diagnostics-and-crash-reports.md)). The dialog bounds the first diagnostic to its final eight lines and limits the complete detail to 1,200 UTF-16 code units, including truncation notice, report path, and reinstall advice, because native dialogs cannot scroll. The dialog offers exit, restart, or disabling third-party bundles followed by a whole-application restart. Recovery calls the shared app-boot `sanitizeProfile` function under the existing profile lock after Host shutdown. The function restores caller-supplied bundles and renames the profile patch to a unique backup without parsing it, requiring runtime initialization, or deleting installed files. Callers own profile shutdown and write exclusion; Desktop is the current production caller. The home-level patch remains unchanged. An explicit recovery-operation failure is presented separately and does not count as another automatic fatal report.
|
||||
|
||||
The Web document stays in place. A carrier callback owns startup failure presentation while the shared boot page retains its spinner; ordinary browser boot still renders its own failure report. Only the primary application frame may report a Web boot failure. Backend state remains in the main process, and native recovery directly owns disabling all third-party bundles. Desktop has no profile reset or emergency recovery document. A fatal backend failure requires one of the native recovery actions rather than an in-process retry.
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
Electron 在每个应用进程中提供一次原生致命错误对话框。明确的主窗口创建、文档加载、preload、渲染器、Web 初始化和后端失败进入此路径。普通请求保留局部错误处理,共享 Web 插件管理器负责包操作失败。预期取消和关闭不进入恢复。不通过耗时推断慢启动为致命故障。
|
||||
|
||||
首次报告在等待对话框前取得展示权。后续报告保留在日志中。Electron 控制台保留完整的已报告诊断。原生对话框无法滚动,因此仅显示首次诊断末尾八行,并将包含截断提示和重装建议的完整详情限制为 1,200 个 UTF-16 代码单元。对话框提供退出、重启或禁用第三方 bundle 后重启整个应用。恢复操作等待 Host 关闭后,在已有 profile 锁内调用共享 app-boot `sanitizeProfile` 函数。该函数恢复调用方指定的 bundle,并将 profile patch 重命名为唯一备份,无需解析 patch、初始化运行时或删除安装文件。调用方负责 profile 关闭并排除并发写入;当前生产调用方是 Desktop。home 级 patch 保持不变。显式恢复操作失败会单独展示,不计为另一次自动致命报告。
|
||||
首次报告在等待对话框前取得展示权。后续报告保留在日志中。应用日志目录下的崩溃报告文件保留完整的已报告诊断,对话框单独一行标明该文件([致命诊断与崩溃报告](2026-09-22-fatal-diagnostics-and-crash-reports.zh.md))。原生对话框无法滚动,因此仅显示首次诊断末尾八行,并将包含截断提示、报告路径和重装建议的完整详情限制为 1,200 个 UTF-16 代码单元。对话框提供退出、重启或禁用第三方 bundle 后重启整个应用。恢复操作等待 Host 关闭后,在已有 profile 锁内调用共享 app-boot `sanitizeProfile` 函数。该函数恢复调用方指定的 bundle,并将 profile patch 重命名为唯一备份,无需解析 patch、初始化运行时或删除安装文件。调用方负责 profile 关闭并排除并发写入;当前生产调用方是 Desktop。home 级 patch 保持不变。显式恢复操作失败会单独展示,不计为另一次自动致命报告。
|
||||
|
||||
Web 文档保留在原位。宿主回调负责启动失败展示,共享启动页保留加载动画;普通浏览器启动仍显示自身的失败报告。只有主应用框架可以上报 Web 启动失败。后端状态保留在主进程中,原生恢复直接负责禁用全部第三方 bundle。Desktop 不提供 profile 重置或应急恢复文档。后端致命故障必须通过原生恢复操作处理,不在当前进程中重试。
|
||||
|
||||
|
||||
+2
-2
@@ -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/architecture/2026-09-18-chat-navigation-performance.md
|
||||
2026-09-18-chat-navigation-performance.md: d990c0cb9a351ea3b3224ce5feec912c176a5fc9
|
||||
2026-09-18-chat-navigation-performance.zh.md: 1a330d3b6bf6357199eb09ffc1cdb434cda1c1af
|
||||
2026-09-18-chat-navigation-performance.md: d82c68098fc422abb181ed4e5e1ca41189cd0951
|
||||
2026-09-18-chat-navigation-performance.zh.md: 9c2f81a51ce53660aabf2dd1e729d02e8028b7b6
|
||||
@@ -22,6 +22,8 @@ The rail uses ResizeObserver-delivered viewport dimensions and fixed item sizes.
|
||||
|
||||
[ChatReading](../../../../packages/client/ui-chat/src/client/chat/use-chat-reading.ts) owns follow-tail policy, semantic restoration, and sampled reader movement. [ChatNavigation](../../../../packages/client/ui-chat/src/client/chat/use-chat-navigation.ts) owns loaded or unloaded jumps, paging anchors, replacement tasks, and fallback. [useChatScroll](../../../../packages/client/ui-chat/src/client/chat/use-chat-scroll.ts) supplies committed inputs and coordinates these owners. Timer and animation-frame completion invoke the owners directly instead of incrementing React tick state. The [pinned-delivery rule](../../../../packages/client/ui-chat/README.md#scroll-ownership) remains independently necessary: small reader gestures cannot be erased by intervening layout growth.
|
||||
|
||||
Shared nested-follow mechanics and control placement are recorded in the [Chat scroll and footer decision](../bug-fix/2026-09-22-chat-scroll-follow-and-footer-geometry.md); sampled outer reading and historical navigation retain the owners above.
|
||||
|
||||
### One publication per historical jump
|
||||
|
||||
[Session.loadThrough](../../../../packages/api/session-controller/src/client/sessions/session.ts) retains accepted pages and a private paging cursor until its shared target is covered or loading ends. It reverses the page list and flattens it once, then publishes one ordered prepend. Live events continue through their normal publication path. A later page failure publishes the successful prefix once; stream or window replacement discards obsolete buffered pages. Single-page `loadOlder` remains immediate, and repeated jump requests retain the existing lowest-target policy.
|
||||
|
||||
@@ -22,6 +22,8 @@ Status: implemented
|
||||
|
||||
[ChatReading](../../../../packages/client/ui-chat/src/client/chat/use-chat-reading.ts) 负责跟随底部、语义恢复和读者移动采样。[ChatNavigation](../../../../packages/client/ui-chat/src/client/chat/use-chat-navigation.ts) 负责已加载或未加载轮次跳转、分页锚点、任务替换和回退。[useChatScroll](../../../../packages/client/ui-chat/src/client/chat/use-chat-scroll.ts) 提供已提交输入并协调各对象。定时器与动画帧完成时直接调用负责对象,不再递增 React tick 状态。[贴底事件规则](../../../../packages/client/ui-chat/README.zh.md#scroll-ownership) 仍有独立必要性:期间发生的布局增长不能抵消读者的小幅操作。
|
||||
|
||||
共用的嵌套跟随机制与控件定位见 [Chat 滚动与页脚决策](../bug-fix/2026-09-22-chat-scroll-follow-and-footer-geometry.zh.md);外层采样阅读与历史导航仍由上述对象负责。
|
||||
|
||||
### 每次历史跳转统一发布
|
||||
|
||||
[Session.loadThrough](../../../../packages/api/session-controller/src/client/sessions/session.ts) 在共享目标被覆盖或加载结束前,保留已接受的页面和私有分页游标。它只反转一次页面列表并展开一次,然后发布一次有序前插。实时事件继续走正常发布路径。后续页面失败时,已成功取得的部分统一发布一次;流或窗口被替换时丢弃失效的暂存页面。单页 `loadOlder` 仍立即发布,重复跳转请求保留既有的最低目标策略。
|
||||
|
||||
+2
-2
@@ -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/architecture/2026-09-21-conversation-build-groups.md
|
||||
2026-09-21-conversation-build-groups.md: 6591ebea61d8edd06edbaf5f54f2bebd1bd00602
|
||||
2026-09-21-conversation-build-groups.zh.md: 70657ded8e04f0badb987ada965aef1184cfc7b0
|
||||
2026-09-21-conversation-build-groups.md: bccb48b0e40a076bbcb59aaca9e9ef6208e9469d
|
||||
2026-09-21-conversation-build-groups.zh.md: 5d2ec9e684c75b3db3568c4f046129431295f736
|
||||
@@ -12,9 +12,11 @@ A Chat Builder aggregates one target's Nodes and indexes. Hardcoding a Chat proc
|
||||
|
||||
Process groups are orthogonal to Steps: one Assistant Node can contribute reasoning inside a group and a reply outside it; tools after that reply enter a later group. Step-number ranges cannot describe membership. Existing event Definitions interpret one event at a time, whereas grouping needs the Nodes those Definitions have already interpreted.
|
||||
|
||||
Broad benchmark budgets do not establish that grouping updates are local. Keyed notifications, component identity, browser layout, and retained memory are distinct properties; passing one check does not establish the others.
|
||||
|
||||
## Decision
|
||||
|
||||
The grouping foundation provides a separate, Node-input `ConversationGroupDefinition`, its registry and Session-local context, generic Builder input/publication, keyed Group storage, and the two React branches. Production Chat segmentation and PR #4565 presentation are not registered by this foundation; the [business migration proposal](../../proposed/feature/2026-09-20-chat-work-details-migration.md) retains their requirements. The [subsystem reference](../../../../docs/subsystems/conversation.md#group-definitions) and [source types](../../../../packages/client/ui-conversation/src/client/contract/groups.ts) own the current API details.
|
||||
The grouping foundation provides a separate, Node-input `ConversationGroupDefinition`, its registry and Session-local context, generic Builder input/publication, keyed Group storage, and the two React branches. Chat owns its registered segmentation and presentation in the [business-rule reference](../../../../packages/client/ui-chat/src/client/conversation-nodes/README.md). The [subsystem reference](../../../../docs/subsystems/conversation.md#group-definitions) and [source types](../../../../packages/client/ui-conversation/src/client/contract/groups.ts) own the current API details.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
@@ -22,21 +24,20 @@ The grouping foundation provides a separate, Node-input `ConversationGroupDefini
|
||||
|---|---|
|
||||
| Business grouping belongs to a registered Definition | Membership, segmentation, replies, steering, retries, summaries, and cache invalidation stay together. |
|
||||
| Builder and assembler do generic work | Supply inputs, dispatch Definitions, validate references, reuse identities, and publish; never construct a concrete grouping class. |
|
||||
| PR #4565 defines product behavior | Preserve its behavior without copying its invasive implementation. |
|
||||
| PR #4565 supplies the product reference | The Chat business-rule reference owns the current behavior and its agreed adjustments. |
|
||||
| React has two root kinds | The ordered root contains NodeReference and GroupReference, not another layout object model. |
|
||||
| All modes share grouping | Mode is absent from Definition input, keys, and parent selection. |
|
||||
| No buildLayout | ConversationViewDefinition keeps its existing responsibilities. |
|
||||
| Foundation precedes business | No production Chat grouping, summaries, notifications, or footer changes in the foundation PR. |
|
||||
| Business remains target-owned | Chat owns process grouping, summaries, notifications, and footer behavior; generic assembly does not interpret them. |
|
||||
|
||||
### Related decisions
|
||||
|
||||
| Record | Relationship |
|
||||
|---|---|
|
||||
| [Business Node assembly](2026-08-09-client-conversation-node-assembly.md) | Retains event matching, Contexts, Locations, one business Node per Context, and target Builders; grouping adds another input category. |
|
||||
| [Presentation policy and grouping](../../proposed/architecture/2026-09-20-chat-presentation-policy-and-step-groups.md) | Retains Definition ownership, local subscriptions, and mode independence; replaces event folding and Step-range membership as the grouping mechanism. |
|
||||
| [Work-details migration](../../proposed/feature/2026-09-20-chat-work-details-migration.md) | Retains product scope and migration tracking; infrastructure alone does not complete those features. |
|
||||
| [Chat scroll and footer](../bug-fix/2026-09-22-chat-scroll-follow-and-footer-geometry.md) | Owns clipping and independent nested following without changing Group Definition membership. |
|
||||
|
||||
These records retain independent rationale and remain active. Cross-View navigation and `toolCallFocus` are outside this change; no View handles, Label Slots, or resource-navigation architecture is introduced.
|
||||
Grouping preserves the independent Node-assembly decisions. Cross-View navigation and `toolCallFocus` remain separate responsibilities; Group references carry no View handles or resource-navigation policy.
|
||||
|
||||
## Definition input, state, and registration
|
||||
|
||||
@@ -106,7 +107,7 @@ Grouping never replays raw events. Node Definitions handle replay first; groupin
|
||||
| [group-store.ts](../../../../packages/client/ui-conversation/src/client/conversation/group-store.ts) | Atomic reference validation, keyed sources, array reuse, and local publication. |
|
||||
| [chat-snapshot-builder.ts](../../../../packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts) | Record projected Node deltas, target positions, and changed Turn orders; provide indexed readers and defer source notification. No process grouping class. |
|
||||
| [ChatView.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatView.tsx) | Read optional root entries and switch between node/group; fallback to existing Node order. |
|
||||
| [ChatGroupSeat.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx) | Stable group parent, member-only subscription, nested Node seats. |
|
||||
| [ChatGroupSeat.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx) | Stable group parent, group-local subscriptions, nested Node seats. |
|
||||
| [ChatNodeSeat.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx) | Existing Node sources/renderers, groupPart forwarding, distinct part anchors, Store-replacement rebind. |
|
||||
| [slots.ts](../../../../packages/client/ui-chat/src/client/contract/slots.ts) and [apply.ts](../../../../packages/client/ui-chat/src/client/apply.ts) | Existing keyed-hook injection for Group sources; no Slot-engine change. |
|
||||
|
||||
@@ -116,7 +117,9 @@ The root selects `views.grouped('chat')?.entries` through the existing useConver
|
||||
|
||||
React keys derive from reference kind, NodeKey/groupPart or GroupKey, using unambiguous tuples. No separate RenderKey type or renderer dispatch field is needed. Compact, Detailed, and Expanded keep the same group container and member parents; presentation changes do not rerun the Group Definition or select a different root branch.
|
||||
|
||||
The stable Group parent is a `div` with `display: contents`, without its own layout box. CSS inheritance remains available; business styles adapt child/sibling selectors and own measurable body containers. Neither CSS variables nor geometry enter the Definition. Reading-position capture measures member Nodes and retains part-specific anchors; existing Turn navigation resolves the original NodeKey to its first visible part.
|
||||
The stable Group parent is a measurable `div`; its body owns capped inner scrolling and its content box reports growth. CSS inheritance remains available, and business styles adapt child/sibling selectors. Neither CSS variables nor geometry enter the Definition. Reading-position capture measures member Nodes and retains part-specific anchors; existing Turn navigation resolves the original NodeKey to its first visible part.
|
||||
|
||||
The presentation channel maps each stored mode to a stable policy object. Seats and renderers select the fields they consume instead of receiving a mode prop through every renderer. Mode changes do not register Definitions or replay Contexts. Whole-Turn eligibility uses that Turn's loaded lifecycle facts rather than Session-wide pagination completion; the business-rule reference defines partial-history folding. Local group disclosure remains component state, while explicit whole-Turn closing resets participating disclosures through the injected Hook without replacing keys.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -138,6 +141,12 @@ The stable Group parent is a `div` with `display: contents`, without its own lay
|
||||
|
||||
**Remove the group wrapper in Expanded.** Rejected: changing the parent remounts members even when their keys survive.
|
||||
|
||||
**Attach a header to the first member or to a per-Step Context.** A first-member header couples group UI to an arbitrary business row. Per-Step identity cannot distinguish multiple groups in one Step; sharing aggregate Turn data also refreshes unrelated headers or leaves later Step reads stale.
|
||||
|
||||
**Emit multiple Nodes from one event Context or derive child Contexts.** Most event Contexts own one Node; arrays add indirection without solving cross-Node grouping. Parent-derived Contexts require forwarding, replay, and removal lifecycles. A separate framework decision is warranted only if derived entities need those lifecycles independently of grouping.
|
||||
|
||||
**Keep all Markdown mounted or reset renderers by remounting.** Retaining lightweight seats is not a reason to retain every full Markdown body. Full bodies remain disclosure-owned, and explicit resets avoid destroying unrelated renderer state. Importing all of #4565 at once would also mix independent visual changes with grouping costs and obscure which behavior caused a regression.
|
||||
|
||||
## Verification
|
||||
|
||||
- [Group store tests](../../../../packages/client/ui-conversation/tests/conversation-group-store.client.spec.ts) cover atomic reference validation, root/member identity reuse, local publication, and removal without deleting source Nodes.
|
||||
@@ -145,6 +154,15 @@ The stable Group parent is a `div` with `display: contents`, without its own lay
|
||||
- [Node source tests](../../../../packages/client/ui-chat/tests/chat-node-source.client.spec.ts) cover projected grouping inputs, indexed readers, and empty change batches.
|
||||
- [Chat rendering tests](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx) retain component state across modes, rebind replacement Node stores, pass independent parts, and omit unreferenced Nodes. [Viewport tests](../../../../packages/client/ui-chat/tests/chat-viewport.client.spec.ts) cover grouped reading anchors, history prepend, and part-aware Turn navigation.
|
||||
|
||||
Business behavior is verified through keyed-update regressions and recorded Web replay.
|
||||
|
||||
| Evidence | Required observation |
|
||||
|---|---|
|
||||
| Keyed notifications and React updates | Content growth affects the owning Node and affected Group; unchanged historical rows and unrelated Turns receive no extra notifications. Mode changes preserve keys and member parents. |
|
||||
| Recorded Web replay | Current accessible titles, group disclosure, hidden ordinary Context, trigger notices, and footer placement match the committed expected output. |
|
||||
|
||||
[Recorded Web scenarios](../../../../apps/web/tests/steering.e2e.ts) cover online steering, reconnect handoff, and grouped presentation through the shipped Web profile. These behavior checks do not establish a quantified latency or memory improvement.
|
||||
|
||||
## Consequences
|
||||
|
||||
- This is an explicit framework extension: a new input protocol, registry, context, publication phase, and reader. It is not merely another callback.
|
||||
@@ -152,4 +170,5 @@ The stable Group parent is a `div` with `display: contents`, without its own lay
|
||||
- Node order and visibility have one owner: the Builder. Grouping must not independently sort raw events or infer membership again in infrastructure.
|
||||
- Stable mounting does not eliminate layout, paint, or retained-memory costs. No measured latency or optimal-performance claim is made.
|
||||
- Real group splits, merges, first-member changes, and pagination repairs can change identity. The mode-switch guarantee does not prohibit those legitimate changes.
|
||||
- Revealing hidden parts, selection/copy, interruption notices, group disclosure, and outer Turn interaction require business adaptation. Foundation tests exercise mixed references without enabling production groups.
|
||||
- Revealing hidden parts, selection/copy, interruption notices, group disclosure, and outer Turn interaction belong to Chat's business adaptation; generic reference tests alone do not establish those behaviors.
|
||||
- Turn status/duration replaces the old tool/message-count title, so status, accessible names, and empty-process Turns require explicit presentation coverage. Hiding ordinary Context retains non-human waking notices and original Session/Trajectory inspection; footer placement depends on the real Turn end rather than group membership.
|
||||
@@ -12,9 +12,11 @@ Chat Builder 汇总整个目标的节点及索引。在其中固定创建 Chat
|
||||
|
||||
过程组与 Step 正交:同一个 Assistant Node 可以把推理放在组内、回复放在组外,回复后的工具进入后面的组。Step 编号范围无法描述这种成员归属。现有事件 Definition 逐个解释事件,分组则需要这些 Definition 已经解释完成的节点。
|
||||
|
||||
宽松的基准预算不能证明分组更新具有局部性。按键通知、组件身份、浏览器布局与常驻内存是不同的性质,通过其中一项检查不能证明其他性质。
|
||||
|
||||
## Decision
|
||||
|
||||
分组基础机制提供独立的节点输入 `ConversationGroupDefinition`、注册表及 Session 内上下文、Builder 通用输入与发布、按键 Group 存储及两种 React 分支。基础机制不注册真实 Chat 分段及 PR #4565 展示,[业务迁移提案](../../proposed/feature/2026-09-20-chat-work-details-migration.zh.md)保留这些需求。[子系统参考](../../../../docs/subsystems/conversation.zh.md#group-definitions)与[源码类型](../../../../packages/client/ui-conversation/src/client/contract/groups.ts)拥有当前 API 细节。
|
||||
分组基础机制提供独立的节点输入 `ConversationGroupDefinition`、注册表及 Session 内上下文、Builder 通用输入与发布、按键 Group 存储及两种 React 分支。Chat 在[业务规则参考](../../../../packages/client/ui-chat/src/client/conversation-nodes/README.zh.md)中拥有其已注册的分段与展示规则。[子系统参考](../../../../docs/subsystems/conversation.zh.md#group-definitions)与[源码类型](../../../../packages/client/ui-conversation/src/client/contract/groups.ts)拥有当前 API 细节。
|
||||
|
||||
### 职责
|
||||
|
||||
@@ -22,21 +24,20 @@ Chat Builder 汇总整个目标的节点及索引。在其中固定创建 Chat
|
||||
|---|---|
|
||||
| 分组业务属于已注册 Definition | 成员、分段、回复、steering、重试、摘要和缓存失效放在一起。 |
|
||||
| Builder 与 assembler 做统一工作 | 提供输入、调度 Definition、校验引用、复用身份并发布,不创建具体分组类。 |
|
||||
| 产品行为以 PR #4565 为准 | 保留行为,不复制其侵入性实现。 |
|
||||
| PR #4565 提供产品参考 | Chat 业务规则参考拥有当前行为及已确认的调整。 |
|
||||
| React 根循环只有两种 kind | 根顺序包含 NodeReference 和 GroupReference,不引入另一套布局对象模型。 |
|
||||
| 所有模式共用分组 | 模式不进入 Definition 输入、key 或父级选择。 |
|
||||
| 不采用 buildLayout | ConversationViewDefinition 保留现有职责。 |
|
||||
| 先基础、后业务 | 基础 PR 不包含真实 Chat 分组、摘要、通知或页脚变化。 |
|
||||
| 业务属于目标包 | Chat 拥有过程分组、摘要、通知和页脚行为,通用组装不解释这些业务。 |
|
||||
|
||||
### 与已有记录的关系
|
||||
|
||||
| 记录 | 关系 |
|
||||
|---|---|
|
||||
| [业务节点组装](2026-08-09-client-conversation-node-assembly.zh.md) | 保留事件匹配、Context、Location、每个 Context 一个业务 Node 及目标 Builder,分组增加另一种输入类别。 |
|
||||
| [展示策略与过程分组](../../proposed/architecture/2026-09-20-chat-presentation-policy-and-step-groups.zh.md) | 保留 Definition 归属、局部订阅和模式无关要求,以新分组机制替代事件 fold 及 Step 范围成员推断。 |
|
||||
| [工作过程展示迁移](../../proposed/feature/2026-09-20-chat-work-details-migration.zh.md) | 保留产品范围及迁移跟踪,基础机制本身不代表这些功能完成。 |
|
||||
| [Chat 滚动与页脚](../bug-fix/2026-09-22-chat-scroll-follow-and-footer-geometry.zh.md) | 负责裁剪与独立的嵌套跟随,不改变 Group Definition 成员关系。 |
|
||||
|
||||
这些记录仍有独立理由,保留有效状态。跨 View 导航与 `toolCallFocus` 不属于本次改动;不引入 View 句柄、Label Slot 或资源导航架构。
|
||||
分组保留独立的节点组装决策。跨 View 导航与 `toolCallFocus` 仍是独立职责,Group 引用不携带 View 句柄或资源导航策略。
|
||||
|
||||
## Definition 输入、状态与注册
|
||||
|
||||
@@ -106,7 +107,7 @@ GroupStore 按 GroupKey 索引记录和来源,不反复查找数组。等价
|
||||
| [group-store.ts](../../../../packages/client/ui-conversation/src/client/conversation/group-store.ts) | 原子引用校验、按键来源、数组复用及局部发布。 |
|
||||
| [chat-snapshot-builder.ts](../../../../packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts) | 记录投影后节点增量、目标位置及变化轮次顺序,提供索引读取器并推迟来源通知,不持有过程分组类。 |
|
||||
| [ChatView.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatView.tsx) | 读取可选根引用并切换 node/group,无分组时使用既有 Node 顺序。 |
|
||||
| [ChatGroupSeat.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx) | 稳定组父级、仅成员订阅及嵌套 Node 容器。 |
|
||||
| [ChatGroupSeat.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx) | 稳定组父级、组内订阅及嵌套 Node 容器。 |
|
||||
| [ChatNodeSeat.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx) | 既有 Node 来源及渲染器、groupPart 传递、独立部分锚点及 Store 替换时重绑定。 |
|
||||
| [slots.ts](../../../../packages/client/ui-chat/src/client/contract/slots.ts) 与 [apply.ts](../../../../packages/client/ui-chat/src/client/apply.ts) | 使用现有按键钩子注入 Group 来源,不改 Slot 引擎。 |
|
||||
|
||||
@@ -116,7 +117,9 @@ GroupStore 按 GroupKey 索引记录和来源,不反复查找数组。等价
|
||||
|
||||
React key 由引用 kind、NodeKey/groupPart 或 GroupKey 的无歧义元组派生,不另设 RenderKey 类型或 renderer 分发字段。Compact、Detailed、Expanded 保留同一组容器及成员父级,展示变化不重新运行 Group Definition,也不选择不同的根分支。
|
||||
|
||||
稳定 Group 父级使用 `div` 与 `display: contents`,自身不产生布局盒子。CSS 继承仍然可用,业务样式适配子级/兄弟选择器并拥有可测量的正文容器。CSS 变量和几何信息都不进入 Definition。阅读位置采样测量成员 Node,保留部分专属锚点;既有轮次导航将原 NodeKey 解析到它的第一个可见部分。
|
||||
稳定 Group 父级使用可测量的 `div`,正文拥有组内限高滚动,内容盒子报告尺寸增长。CSS 继承仍然可用,业务样式适配子级/兄弟选择器。CSS 变量和几何信息都不进入 Definition。阅读位置采样测量成员 Node,保留部分专属锚点;既有轮次导航将原 NodeKey 解析到它的第一个可见部分。
|
||||
|
||||
展示通道将每个存储模式映射为稳定策略对象。Seat 和 renderer 选择自己使用的字段,不通过所有 renderer 透传模式 prop。模式变化不注册 Definition,也不重放 Context。整轮折叠资格使用本轮已加载的生命周期事实,不依赖整个 Session 的分页完成状态;半截历史的折叠规则由业务参考定义。组开合保留在组件本地状态中,显式收起整轮通过注入 Hook 重置参与折叠的内部开合,不更换 key。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -138,6 +141,12 @@ React key 由引用 kind、NodeKey/groupPart 或 GroupKey 的无歧义元组派
|
||||
|
||||
**Expanded 移除组容器。** 不采用,即使 key 不变,父级改变仍会重挂载成员。
|
||||
|
||||
**让首成员或每 Step 的 Context 承载组头。** 首成员组头把组 UI 耦合到任意业务行。每 Step 的身份无法区分同一步内的多个组,共享整轮聚合数据还会刷新无关组头,或让后续 Step 的读取过期。
|
||||
|
||||
**一个事件 Context 输出多个 Node,或派生子 Context。** 多数事件 Context 只拥有一个 Node,数组增加间接层却不能解决跨 Node 分组。父级派生 Context 需要转发、重放及移除生命周期,只有派生实体需要独立于分组的这些生命周期时,才值得另作框架决策。
|
||||
|
||||
**常驻全部 Markdown,或靠重挂载重置 renderer。** 保留轻量 Seat 不意味着需要常驻所有完整 Markdown 正文。完整正文仍由开合状态控制,显式重置避免销毁无关 renderer 状态。一次性引入 #4565 的全部改动还会混合独立视觉变化与分组成本,难以判断导致回退的具体行为。
|
||||
|
||||
## Verification
|
||||
|
||||
- [Group 存储测试](../../../../packages/client/ui-conversation/tests/conversation-group-store.client.spec.ts)覆盖原子引用校验、根与成员身份复用、局部发布,以及删除组但保留原 Node。
|
||||
@@ -145,6 +154,15 @@ React key 由引用 kind、NodeKey/groupPart 或 GroupKey 的无歧义元组派
|
||||
- [Node 来源测试](../../../../packages/client/ui-chat/tests/chat-node-source.client.spec.ts)覆盖投影后的分组输入、索引读取器和空变化批次。
|
||||
- [Chat 渲染测试](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx)保留模式切换时的组件状态,重绑定替换后的 Node 存储,传递独立部分,并省略未引用 Node。[视口测试](../../../../packages/client/ui-chat/tests/chat-viewport.client.spec.ts)覆盖组内阅读锚点、历史前插及部分感知的轮次导航。
|
||||
|
||||
业务行为通过按键更新回归与已录制的 Web 回放验证。
|
||||
|
||||
| 证据 | 必须观察到的结果 |
|
||||
|---|---|
|
||||
| 按键通知与 React 更新 | 正文增长影响所属 Node 和相关 Group;未变化历史行及其他 Turn 没有额外通知。模式变化保留 key 与成员父级。 |
|
||||
| 已录制的 Web 回放 | 当前可访问标题、组开合、普通 Context 隐藏、触发通知及页脚位置与提交的预期输出一致。 |
|
||||
|
||||
[已录制的 Web 场景](../../../../apps/web/tests/steering.e2e.ts)通过正式 Web profile 覆盖在线 steering、重连接续与分组展示。这些行为检查不构成量化延迟或内存改善的证明。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 这是明确的框架扩展,包含新输入协议、注册表、上下文、发布阶段及读取器,不只是增加一个回调。
|
||||
@@ -152,4 +170,5 @@ React key 由引用 kind、NodeKey/groupPart 或 GroupKey 的无歧义元组派
|
||||
- Node 顺序及可见性只有 Builder 一个所有者,分组不独立排序原始事件,也不在通用层再次推断成员。
|
||||
- 稳定挂载不消除布局、绘制或保留内存的成本,不宣称实测延迟或最优性能。
|
||||
- 真实组拆并、首成员变化及分页修正可以改变身份,模式切换保证不禁止这些正常变化。
|
||||
- 隐藏部分的揭示、选择复制、中断提示、组开合与外层 Turn 联动需要业务适配。基础测试使用混合引用,不启用真实分组。
|
||||
- 隐藏部分的揭示、选择复制、中断提示、组开合与外层 Turn 联动属于 Chat 业务适配,仅凭通用引用测试不能确认这些行为。
|
||||
- 整轮状态与用时替代旧工具/消息计数标题,因此状态、可访问名称和空过程轮次需要明确的展示覆盖。隐藏普通 Context 时保留非人工唤醒通知及原始 Session/Trajectory 查看能力;页脚定位依赖真实 Turn 结束,不依赖分组成员。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/architecture/2026-09-22-fatal-diagnostics-and-crash-reports.md
|
||||
2026-09-22-fatal-diagnostics-and-crash-reports.md: 01cbb24fc84b19465a15e9f82c03ae87621b5fe9
|
||||
2026-09-22-fatal-diagnostics-and-crash-reports.zh.md: 38f6a6802b61b128ab6b8ffda0465691c5244523
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Agent Note: Fatal diagnostics and Desktop crash reports
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-22-fatal-diagnostics-and-crash-reports.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Two field failures of the Desktop application were diagnosed from screenshots of the fatal recovery dialog, which shows the last eight lines of the error and used to promise that the full diagnostic was in the Electron console. A packaged installation never surfaces that console.
|
||||
|
||||
On Windows, `OutputCollector.spillAll` in `dsh-subprocess-local` opened its spill file inside the child's stdout `'data'` listener. The `openSync` with `'wx'` threw `ENOENT`, which for that flag means the private per-process spill directory was gone (it is created once and never recreated; what removed it was not observed), and because no process-level handler existed the Desktop Host died with Node's default uncaught-exception output. The `path:` property happened to land in the eight visible lines; nothing else about the failure was recoverable.
|
||||
|
||||
On macOS, the boot audit listed a set of Web client rows as `import failed (see console for the import error)` while the real import error stayed in the renderer console. The eight visible names match exactly the rows that depend on one application batch, so a single failed load of that batch script explains the screenshot; whether the failure was a transport error, a stale-URL 404, or a truncated response was not determinable. A batch was requested exactly once, so any transient failure was final.
|
||||
|
||||
Both cases shared three gaps: fatal diagnostics were not persisted, the Host had no `uncaughtException` policy, and a single fallible step was allowed to fail a whole feature.
|
||||
|
||||
## Decision
|
||||
|
||||
Four decisions ship together.
|
||||
|
||||
**Uncaught exceptions are fatal and reported; the failed operation is never resumed.** `installFailLoud` in `dsh-app-boot` registers the same handler for `'uncaughtException'` as for `'unhandledRejection'`: write one labelled `util.inspect` diagnostic to stderr, await the surface's release hook under the existing timeout, exit 1. Control never returns to the failed operation and the event loop runs only until the release settles or times out, because only the throw site knows which state is intact; a `'data'` listener that threw mid-update has already dropped a chunk and half-set its own fields, and resuming would turn a visible crash into silently wrong output. `util.inspect` replaces `err.stack` because a `node:fs` error's `code`, `syscall`, and `path`, and any `cause` chain, are enumerable properties the stack line omits. The rejection label `fatal load failure` is unchanged because the Web profile expected-output e2e tests match it; exceptions use `fatal uncaught exception`.
|
||||
|
||||
**Spill files are best-effort at every step.** `spillPath` was already optional and a failed final close already withheld it. `spillAll` now contains every filesystem failure while opening or appending: it discards the spill, reports once through the owner's logger (`SpillOptions.onFailure`, wired to `ctx.logger.error` by the runtime and the SSH helper), and leaves the in-memory tail collecting. The removed directory is not recreated: the security of the design rests on a random name created once by this process, and recreating a known name would give that up. A fresh random directory on `ENOENT` is deferred work.
|
||||
|
||||
**A fatal Desktop failure writes a crash report before its dialog.** Every `reportFatal` caller names its source (`host`, `web-boot`, `renderer`, `main`). `DesktopFatalRecovery.report` awaits `writeCrashReport` for at most one second, then shows the dialog with the file path on its own line, independent of whether the error was shortened and in the listener-conflict variant too. The report holds the source, whether the backend had reached ready, application and runtime versions, the complete inspected error (the backend state carries the original failure object, and a Host startup failure carries the Host's own inspected error over the fatal IPC event, because the Host's stderr and that message race and the shell reports the first failure it sees), and the primary window's recent error-level console output captured from `webContents` `'console-message'` (a 64 KiB tail). Reports live under `app.getPath('logs')`, are owner-only where the platform allows, and startup keeps the ten newest. A fatal failure during shutdown writes a report without a dialog. Buttons and recovery actions are unchanged.
|
||||
|
||||
**A failed batch script is a per-row problem, not a boot failure.** In `ClientModuleSystem.arrive`, a transport failure (the script `error` event; nothing executed) is retried once on the same URL, shared by every row waiting on that batch. A script that loaded without registering a row is never re-executed: the batch registers packages in sequence and `register()` rejects duplicates, so a replay would stop at the first package the original run did register. Every executed batch URL is remembered, so this holds even when the first importer from the batch was a row it did register and a later row finds itself missing. Either way each still-missing row then loads its own one-resource combo URL, which the Host serves for every package; a failed batch URL is remembered so later rows skip to their fallback, while a one-resource URL stays retryable across imports (within one boot the Loader imports each entry once, so a missing row gets one fallback attempt). Dependency failures are wrapped with the consumer and dependency names. The module system records the last `import()` or `prefetch()` failure per row at the outer operation, so factory execution errors are captured as well as arrival errors, and `assertEntriesActive` reports that text per fiberless entry.
|
||||
|
||||
## Relationship to earlier decisions
|
||||
|
||||
This partially supersedes the [native Desktop fatal recovery](2026-09-15-desktop-native-fatal-recovery.md) note: the crash report file, not the Electron console, is now the record the dialog points to, and the dialog detail carries the report-path line; that note's dialog ownership, button set, and profile recovery remain active. The `uncaughtException` handling extends the release mechanism of the [fail-loud release](../bug-fix/2026-07-31-fail-loud-releases-the-terminal.md) note to synchronous throws.
|
||||
|
||||
## Relationship to the Desktop logging design
|
||||
|
||||
The Desktop local logging design (a Worker-owned rolling JSONL log with a closed event vocabulary, redaction, and rate limits) is a continuous-log design and remains future work. It stated that Host stderr and renderer console output are never persisted and that errors reach the log only as fixed categories. This decision carves out one exception: the one-time crash report of a fatal failure keeps the raw error and the retained stderr and console tails, because that raw content is what both field diagnoses needed and the report is bounded (the error section is cut at 256 KiB, the Host diagnostic at 64 KiB, the renderer console tail at 64 KiB), local, owner-only, and written only when the application is already showing its fatal dialog. The dialog names the file so a user knows what they would be sharing. The continuous-log privacy posture is unchanged for anything that is not a crash report.
|
||||
|
||||
## Testing
|
||||
|
||||
- `dsh-app-boot`: uncaught exceptions produce the labelled diagnostic with enumerable properties and the cause chain, share the exit latch and release timeout with rejections, and both handlers uninstall.
|
||||
- `dsh-subprocess-local`: `ENOENT` on open (removed directory), `ENOTDIR` on open, and `ENOSPC` on append after the file exists each leave the tail byte-exact, withdraw the spill file, report once, and never throw.
|
||||
- `dsh-client-modules`: a transport failure retries once and shares the retry; a persistent failure falls back per missing row without re-fetching the batch per row; a batch that registered nothing or only some rows is not re-executed; a one-resource URL stays retryable; the final error lists every attempt; import errors are recorded for transport, factory, and dependency failures and cleared by success and invalidation.
|
||||
- `dsh-client-web`: the boot audit names the recorded import error when the module system is supplied.
|
||||
- Desktop: `crash-report` unit tests cover rendering, owner-only writing, write failure, pruning that leaves other files alone, and the console tail; `fatal-recovery` tests pin the dialog text with and without a report path in both locales, the one-second bound, and a single write per fatal; the startup harness asserts the report input for a running-phase Host exit including the captured renderer console.
|
||||
|
||||
## Consequences
|
||||
|
||||
The Host now exits through one path for both failure kinds, up to the release timeout later than Node's immediate default exit, so a fatal dialog can appear up to two seconds after the failure, and plugin stderr written during that release window can push the fail-loud line out of the dialog's eight-line excerpt (the report file keeps the 64 KiB tail). Spill failures are no longer visible to the user beyond a truncated result with no path; the plugin logger line is the only trace, which the Host-side continuous log design would persist. Crash reports persist raw stderr and console tails, accepted for the reasons above. A batch load failure now costs up to three requests per missing row and never fails registered rows; the shared-batch memory means a later import of a row from a failed batch goes straight to its one-resource URL for the life of the page.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Catch the uncaught exception and keep the Host running.** Rejected: the state after a listener throws is unknown to anything but the throw site. In the Windows case, continuing would have dropped the chunk, advertised a spill path to a file that did not exist, and thrown again on every following chunk.
|
||||
|
||||
**Recreate the spill directory on `ENOENT`.** Rejected for this change: recreating a known name gives up the random-directory property the design exists for. A fresh `mkdtemp` on failure needs the collector to hold a directory provider rather than a string and is deferred.
|
||||
|
||||
**Write the crash report synchronously so the dialog stays synchronous.** Rejected: a hung write on a slow or network-mounted profile directory would freeze the shell with no dialog at all. The asynchronous write with a one-second bound shows the dialog either way.
|
||||
|
||||
**Persist only classified error categories in the crash report, as the continuous-log design prescribes.** Rejected for crash reports: the `path:` property and the renderer's `bundle script … failed to load` line were the load-bearing evidence in both diagnoses and would have been reduced to `UNCLASSIFIED`.
|
||||
|
||||
**Retry the batch URL for every failure kind.** Rejected: re-executing a batch that already registered some packages throws `duplicate factory registration` inside the script, the `load` event still fires, and the caller sees the same "loaded without registering" it started with, unable to tell that the retry caused it.
|
||||
|
||||
**Keep more generations of Host batch URLs so a recomposed graph does not 404 an in-flight boot.** Deferred rather than shipped: no production trigger for a post-ready graph change was found, `lazyBody` caches response buffers so retention has a real memory cost, and the generation and age limits are deployment tunables that need `Config` fields.
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Agent Note: 致命诊断与 Desktop 崩溃报告
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-22-fatal-diagnostics-and-crash-reports.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
Desktop 应用的两起用户侧故障都是靠致命恢复对话框的截图定位的。该对话框只显示错误的最后八行,并且当时声称完整诊断已写入 Electron 控制台,而打包安装的应用从不暴露那个控制台。
|
||||
|
||||
Windows 上,`dsh-subprocess-local` 的 `OutputCollector.spillAll` 在子进程 stdout 的 `'data'` 监听器内打开 spill 文件。带 `'wx'` 的 `openSync` 抛出 `ENOENT`,对该标志而言这意味着每进程私有 spill 目录已不存在(它只创建一次、从不重建;是什么删除了它没有被观测到),由于没有任何进程级 handler,Desktop Host 以 Node 默认的未捕获异常输出退出。`path:` 属性恰好落在可见的八行里;此外关于该故障的信息一无所获。
|
||||
|
||||
macOS 上,启动审计把一组 Web 客户端行列为 `import failed (see console for the import error)`,而真正的 import 错误只留在渲染进程 console 中。可见的八个名字与依赖同一个 application batch 的行完全吻合,因此该 batch 脚本的一次加载失败可以解释截图;失败是传输错误、过期 URL 的 404 还是响应截断,无法判定。batch 只请求一次,因此任何瞬态失败都是最终结果。
|
||||
|
||||
两起事故共享三个缺口:致命诊断没有持久化、Host 没有 `uncaughtException` 策略、一个可失败步骤被允许拖垮整个功能。
|
||||
|
||||
## Decision
|
||||
|
||||
四条决定一并交付。
|
||||
|
||||
**未捕获异常是致命的并会被报告;失败的操作绝不恢复。** `dsh-app-boot` 的 `installFailLoud` 为 `'uncaughtException'` 注册与 `'unhandledRejection'` 相同的 handler:向 stderr 写一条带标签的 `util.inspect` 诊断,在既有超时内等待界面的 release 钩子,以 1 退出。控制流不会回到失败的操作,事件循环只运行到 release 结束或超时,因为只有抛出点知道哪些状态仍然完整;在更新过程中抛出的 `'data'` 监听器已经丢掉了一块数据并只设置了一半自己的字段,恢复运行会把可见的崩溃变成静默错误的输出。用 `util.inspect` 替代 `err.stack`,因为 `node:fs` 错误的 `code`、`syscall`、`path` 以及 `cause` 链都是堆栈行省略的可枚举属性。rejection 的标签 `fatal load failure` 保持不变,因为 Web profile 的 expected-output e2e 测试匹配它;异常使用 `fatal uncaught exception`。
|
||||
|
||||
**spill 文件在每一步都是尽力而为。** `spillPath` 本来就是可选的,最终关闭失败时已经会撤回它。`spillAll` 现在包住打开或追加过程中的每一种文件系统失败:丢弃该 spill,通过所有者的 logger 报告一次(`SpillOptions.onFailure`,由运行时和 SSH helper 接到 `ctx.logger.error`),并让内存尾部继续收集。被删除的目录不会重建:该设计的安全性依赖于由本进程一次性创建的随机名称,重建一个已知名称就放弃了这一点。在 `ENOENT` 时新建随机目录是待办工作。
|
||||
|
||||
**Desktop 致命失败在弹窗前写入崩溃报告。** 每个 `reportFatal` 调用方都标明来源(`host`、`web-boot`、`renderer`、`main`)。`DesktopFatalRecovery.report` 最多等待 `writeCrashReport` 一秒,然后弹出对话框并把文件路径单独占一行——与错误是否被截短无关,端口占用变体同样如此。报告包含来源、后端是否已就绪、应用与运行时版本、完整的 inspect 错误(backend 状态携带原始错误对象;Host 启动失败通过 fatal IPC 事件携带 Host 自己的 inspect 错误,因为 Host 的 stderr 与该消息存在竞争,而壳只报告它看到的第一个失败),以及从 `webContents` 的 `'console-message'` 捕获的主窗口最近 error 级 console 输出(64 KiB 尾部)。报告位于 `app.getPath('logs')` 下,平台允许时仅所有者可读,启动时保留最新十份。关闭过程中的致命失败只写报告、不弹窗。按钮与恢复操作不变。
|
||||
|
||||
**batch 脚本失败是按行的问题,不是启动失败。** 在 `ClientModuleSystem.arrive` 中,传输失败(脚本 `error` 事件;什么都没执行)在同一 URL 上重试一次,等待该 batch 的所有行共享这次重试。加载成功但没有注册某行的脚本绝不重新执行:batch 按顺序注册各包,`register()` 拒绝重复,重放会在原次运行已注册的第一个包处停止。每个已执行的 batch URL 都会被记住,因此即使首个从该 batch 导入的是它已注册的行、之后另一行发现自己缺失,这一点仍然成立。两种情况下每条仍缺失的行随后加载自己的单资源 combo URL,Host 为每个包都提供该 URL;失败的 batch URL 会被记住,后续行直接走回退,而单资源 URL 跨 import 保持可重试(一次启动内 Loader 对每个条目只 import 一次,因此缺失行只有一次回退尝试)。依赖失败用消费者与依赖的名字包装。模块系统在最外层操作按行记录最后一次 `import()` 或 `prefetch()` 失败,因此 factory 执行错误与到达错误一样被捕获,`assertEntriesActive` 按无 fiber 的条目报告该文本。
|
||||
|
||||
## 与既有决定的关系
|
||||
|
||||
本 note 部分取代 [Desktop 原生致命恢复](2026-09-15-desktop-native-fatal-recovery.zh.md):对话框指向的记录现在是崩溃报告文件而非 Electron 控制台,对话框详情多出报告路径一行;该 note 对对话框归属、按钮集合与 profile 恢复的决定仍然有效。`uncaughtException` 的处理把 [fail-loud release](../bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md) note 的 release 机制扩展到同步抛出。
|
||||
|
||||
## 与 Desktop 日志体系设计的关系
|
||||
|
||||
Desktop 本地日志设计(Worker 独占写入的滚动 JSONL 日志、封闭事件词汇、脱敏与限流)是持续日志设计,仍是未来工作。它规定 Host stderr 与渲染进程 console 输出永不持久化、错误只以固定分类进入日志。本决定划出一个例外:致命失败的一次性崩溃报告保留原始错误以及保留的 stderr 与 console 尾部,因为两次现场定位需要的正是这些原始内容,而报告有界(error 段截至 256 KiB、Host 诊断 64 KiB、渲染进程 console 尾部 64 KiB)、本地、仅所有者可读,且只在应用已经弹出致命对话框时写入。对话框标明文件名,用户知道自己会分享什么。任何非崩溃报告的内容仍遵循持续日志的隐私姿态。
|
||||
|
||||
## Testing
|
||||
|
||||
- `dsh-app-boot`:未捕获异常产生带可枚举属性与 cause 链的带标签诊断,与 rejection 共享退出闩锁和 release 超时,两个 handler 都能卸载。
|
||||
- `dsh-subprocess-local`:打开时 `ENOENT`(目录被删)、打开时 `ENOTDIR`、文件已存在后追加时 `ENOSPC`,三者都保持尾部逐字节一致、撤回 spill 文件、报告一次且不抛出。
|
||||
- `dsh-client-modules`:传输失败重试一次并共享重试;持续失败按缺失行回退且不重复请求 batch;零注册或部分注册的 batch 不重新执行;单资源 URL 保持可重试;最终错误列出每次尝试;传输、factory 与依赖失败都被记录,成功和失效会清除记录。
|
||||
- `dsh-client-web`:提供模块系统时启动审计写出记录的 import 错误。
|
||||
- Desktop:`crash-report` 单测覆盖渲染、仅所有者可读的写入、写入失败、不触碰其他文件的清理以及 console 尾部;`fatal-recovery` 测试在两种语言下钉住带与不带报告路径的对话框文本、一秒上限以及每次致命失败只写一次;启动夹具断言运行阶段 Host 退出的报告输入,包括捕获的渲染进程 console。
|
||||
|
||||
## Consequences
|
||||
|
||||
Host 现在对两种失败走同一条退出路径,比 Node 立即退出的默认行为最多晚 release 超时的时间,因此致命对话框可能在失败后最多两秒才出现,且 release 窗口内插件写到 stderr 的内容可能把 fail-loud 那一行挤出对话框的八行摘录(报告文件保留 64 KiB 尾部)。spill 失败对用户不再可见,只剩一个带截断标记且没有路径的结果;插件 logger 那一行是唯一痕迹,Host 侧持续日志设计将来会持久化它。崩溃报告持久化原始 stderr 与 console 尾部,理由如上。batch 加载失败现在对每条缺失行最多花费三次请求,且绝不让已注册的行失败;共享的 batch 记忆意味着页面生命周期内,失败 batch 中某行的后续 import 会直接走其单资源 URL。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**捕获未捕获异常并让 Host 继续运行。** 否决:监听器抛出后的状态除抛出点外无人知晓。在 Windows 案例中,继续运行会丢掉该块数据、向模型宣告一个不存在文件的 spill 路径,并在之后每一块上再次抛出。
|
||||
|
||||
**在 `ENOENT` 时重建 spill 目录。** 本次否决:重建已知名称放弃了该设计赖以存在的随机目录属性。失败时新建 `mkdtemp` 需要收集器持有目录提供函数而非字符串,作为待办。
|
||||
|
||||
**同步写崩溃报告以保持对话框同步。** 否决:在缓慢或网络挂载的 profile 目录上,一次挂起的写入会让壳冻结且完全没有对话框。带一秒上限的异步写入无论如何都会弹出对话框。
|
||||
|
||||
**按持续日志设计的规定,崩溃报告只持久化分类后的错误类别。** 对崩溃报告否决:`path:` 属性和渲染进程的 `bundle script … failed to load` 那一行是两次定位的关键证据,按该规则会被抹成 `UNCLASSIFIED`。
|
||||
|
||||
**对每种失败都重试 batch URL。** 否决:重新执行一个已注册部分包的 batch 会在脚本内抛出 `duplicate factory registration`,`load` 事件照常触发,调用方看到的还是最初那个「loaded without registering」,无法分辨是重试造成的。
|
||||
|
||||
**保留更多代 Host batch URL,使重组的 graph 不会让进行中的启动收到 404。** 推迟而非交付:没有找到 ready 之后改动 graph 的生产触发者,`lazyBody` 缓存响应 Buffer 使保留有实际内存成本,代数与时限是需要 `Config` 字段的部署可调项。
|
||||
+2
-2
@@ -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-fail-loud-releases-the-terminal.md
|
||||
2026-07-31-fail-loud-releases-the-terminal.md: a6b8ccc5a0ad885a349b32707face7bfce9d7bed
|
||||
2026-07-31-fail-loud-releases-the-terminal.zh.md: 728fca424d2622af6222d51a3240eafd0e117986
|
||||
2026-07-31-fail-loud-releases-the-terminal.md: 9c8aa41e960764c6d9311c49a8757a25b3996b78
|
||||
2026-07-31-fail-loud-releases-the-terminal.zh.md: e44d4672dcf768c33d94b801809cf7aa653aa62f
|
||||
@@ -29,6 +29,7 @@ The `/exit` path was never affected, because it disposes the tree and reaches th
|
||||
- A latch, not an uninstall, keeps the first rejection the reported one. Removing the listener during teardown would let a second concurrent rejection become uncaught, and Node would kill the process mid-teardown — stranding exactly the terminal state this restores. Later rejections, including the release's own, fall through to the pending exit.
|
||||
- The release is bounded by `FAIL_LOUD_RELEASE_TIMEOUT_MS` (2s) and its rejection is swallowed. A wedged or failing disposer delays the fatal exit; it never cancels it. That timer stays **referenced**: an `unref()`ed one lets Node reach an empty event loop and exit 0 on the very failure being reported, because an `unhandledRejection` listener suppresses the default fatal exit.
|
||||
- Omitting `release` keeps the previous behavior exactly, so the ACP, JSON-RPC, and demo bins are unchanged.
|
||||
- The same handler, latch, and release run for `uncaughtException` since the [fatal diagnostics and crash reports](../architecture/2026-09-22-fatal-diagnostics-and-crash-reports.md) decision, which also switched the diagnostic to `util.inspect`.
|
||||
|
||||
`dsh`'s TUI launcher passes a release that disposes the root context, which runs the TUI's existing `shutdown()` and hands the terminal back.
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ Loader 并发挂载各个条目,因此条目失败的顺序并不等于启动
|
||||
- 使用闩锁(latch)而非卸载监听器,来保证被报告的始终是第一个 rejection。若在拆卸期间移除监听器,第二个并发 rejection 就会变成未捕获错误,Node 会在拆卸中途杀死进程——恰好残留下本次要恢复的终端状态。后续 rejection(包括 release 自身的)都会落入已挂起的退出流程。
|
||||
- release 以 `FAIL_LOUD_RELEASE_TIMEOUT_MS`(2 秒)为上限,且其 rejection 被吞掉。卡住或失败的 disposer 只会延迟致命退出,绝不会取消它。该定时器保持 **referenced**:一旦 `unref()`,Node 就会在事件循环清空后、恰恰在报告这次失败时以 0 退出,因为 `unhandledRejection` 监听器抑制了默认的致命退出。
|
||||
- 不传 `release` 时行为与此前完全一致,因此 ACP(Agent Client Protocol)、JSON-RPC 和各 demo bin 均无变化。
|
||||
- 自[致命诊断与崩溃报告](../architecture/2026-09-22-fatal-diagnostics-and-crash-reports.zh.md)决定起,同一 handler、闩锁与 release 也对 `uncaughtException` 生效,诊断同时改为 `util.inspect`。
|
||||
|
||||
`dsh` 的 TUI 启动器传入的 release 会释放根上下文,从而执行 TUI 已有的 `shutdown()` 并把终端交还。
|
||||
|
||||
|
||||
@@ -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-17-durable-web-queue-recovery.md
|
||||
2026-08-17-durable-web-queue-recovery.md: 5fbfdf0e62c1034fbea138016cc2979489de474f
|
||||
2026-08-17-durable-web-queue-recovery.zh.md: 2aaffd6bc571aab449d9429758fdf45bb4dd8d4c
|
||||
2026-08-17-durable-web-queue-recovery.md: 0f18cd7a2b9491d6a8bdfa960409d97a5338f95d
|
||||
2026-08-17-durable-web-queue-recovery.zh.md: 476c1b2fe88156ba8c34e281ff1d1e5663c7913c
|
||||
@@ -49,3 +49,5 @@ Client coverage pins generic Inbox projection delivery, reconnect invalidation f
|
||||
Pending Queue and steering input recover after Host process restart without resuming an Agent. Live Inbox reads, cold history, reconnect, and projection caching use the same domain-owned fold and registry state. Operations on a restored row do resume its ordinary Agent, preserving preset composition and ownership checks.
|
||||
|
||||
Clients receive the raw two-list Inbox value and decide which messages their surface presents. The projection state version invalidates cached rows whenever its serialized state or fold semantics change.
|
||||
|
||||
The [input echo admission decision](2026-09-22-input-echo-admission-ownership.md) owns local Chat/Dock handoff and suppression of delayed Inbox rows without replacing this recovery model.
|
||||
@@ -49,3 +49,5 @@ Host 投影覆盖会读取包含待处理输入的已分离持久 Session,在
|
||||
待处理 Queue 与 steering 输入可在 Host 进程重启后恢复,而无需恢复 Agent。live Inbox 读取、冷历史、重连与投影缓存使用同一份领域拥有的折叠与注册表状态。操作恢复出的行时会恢复其普通 Agent,从而保留 preset 组合与所有权检查。
|
||||
|
||||
客户端接收原始的两列表 Inbox 值,并自行决定界面呈现哪些消息。投影的状态版本会在其序列化状态或折叠语义变化时使缓存行失效。
|
||||
|
||||
[输入回显入档决策](2026-09-22-input-echo-admission-ownership.zh.md)负责本地 Chat/Dock 接管和迟到 Inbox 行的排除,不替换这里的恢复模型。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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-09-21-multimodal-tool-result-retention.md
|
||||
2026-09-21-multimodal-tool-result-retention.md: 41470724911d3086878d206f82c76ad9a4993146
|
||||
2026-09-21-multimodal-tool-result-retention.zh.md: 8e6d4953f7380d45235ac13f9f245e2e005f1abf
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Multimodal tool-result retention
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-21-multimodal-tool-result-retention.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
MCP prepares durable images asynchronously while its pure renderer emits text placeholders. Installing prepared content after post-execute policy makes text retention invalidate image replacement. A byte-only limit also cannot compare text with visual input costs.
|
||||
|
||||
## Decision
|
||||
|
||||
`ToolDefinition.projectContent` installs execution-prepared content before post-execute policy. The final callback retains its separate role for terminal and job limits. Policy blocks and replacements remain authoritative.
|
||||
|
||||
The spill policy prices ordered text/image blocks in estimated tokens, reusing the text estimator and the active route image calculator, including descriptor text. It reserves omission notices, divides remaining tokens between contiguous ends, and omits the middle. Text can split between code points; images remain indivisible at their original positions.
|
||||
|
||||
The complete spill file contains accepted text and readable attachment paths in order. Attachments own image bytes. Missing image pricing or a readable recovery path preserves the original result with a diagnostic. PTC program values remain complete; forwarding and logs retain a recovery notice even when every image is omitted.
|
||||
|
||||
The [spill storage decision](../architecture/2026-07-08-tool-output-spill-files.md) still owns backend separation and secure local files. The [MCP client decision](../feature/2026-07-07-mcp-client-plugin.md) still owns canonical values and image admission.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Restore images after policy edits.** This can resurrect blocked images or discarded text. Preparing content before policy lets transformations inspect the actual images.
|
||||
|
||||
**Keep images outside the budget or charge a fixed maximum.** Images must share the result budget with text. The existing route calculator already prices projected dimensions and descriptor text, so it owns that cost.
|
||||
|
||||
**Embed image bytes in spill text.** Base64 inflates the artifact and duplicates attachment storage. Readable attachment paths preserve retrieval and ordering without copying bytes.
|
||||
|
||||
## Consequences
|
||||
|
||||
The shipped result budget is 12,500 estimated tokens, replacing the former 50,000-byte setting. Text estimation remains heuristic; provider usage is authoritative. Unused space at an image boundary stays unused to preserve contiguous ends. Other block types pass through.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 工具结果的图文保留
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-21-multimodal-tool-result-retention.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
MCP 异步准备持久化图片,而纯渲染器输出文字占位。在执行后策略完成后才安装已准备内容,会让文字保留使图片替换失效。按字节限制也无法比较文字与视觉输入的成本。
|
||||
|
||||
## Decision
|
||||
|
||||
`ToolDefinition.projectContent` 在执行后策略之前安装执行期间准备的内容。最终回调保留其对终端和任务结果设定上限的独立职责。策略阻止和替换仍然生效。
|
||||
|
||||
spill 策略以估算 token 对有序图文块计量,复用文字估算和当前模型图片计算器,并计入说明文字。它预留省略提示,将剩余 token 平分给连续两端,省略中间。文字可在码点之间切分;图片不可切分,并保持原位置。
|
||||
|
||||
完整结果文件按顺序保存已接受文字和可读取的附件路径。附件拥有图片字节。缺少图片计量或可读取的恢复路径时,保留原结果并记录诊断。PTC 程序返回值保持完整;全部图片被省略时,转发内容和日志仍保留读取提示。
|
||||
|
||||
[spill 存储决策](../architecture/2026-07-08-tool-output-spill-files.zh.md)继续负责后端拆分和安全的本地文件。[MCP 客户端决策](../feature/2026-07-07-mcp-client-plugin.zh.md)继续负责规范值和图片准入。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在策略修改后恢复图片。** 这可能恢复已阻止图片或已丢弃文字。在策略之前准备内容,让转换能够检查真实图片。
|
||||
|
||||
**图片不计入预算或按固定上限计费。** 图片必须与文字共享结果预算。现有模型计算器已经衡量请求尺寸和说明文字,因此由它负责成本。
|
||||
|
||||
**把图片字节嵌入 spill 文本。** Base64 会增大产物并重复附件存储。可读取的附件路径在不复制字节的情况下保留读取方式和顺序。
|
||||
|
||||
## Consequences
|
||||
|
||||
随产品提供的结果预算为 12,500 个估算 token,替代此前的 50,000 字节配置。文字计量仍为估算,提供方用量具有权威性。图片分界处剩余空间保持空闲,以维持连续的首尾内容。其他内容块类型原样通过。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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-09-21-persistent-pwsh-keeps-controlled-prompt.md
|
||||
2026-09-21-persistent-pwsh-keeps-controlled-prompt.md: 3fd42b77d4536215f34c32643526dbb03d21674f
|
||||
2026-09-21-persistent-pwsh-keeps-controlled-prompt.zh.md: 88d4e2deef26af4ad92ef4a69d8ae17403027f70
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Persistent pwsh keeps the backend's controlled prompt
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-21-persistent-pwsh-keeps-controlled-prompt.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-tool-pwsh-persistent` initialized its shell with a `prompt` function of its own (`'__DSH_PERSISTENT_PWSH_PROMPT__ '`), overwriting the `prompt` that `dsh-terminal-bash` installs in its pwsh startup sequence. The backend's prompt readiness requires the printable tail after the OSC `133;D` marker to exactly equal the controlled `dsh> ` prompt ([design](../feature/2026-07-16-persistent-pty-sessions.md)), so after initialization no send could settle through it and every send paid the silence tier plus handoff grace. Measured on Windows through the real Loader composition with production defaults: 8493 ms for the first call (spawn, initialization, and command) and 3722/3832/3759 ms for the next three, against 1340/255/251/241 ms with the controlled prompt intact. The package tests masked it by configuring `idleSilenceMs: 300`.
|
||||
|
||||
The [persistent bash tool fixed the identical defect](../../archived/bug-fix/2026-08-15-persistent-bash-keeps-controlled-prompt.md) by giving up its own prompt override; the pwsh twin kept it. The override served the same two consumers there: a viewport-suffix fallback detecting "shell at a prompt without the end marker", and cosmetic stripping of prompt text from partial output.
|
||||
|
||||
## Decision
|
||||
|
||||
The tool no longer installs a prompt. `dsh-terminal-bash` owns the pwsh `prompt` function, installs it during `spawn`, and returns the session only after that startup reached `stdin_read` readiness, so the tool's initialization send had nothing left to establish and is deleted together with its `PWSH_PROMPT_SETUP` command.
|
||||
|
||||
The viewport-suffix fallback is replaced with the seam's own signal: a send that settles as `stdin_read` without the end marker in scrollback returns the captured partial output. The private prompt constant and its stripping are deleted; partial output may now end with the backend's own prompt text, which the tool cannot and should not know. `stripPrompt` collapses to `trimTrailingNewline`, matching `dsh-tool-bash-persistent`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Align the tool's prompt text with the backend's `dsh> `.** Rejected: it keeps a second installation of one protocol constant and a second owner of it, and the send is redundant because the backend had already installed the identical prompt before `spawn` resolved.
|
||||
|
||||
**Import the controlled prompt into the tool.** Rejected: the prompt is one provider's protocol constant; a Consumer matching it would couple the tool to `dsh-terminal-bash` specifically, and any other mounted pwsh-dialect backend would break it again.
|
||||
|
||||
**Lower `handoffGraceMs`/`idleSilenceMs` instead.** Rejected: no silence value fixes a dead fast path; it rebalances how much every call overpays, and the silence tier is also what absorbs a genuinely broken prompt.
|
||||
|
||||
**Re-assert the pwsh prompt per prompt, as `PROMPT_COMMAND` does for bash.** Rejected: PowerShell has no per-prompt hook outside the `prompt` function itself, so a redefinition cannot be healed the way bash's variable is; the residue stays a documented limitation.
|
||||
|
||||
## Consequences
|
||||
|
||||
Windows, production defaults, real Loader composition: tool calls drop from 8493/3722/3832/3759 ms to 1340/255/251/241 ms (spawn + initialization + first command, then three warm commands).
|
||||
|
||||
The `stdin_read` fallback is behavior, not only cosmetics: where a provider proves a foreground stdin wait, an interactive child returns captured partial output instead of spinning to the command deadline, and elsewhere the call still runs to `timeoutMs`. That early return keeps the session, so a child still reading stdin can consume the next call's command until that call reaches `timeoutMs` and resets the shell; `dsh-tool-bash-persistent` behaves the same way, and distinguishing the shell's own prompt readiness from a child's stdin wait belongs to the seam rather than to either tool. Complete marker-delimited output is unchanged.
|
||||
|
||||
The loader-composition suite records every send's `waitReason` and requires at least six `stdin_read` settlements and no `inferred_idle`: the regression is reported by the settle reason itself, not by how long silence takes. Reinstating the removed prompt override fails that assertion after 30 s with `expected 0 to be greater than or equal to 6`, while the case's output assertions still pass. The stub suite drops the initialization-handshake modes and pins the prompt text retained in partial output instead of its absence.
|
||||
|
||||
A model command that redefines `prompt` still removes the readiness marker and degrades later sends to the silence tier: the backend installs its prompt once at startup and cannot re-assert it.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Agent Note: 持久 pwsh 保留后端的受控提示符
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-21-persistent-pwsh-keeps-controlled-prompt.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-tool-pwsh-persistent` 用自己安装的 `prompt` 函数(`'__DSH_PERSISTENT_PWSH_PROMPT__ '`)初始化其 shell,覆盖了 `dsh-terminal-bash` 在 pwsh 启动序列中安装的 `prompt`。后端的提示符就绪检测要求 OSC `133;D` 标记之后的可打印尾部与受控提示符 `dsh> ` 完全相等([设计](../feature/2026-07-16-persistent-pty-sessions.zh.md)),因此初始化之后任何 send 都无法经由该路径结算,每次 send 都要支付静默层加交接宽限。在 Windows 上通过真实 Loader 组合以生产默认值实测:首次调用(spawn、初始化与命令)8493 毫秒,随后三次为 3722/3832/3759 毫秒;受控提示符完好时为 1340/255/251/241 毫秒。包测试把 `idleSilenceMs` 配成 300,掩盖了该问题。
|
||||
|
||||
[持久 bash 工具已修复同一缺陷](../../archived/bug-fix/2026-08-15-persistent-bash-keeps-controlled-prompt.md),方式是放弃自己的提示符覆盖;pwsh 孪生工具保留了它。该覆盖在两个工具中服务同样的两个消费点:用视口后缀检测「shell 已回到提示符但没有结束标记」的回退判定,以及从部分输出中剥离提示符文本的美化。
|
||||
|
||||
## Decision
|
||||
|
||||
工具不再安装提示符。pwsh 的 `prompt` 函数由 `dsh-terminal-bash` 拥有,在 `spawn` 期间安装,且只在该启动流程达到 `stdin_read` 就绪后才返回会话,因此工具的初始化 send 已无可确立之事,与其 `PWSH_PROMPT_SETUP` 命令一并删除。
|
||||
|
||||
视口后缀回退替换为 seam 自己的信号:一次以 `stdin_read` 结算而 scrollback 中没有结束标记的 send,返回已捕获的部分输出。私有提示符常量及其剥离逻辑删除;部分输出现在可能以后端自己的提示符文本结尾,工具无法也不应知道该文本。`stripPrompt` 收敛为 `trimTrailingNewline`,与 `dsh-tool-bash-persistent` 一致。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**把工具的提示符文本对齐到后端的 `dsh> `。** 被拒绝:这保留了同一个协议常量的第二份安装与第二个拥有者,而且该 send 是多余的——后端在 `spawn` 返回前就已安装相同的提示符。
|
||||
|
||||
**把受控提示符导入工具。** 被拒绝:提示符是单个提供方的协议常量;Consumer 匹配它就把工具与 `dsh-terminal-bash` 具体耦合,换任何其他 pwsh 方言后端都会再次损坏。
|
||||
|
||||
**改为调小 `handoffGraceMs`/`idleSilenceMs`。** 被拒绝:任何静默值都修不好已死的快速路径,只是重新分配每次调用多付多少;而静默层同时也是吸收真正损坏提示符的那一层。
|
||||
|
||||
**像 bash 的 `PROMPT_COMMAND` 那样在每个提示符处重新设定 pwsh 提示符。** 被拒绝:除 `prompt` 函数本身之外,PowerShell 没有每提示符钩子,因此重定义无法像 bash 的变量那样被治好;该残余已记入已知限制。
|
||||
|
||||
## Consequences
|
||||
|
||||
Windows、生产默认值、真实 Loader 组合:工具调用从 8493/3722/3832/3759 毫秒降至 1340/255/251/241 毫秒(spawn + 初始化 + 首条命令,随后三条热命令)。
|
||||
|
||||
`stdin_read` 回退是行为而不只是美化:在提供方能证明前台 stdin 等待之处,交互式子进程会返回已捕获的部分输出,而不是空转到命令期限;其他情况下调用仍会运行到 `timeoutMs`。该提前返回保留会话,因此仍在读取 stdin 的子进程可能吞掉下一次调用的命令,直到那次调用到达 `timeoutMs` 并重置 shell;`dsh-tool-bash-persistent` 行为相同,而区分「shell 自身的提示符就绪」与「子进程的 stdin 等待」属于 seam,而不是任一工具。由标记界定的完整输出保持不变。
|
||||
|
||||
loader 组合套件记录每次 send 的 `waitReason`,要求至少六次 `stdin_read` 且没有 `inferred_idle`:回归由结算原因本身报告,而不是靠静默层耗时。把删掉的提示符覆盖加回去,该断言会在 30 秒后以 `expected 0 to be greater than or equal to 6` 失败,而同一用例的输出断言仍然通过。stub 套件删除了初始化握手模式,并改为锚定部分输出中保留的提示符文本,而不是它的缺席。
|
||||
|
||||
重定义 `prompt` 的模型命令仍会移除就绪标记,并把后续 send 降级到静默层:后端只在启动时安装一次提示符,无法重新设定。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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-09-22-chat-scroll-follow-and-footer-geometry.md
|
||||
2026-09-22-chat-scroll-follow-and-footer-geometry.md: 56cd1681eef23162df2595a138545ade25e34212
|
||||
2026-09-22-chat-scroll-follow-and-footer-geometry.zh.md: 9979688d52373ae7a64a3f4fc22b932b43091b34
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# Agent Note: Separate Chat clipping and independent follow intent
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-22-chat-scroll-follow-and-footer-geometry.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Transcript overflow must remain clipped without trapping floating controls or changing the scroll ancestor of sticky content. An `overflow-y: hidden` ancestor becomes the nearest scroll container even when readers scroll a different outer element. It prevents Markdown code banners and expanded compaction headers from sticking to the visible conversation scrollport; floating controls inside that ancestor also use transcript geometry instead of viewport geometry.
|
||||
|
||||
A height-capped process group also has a separate reading position. Outer bottom-follow cannot expose newly appended content inside a group whose own scroll offset stays unchanged. Treating every non-bottom scroll event as reader input does not support native smooth following: the browser emits those same events while an animation approaches its target.
|
||||
|
||||
## Decision
|
||||
|
||||
The transcript, floating controls, and scroll intent retain separate ownership. The [scroll-state table](../../../../packages/client/ui-chat/README.md#scroll-ownership) and [group display rules](../../../../packages/client/ui-chat/src/client/conversation-nodes/README.md#display-modes) define the current product behavior.
|
||||
|
||||
| Area | Implementation | Reason |
|
||||
|---|---|---|
|
||||
| Transcript clipping | `ChatView` uses `overflow-x: visible; overflow-y: clip` on `.root` inside an unclipped frame. | Constrain vertical overflow without introducing a scroll container between sticky content and its actual scrollport. |
|
||||
| Floating controls | TurnNavigator and the back-to-bottom button are siblings of the clipped transcript. | Shared Conversations use the outer scrollport for sticky positioning; standalone Chat uses its frame for absolute positioning. |
|
||||
| Follow intent | `useScrollFollow` retains one independent controller per scrollport. | Reuse thresholds, position sampling, and native scrolling without sharing one follow flag. |
|
||||
| Group lifecycle | `useProcessScroll` owns its existing body/content observer, directional fades, and one-shot opening position. | Group growth remains observable after the body reaches its cap; opening performs no immediate geometry read. |
|
||||
|
||||
The clipped root owns no scroll offset. Shared Conversation scrolling, standalone Chat scrolling, capped process groups, and terminal sections retain their existing scrollports. The horizontal axis stays `visible`: pairing vertical `clip` with horizontal `auto` or `hidden` computes the vertical overflow back to `hidden` and restores the unwanted scroll container.
|
||||
|
||||
An unclosed group opens at its bottom; a closed group opens at its top. The group’s `closed` value, not the whole Turn’s status or an all-node search, decides that initial position. A later close does not reset an already-open reader. Browser find keeps its own reveal position, and display-mode changes preserve mounted members.
|
||||
|
||||
Native smooth growth keeps an outstanding target separate from the current position. Animation progress retains follow intent. Input interrupts native motion before the browser applies reader movement; the resulting actual displacement determines whether following stops or resumes. Further growth retains the issued target until `scrollend`. Arrival at that target, including shrink clamping, allows one request to the latest floor; an off-target stop releases following so focus and find keep their landing. Reduced motion and explicit opening positions use immediate scrolling. Outer following keeps its existing immediate behavior and sampling policy.
|
||||
|
||||
The back-to-bottom button depends only on outer follow intent and restores only that intent. Inner scrolling affects the outer controller only when scroll chaining actually moves the outer scrollport. Group height growth can resize the outer transcript, but the controllers do not invoke each other’s follow actions. Paging and semantic restoration remain with the existing outer reading/navigation owners.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Remove transcript clipping.** That would abandon the overflow constraint. Vertical `clip` retains clipping without creating a scroll container; the unclipped sibling frame keeps floating controls outside the clipped area without a Portal or document-level coordinate synchronization.
|
||||
|
||||
**Keep `overflow-y: hidden` and move only floating controls.** That protects the controls but leaves sticky content inside the unwanted scroll container. Changing the root to `clip` also preserves the scroll ancestor of code and compaction headers without moving those headers out of their content.
|
||||
|
||||
**Share one follow flag, or duplicate the complete controllers.** One flag would pull a group away from a reader merely because the outer transcript follows. Two complete implementations would duplicate animation attribution and threshold rules. Sharing the small controller retains independent state without moving paging, Turn navigation, or semantic memory into it.
|
||||
|
||||
**Only replace `scrollTop` assignment with smooth `scrollTo`.** Intermediate animation offsets would appear to be reader movement and disable following. Native motion needs an explicit target and input interruption; a custom animation-frame interpolator is unnecessary.
|
||||
|
||||
## Verification
|
||||
|
||||
[Controller tests](../../../../packages/client/ui-chat/tests/scroll-follow.client.spec.ts) cover independent thresholds, animation progress, target reuse, input interruption, reduced motion, and immediate positioning. [Chat component tests](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx) cover group opening, pause/resume, closing while open, and browser-find preservation. [Viewport tests](../../../../packages/client/ui-chat/tests/chat-viewport.client.spec.ts) retain the outer anchor and attribution behavior.
|
||||
|
||||
[Browser scroll cases](../../../../apps/web/tests/chat-scroll-contract.e2e.ts) exercise floating-control geometry, nested following, paging, streaming, and real wheel input through the shipped Web composition.
|
||||
|
||||
[Seeded-history cases](../../../../apps/web/tests/seeded-history.e2e.ts) check an expanded compaction header and its nested code banner against the actual conversation scrollport, including their stacking and copy-button access.
|
||||
|
||||
## Consequences
|
||||
|
||||
The shared controller stores intent without scheduling React renders; callers still publish visible state changes, and group edge fades update only when their booleans change. Follow operations read the affected scrollport, not its Node collection. Native scrolling still performs browser layout and paint work. These changes establish behavior and ownership, not a measured latency or memory improvement.
|
||||
|
||||
The [navigation decision](../architecture/2026-09-18-chat-navigation-performance.md) still owns sampled reading, known landings, and batched historical jumps. The [grouping decision](../architecture/2026-09-21-conversation-build-groups.md) still owns Definition-based membership and stable rendering identities. Extracting scroll-follow mechanics does not supersede either decision.
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# Agent Note: 分离 Chat 裁剪与独立滚动跟随意图
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-22-chat-scroll-follow-and-footer-geometry.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
正文溢出必须保持裁剪,同时不能限制悬浮控件或改变 sticky 内容的滚动祖先。即使读者实际滚动的是外层元素,`overflow-y: hidden` 祖先也会成为最近的滚动容器。这会阻止 Markdown 代码块头栏和展开的压缩摘要头栏吸附在可见会话滚动区域;位于该祖先内的悬浮控件也会参照正文几何,而非视口几何。
|
||||
|
||||
限高过程组也有独立阅读位置。若组内偏移不变,外层跟随底部无法展示组内新增内容。把每个未到底的滚动事件都当成读者输入,也无法支持原生平滑跟随:浏览器在动画接近目标期间同样会发出这些事件。
|
||||
|
||||
## 决策
|
||||
|
||||
正文、悬浮控件及滚动意图分别由独立对象负责。[滚动状态表](../../../../packages/client/ui-chat/README.zh.md#scroll-ownership)与[分组展示规则](../../../../packages/client/ui-chat/src/client/conversation-nodes/README.zh.md#display-modes)定义当前产品行为。
|
||||
|
||||
| 范围 | 实现 | 原因 |
|
||||
|---|---|---|
|
||||
| 正文裁剪 | `ChatView` 在无裁剪的外框内,对 `.root` 使用 `overflow-x: visible; overflow-y: clip`。 | 限制纵向溢出,不在 sticky 内容与其实际滚动区域之间引入滚动容器。 |
|
||||
| 悬浮控件 | TurnNavigator 与回到底部按钮位于正文裁剪层的同级。 | 共享 Conversation 相对外层滚动容器做 sticky 定位;独立 Chat 相对自身外框做 absolute 定位。 |
|
||||
| 跟随意图 | `useScrollFollow` 为每个滚动区保留独立控制器。 | 共用阈值、位置采样及原生滚动,不共用一个跟随开关。 |
|
||||
| 小组生命周期 | `useProcessScroll` 负责既有的正文与内容观察器、方向渐隐和一次性展开位置。 | 正文达到限高后,内容增长仍可被观察;展开时不立即读取几何。 |
|
||||
|
||||
被裁剪的根节点不持有滚动偏移。共享 Conversation、独立 Chat、限高过程组和终端区域保留各自原有的滚动容器。横轴保持 `visible`:纵向 `clip` 如果与横向 `auto` 或 `hidden` 组合,纵向溢出的计算值会变回 `hidden`,重新引入不需要的滚动容器。
|
||||
|
||||
未关闭的小组展开到自己的底部,已关闭的小组展开到顶部。初始位置由组自身的 `closed` 决定,不取整个 Turn 的状态,也不遍历全部节点。组随后关闭不会重置已展开的阅读位置。浏览器查找保留自身的展开定位,模式切换保留已挂载成员。
|
||||
|
||||
原生平滑跟随将未完成的目标与当前位置分开记录。动画进度保留跟随意图。输入在浏览器处理读者移动前中断原生动画,再由实际位移决定是否暂停或恢复跟随。后续增长保留已发出的目标,直到 `scrollend`。到达该目标(包括内容缩短后的钳制位置)才允许再次请求最新底部;停在目标之外则释放跟随,让焦点和查找保留落点。减少动态效果与明确的展开定位使用即时滚动。外层跟随保留既有的即时行为和采样策略。
|
||||
|
||||
回到底部按钮只取决于外层跟随意图,也只恢复该意图。只有滚动传递确实移动外层滚动区域时,内层滚动才影响外层控制器。小组长高可以改变外层正文尺寸,但两个控制器不会互相调用跟随操作。分页和语义恢复仍由既有的外层阅读与导航对象负责。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**删除正文裁剪。** 这会放弃溢出约束。纵向 `clip` 保留裁剪而不创建滚动容器;无裁剪的同级外框让悬浮控件位于裁剪区域之外,不需要 Portal 或文档级坐标同步。
|
||||
|
||||
**保留 `overflow-y: hidden`,仅移动悬浮控件。** 这可以保护控件,但 sticky 内容仍位于不需要的滚动容器内。将根节点改为 `clip` 也能保留代码和压缩摘要头栏的滚动祖先,无需把这些头栏移出其正文。
|
||||
|
||||
**共用一个跟随开关,或复制完整控制器。** 共用开关会仅因外层跟随就把小组拉离读者位置。复制两套完整实现则会重复动画归因和阈值规则。共用小控制器可以保留独立状态,同时不把分页、轮次导航及语义记忆搬入其中。
|
||||
|
||||
**只把 `scrollTop` 赋值换成平滑 `scrollTo`。** 动画中间偏移会被误判为读者移动,导致跟随关闭。原生动画需要明确的目标与输入中断处理,无需自写动画帧插值器。
|
||||
|
||||
## 验证
|
||||
|
||||
[控制器测试](../../../../packages/client/ui-chat/tests/scroll-follow.client.spec.ts)覆盖独立阈值、动画进度、目标复用、输入中断、减少动态效果和即时定位。[Chat 组件测试](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx)覆盖小组展开、暂停与恢复、展开期间关闭及浏览器查找位置保持。[视口测试](../../../../packages/client/ui-chat/tests/chat-viewport.client.spec.ts)保留外层锚点与归因行为。
|
||||
|
||||
[浏览器滚动用例](../../../../apps/web/tests/chat-scroll-contract.e2e.ts)通过正式 Web 装配验证悬浮控件几何、嵌套跟随、分页、流式内容与真实滚轮输入。
|
||||
|
||||
[历史回放用例](../../../../apps/web/tests/seeded-history.e2e.ts)相对实际会话滚动区域检查展开的压缩摘要头栏及其内部代码头栏,包括层叠关系和复制按钮的可访问性。
|
||||
|
||||
## 影响
|
||||
|
||||
共享控制器只保存意图,不自行调度 React 渲染;调用方仍发布可见状态变化,小组边缘渐隐仅在布尔值变化时更新。跟随操作读取受影响的滚动容器,而非节点集合。原生滚动仍有浏览器布局和绘制成本。这些改动确立行为与职责,不代表已测得延迟或内存收益。
|
||||
|
||||
[导航决策](../architecture/2026-09-18-chat-navigation-performance.zh.md)继续负责采样阅读、已知落点与聚合历史跳转。[分组决策](../architecture/2026-09-21-conversation-build-groups.zh.md)继续负责 Definition 成员关系与稳定渲染身份。提取滚动跟随机制不取代这两项决策。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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-09-22-input-echo-admission-ownership.md
|
||||
2026-09-22-input-echo-admission-ownership.md: 227857bd8168aa1d7f51b621737c2ed0ab572632
|
||||
2026-09-22-input-echo-admission-ownership.zh.md: 1d64de220a3c6d5c9357770175c79266b8f9f063
|
||||
@@ -0,0 +1,54 @@
|
||||
# Agent Note: Input echoes retain display ownership through admission
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-22-input-echo-admission-ownership.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
An idle submission can appear first as a local Chat echo, then as an Inbox row in QueueDock, and finally as a durable user message. Inbox projection and conversation history arrive independently: retiring the echo on acceptance leaves a gap before admission, while a delayed Inbox projection can duplicate an already-admitted message. Separately, turn/start precedes user/message, so an empty progress title can appear before the echo and exchange positions with it when the real input arrives.
|
||||
|
||||
These transitions concern display ownership, not scrolling or composer geometry. A delay cannot identify which representation is authoritative, and moving every echo before the progress title would move running steering ahead of existing work.
|
||||
|
||||
## Decision
|
||||
|
||||
- A submission keeps its locally selected placement: idle input stays in Chat, running steering stays at the process tail, and explicitly queued input stays in QueueDock. Inbox acceptance does not change that placement.
|
||||
- Chat hides an echo by matching the durable input's rpcId in the same render, and excludes stale next-step Inbox rows matched by admitted local Chat submissions. QueueDock excludes only Inbox rows matched by local transcript echoes; unrelated queued messages remain visible and actionable.
|
||||
- Session retains an admitted transcript or steering submission's local suppression identity until the Inbox projection watermark reaches its claim sequence. Higher-sequence projection retention then prevents an older accepted row from reappearing after that identity retires. Claim tracking and cancellation use the existing per-submission receipt for both Inbox targets; no new SessionSnapshot field, Host event, or persisted format is needed.
|
||||
- At an open, input-free progress control at the end of Chat, the first transcript echo precedes the control. Other echoes remain at the tail. Last-input Turn tracking shares the existing rpcId scan; it does not add a history traversal or predict a sequence of future Turns.
|
||||
- Pending bubbles and durable seats share one keyed React list. Inserting a progress title preserves the pending bubble's mounted identity. Once admitted, the durable node owns ordering and ordinary Group segmentation.
|
||||
|
||||
The [durable Inbox recovery decision](2026-08-17-durable-web-queue-recovery.md) still owns cold reads, reconstruction and projection transport. The [generic file upload decision](../feature/2026-08-26-generic-file-upload.md) still owns attachment preparation and draft recovery; retirement returns each submission's durable references once, in attachment order.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Delay QueueDock or echo removal by a fixed interval.** CPU throttling, transport batching and projection ordering can exceed any such interval. Time does not establish admission or confirm that stale queue state is gone.
|
||||
|
||||
**Retain the echo without changing the other readers.** Chat-side Inbox deduplication can still remove the echo, and QueueDock can still display it. The Session lifetime, Chat handoff and Dock exclusion must agree.
|
||||
|
||||
**Delete the suppression identity as soon as user/message arrives.** A later Inbox acceptance projection can resurrect a Dock or pending-steering row. The claim watermark supplies the required ordering evidence without assuming the streams publish together.
|
||||
|
||||
**Move all pending inputs before the newest Turn control.** Running steering belongs after existing process content, and several ordinary submissions can target different Turns. Only the first local transcript echo receives the empty-control placement adjustment.
|
||||
|
||||
**Add a Host correlation event or a client-side Turn scheduler.** Exact prediction across competing clients and reordered requests would widen the change beyond local optimistic presentation. Durable admission remains authoritative; optimistic ordering is deliberately approximate in those races.
|
||||
|
||||
## Verification
|
||||
|
||||
| Evidence | Covered behavior |
|
||||
|---|---|
|
||||
| [Session submissions](../../../../packages/api/session-controller/tests/session-pending-submissions.client.spec.ts) | Projection-first and history-first handoff, claim watermark, overlapping opening/queue/steering identities, attachment callbacks, cancellation, unadmitted endings, reconnect baselines and disposal. |
|
||||
| [Projection store](../../../../packages/api/session-controller/tests/projection-store.client.spec.ts) | Missing/cached watermarks, monotonic sequence retention and clearing. |
|
||||
| [Chat rendering](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx) | Same-render rpcId replacement, stable echo mounting, title-before-input delivery in all three modes, steering remaining behind the title, historical empty Turns, and six local-versus-Host orderings. |
|
||||
| [QueueDock](../../../../packages/client/ui-conversation/tests/queue-dock.client.spec.tsx) | Local transcript exclusion, unrelated rows/actions, and all six three-message acceptance orders followed by FIFO claims. |
|
||||
| [Recorded Web replay](../../../../apps/web/tests/idle-submission-handoff.e2e.ts) | Real Web composition with FIFO-preserving delivery barriers: both Inbox/history arrival orders at 6× CPU throttling, an explicit turn/start-to-user/message gap, and complete three-Turn handoff for all six request orders. |
|
||||
| [Steering Web replay](../../../../apps/web/tests/steering.e2e.ts) | Local shortcut steering remains unique when durable admission precedes its Inbox claim projection. |
|
||||
|
||||
The single-input regression fails before the fix: Inbox acceptance moves the bubble into Dock, and a separately delivered Turn title precedes the echo. The corrected path retains one Chat representation at a stable vertical position. Multi-input replay verifies durable order and absence of duplicate or residual echoes; it does not assert that optimistic ordering predicts Host order.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Steering without a locally tracked submission still follows the Inbox projection; a delayed projection can briefly duplicate its durable row. Local deduplication adds no history scan for other clients' submissions.
|
||||
- Reconnect may discard receipt-confirmed optimistic echoes and display the new authoritative baseline. A claimed input outside that baseline can briefly have no bubble; reconnect does not promise DOM continuity.
|
||||
- Submissions made before the local running update can all remain transcript echoes. If A, B and C are submitted locally in that order but B or C enters first, durable admission changes their visible order. Even when A enters first, remaining local echoes can differ from the Host queue order.
|
||||
- Cross-client competition and request-preparation races can change the actual opening input. The client does not migrate an idle-classified echo into Dock merely because the Host queued it behind another Turn.
|
||||
- The changes prevent representation handoff flicker on the ordinary online path without changing model input, Inbox scheduling, scroll-follow policy, or released Session data. The browser barriers establish behavior, not a general latency or throughput improvement.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Agent Note: 输入回显在入档期间保持展示归属
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-22-input-echo-admission-ownership.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
空闲时提交的输入可能先显示为 Chat 本地回显,再变为 QueueDock 的 Inbox 行,最后才显示为持久用户消息。Inbox 投影与会话历史独立到达:受理时回收回显会在入档前留下空档,迟到的 Inbox 投影又可能重复显示已经入档的消息。此外,turn/start 先于 user/message,因此空过程标题可能先出现在回显前面,真实输入到达时再与它交换位置。
|
||||
|
||||
这些转换涉及展示归属,而非滚动或 composer 几何。延迟不能判断哪种表示已经成为权威,将所有回显移到过程标题前又会把运行中的 steering 移到已有工作之前。
|
||||
|
||||
## 决策
|
||||
|
||||
- 提交保留本地发送时选择的 placement:空闲输入留在 Chat,运行中的 steering 留在过程末尾,明确排队的输入留在 QueueDock。Inbox 受理不改变展示归属。
|
||||
- Chat 在同一次渲染中按持久输入的 rpcId 隐藏回显,并排除匹配已入档本地 Chat 提交的旧 next-step Inbox 行。QueueDock 只排除匹配本地 transcript 回显的 Inbox 行;其他排队消息仍可见且可操作。
|
||||
- Session 保留已入档 transcript 或 steering 提交的本地排除身份,直到 Inbox 投影水位达到其领取序号。随后,较高序号投影优先的规则阻止旧受理行在该身份回收后重新出现。领取跟踪和取消共用已有逐提交 receipt,并覆盖两种 Inbox target;不新增 SessionSnapshot 字段、Host 事件或持久格式。
|
||||
- Chat 末尾为进行中且没有真实输入的过程控制行时,第一条 transcript 回显显示在控制行前,其他回显留在末尾。最后输入的轮次信息复用现有 rpcId 扫描,不新增历史遍历,也不预测未来轮次序列。
|
||||
- 待处理气泡与持久节点容器共用一个 keyed React 列表。插入过程标题保留回显气泡的挂载身份。入档后,由持久节点决定排序及普通 Group 切分。
|
||||
|
||||
[持久 Inbox 恢复决策](2026-08-17-durable-web-queue-recovery.zh.md)仍负责冷读取、重建与投影传输。[通用文件上传决策](../feature/2026-08-26-generic-file-upload.zh.md)仍负责附件准备及草稿恢复;回收按附件顺序返回各提交的持久引用,且只回调一次。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**固定延迟 QueueDock 或回显回收。** CPU 降速、传输批处理与投影到达顺序都可能超过该间隔。时间不能证明入档,也不能确认过期队列状态已经消失。
|
||||
|
||||
**只延长回显生命周期,不调整其他读取方。** Chat 侧按 Inbox 去重仍会移除回显,QueueDock 仍可能展示同一消息。Session 生命周期、Chat 接管和 Dock 排除必须一致。
|
||||
|
||||
**user/message 到达后立即删除排除身份。** 较晚的 Inbox 受理投影会复活 Dock 行或待处理 steering 行。领取水位提供必要的排序依据,不要求两条流同时发布。
|
||||
|
||||
**把全部待处理输入移到最新 Turn 控制行前。** 运行中的 steering 属于已有过程内容之后,多条普通提交也可能属于不同轮次。只有第一条本地 transcript 回显参与空控制行前的定位调整。
|
||||
|
||||
**新增 Host 关联事件或客户端 Turn 调度器。** 精确预测跨客户端竞争和请求乱序会超出本地乐观展示的范围。持久入档仍是权威;这些竞争场景中的乐观排序有意保持近似。
|
||||
|
||||
## 验证
|
||||
|
||||
| 证据 | 覆盖行为 |
|
||||
|---|---|
|
||||
| [Session 提交](../../../../packages/api/session-controller/tests/session-pending-submissions.client.spec.ts) | 投影先到与历史先到的接管、领取水位、开场/排队/steering 身份交错、附件回调、取消、未入档结束、重连基线及销毁。 |
|
||||
| [投影存储](../../../../packages/api/session-controller/tests/projection-store.client.spec.ts) | 缺失或缓存水位、单调序号保留及清理。 |
|
||||
| [Chat 渲染](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx) | 同帧 rpcId 替换、回显稳定挂载、三种模式下标题先于输入到达、steering 留在标题之后、历史空轮次及六种本地与 Host 排列。 |
|
||||
| [QueueDock](../../../../packages/client/ui-conversation/tests/queue-dock.client.spec.tsx) | 本地 transcript 排除、其他行及操作、三条消息的全部六种受理顺序及后续 FIFO 领取。 |
|
||||
| [录制 Web 回放](../../../../apps/web/tests/idle-submission-handoff.e2e.ts) | 真实 Web 装配配合保留 FIFO 的投递屏障:6× CPU 降速下的 Inbox/历史两种到达顺序、明确的 turn/start 至 user/message 空档,以及六种请求排列的完整三轮接管。 |
|
||||
| [Steering Web 回放](../../../../apps/web/tests/steering.e2e.ts) | 本地快捷键 steering 的持久入档先于 Inbox 领取投影到达时,仍只显示一份。 |
|
||||
|
||||
单条输入回归在修复前失败:Inbox 受理将气泡移入 Dock,单独交付的 Turn 标题又会出现在回显之前。修复后的路径保留唯一的 Chat 表示,且纵向位置稳定。多输入回放验证持久顺序及无重复、无残留回显,不断言乐观排列可以预测 Host 顺序。
|
||||
|
||||
## 后果
|
||||
|
||||
- 没有本地提交身份的 steering 仍按 Inbox 投影显示;投影迟到时可能短暂与持久节点重复。本地去重不为其他客户端的提交增加历史扫描。
|
||||
- 重连可撤销已有接收回执的乐观回显,并展示新的权威基线。已领取但不在该基线中的输入可能短暂无气泡;重连不保证 DOM 连续性。
|
||||
- 本地 running 更新前连续提交的消息可能都留为 transcript 回显。若本地按 A、B、C 提交,但 B 或 C 先入档,持久接管会调整可见顺序。即使 A 先入档,其余本地回显也可能与 Host 队列顺序不同。
|
||||
- 跨客户端竞争和请求准备的先后差异可能改变实际开场输入。客户端不会仅因 Host 将输入排在另一轮后面,就把已按空闲分类的回显迁入 Dock。
|
||||
- 这些改动消除普通在线路径中表示切换带来的抖动,不改变模型输入、Inbox 调度、滚动跟随规则或已发布 Session 数据。浏览器屏障证明行为,不代表普遍的延迟或吞吐提升。
|
||||
@@ -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/feature/2026-07-07-mcp-client-plugin.md
|
||||
2026-07-07-mcp-client-plugin.md: a4aa2cf216d9f28afedebf82f0659db70606bbf1
|
||||
2026-07-07-mcp-client-plugin.zh.md: 5903e4ef976317602527d9f335d640bc86939f72
|
||||
2026-07-07-mcp-client-plugin.md: dd793b67bf2043fa88377d50837df40faadf3fbf
|
||||
2026-07-07-mcp-client-plugin.zh.md: 5056118df0185f1e7ec2b1ea06818ed920d2af26
|
||||
@@ -145,7 +145,7 @@ A unified `execute` handler for all tools from one MCP server:
|
||||
1. Call the SDK's `callTool` with the closed-over `rawName`, model arguments, `exec.signal`, the configured timeout, and the complete discovered `toolDefinition` — the public name is never sent to the server.
|
||||
2. Preserve canonical success as `{ content: JsonValue[], structuredContent? }`; complete MCP JSON blocks remain the programmatic/PTC mode value. `isError: true` throws before any image persistence so the registry owns the failure path.
|
||||
3. Prepare a separate ordered Native projection. Text runs join with `'\n'`; resource links preserve name and URI as text; audio and embedded resources become explicit diagnostics. The SDK rejects malformed wire results. If any image exists, the bridge strictly decodes the complete batch, resolves the calling agent's latest exact route, requires an attachment store plus explicit model image input, and delegates all-member validation and ordered persistence to `AttachmentStore.saveImages()`. Any decode, capability, or storage refusal renders every image as diagnostic text and returns no partial references.
|
||||
4. Keep `output.render` synchronous and pure. The executor stages its richer projection in a generation-local `WeakMap` keyed by the exact execution; `finalizeContent` installs it only when the registry's post-execute result still has the original canonical value and fallback content. A policy block, value replacement, or content replacement remains authoritative, and a re-sync cannot let an older generation consume new execution state.
|
||||
4. The executor preserves the complete canonical MCP value and prepares ordered text/image content. `projectContent` installs that content before `tools/post-execute`, so retention sees real images. Subsequent content replacement, value replacement, and blocking remain authoritative.
|
||||
5. PTC mode receives the untouched canonical value. Its generic dispatch bridge defers a successful final content sequence containing an image through the outer `run_code` result, so MCP requires no private parent-token special case.
|
||||
6. Cancellation: `exec.signal` (from the agent loop's cancel) is passed through to the MCP SDK's `callTool`, exact-model lookup, and the pre-storage gate.
|
||||
|
||||
@@ -157,6 +157,8 @@ Build the child environment from the subprocess seam's shared `scrubbedParentEnv
|
||||
|
||||
A per-instance connection supervisor reconnects automatically after a lost connection with bounded exponential backoff and a per-outage attempt budget, re-running discovery on success; exhaustion unregisters the server's tools and stops until reload. The [auto-reconnect Agent Note](../../archived/feature/2026-08-06-mcp-client-auto-reconnect.md) owns that decision, including the `reconnect` config block and the `reconnect.enabled: false` opt-out that restores manual HMR/restart recovery.
|
||||
|
||||
The [multimodal retention decision](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.md) explains why prepared images enter content before result policies.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### MCP Server side (expose harness tools to external MCP clients)
|
||||
|
||||
@@ -145,7 +145,7 @@ SDK 接受符合协议的工具,并执行现代 HTTP header 声明检查。注
|
||||
1. 调用 SDK 的 `callTool`,传入闭包持有的 `rawName`、模型参数、`exec.signal`、配置的超时时间与完整的已发现 `toolDefinition`——公开名称永远不发送给服务器。
|
||||
2. 把规范成功值保留为 `{ content: JsonValue[], structuredContent? }`;完整 MCP JSON 块仍是程序化调用/PTC mode 值。`isError: true` 会在持久化任何图片前抛出,使失败路径归注册表所有。
|
||||
3. 另行准备有序 Native 投影。连续文本块以 `'\n'` 连接;资源链接以文本保留名称和 URI;音频和嵌入资源成为明确诊断。SDK 拒绝格式错误的协议结果。只要存在图片,桥接层就严格解码完整批次,解析调用 agent 的最新确切路由,要求附件存储以及模型明确支持图片输入,再把全成员校验和有序持久化委托给 `AttachmentStore.saveImages()`。任何解码、能力或存储拒绝都会把全部图片渲染为诊断文本,且不返回部分引用。
|
||||
4. 保持 `output.render` 同步且纯净。执行器把更丰富的投影暂存在按同步世代创建、以确切执行为键的 `WeakMap` 中;只有注册表的 post-execute 结果仍保留原规范值和兜底内容时,`finalizeContent` 才安装该投影。策略阻止、值替换或内容替换仍具有权威性,重新同步也无法让旧世代消费新执行状态。
|
||||
4. 执行器保留完整的规范 MCP 返回值,并准备有序图文内容。`projectContent` 在 `tools/post-execute` 之前安装已准备的内容,让省略策略看到真实图片块。后续策略的内容替换、值替换和阻止均保持生效。
|
||||
5. PTC mode 接收未改动的规范值。其通用分发桥接层会把包含图片的成功最终内容序列经外层 `run_code` 结果延后,因此 MCP 无需私有父 token 特例。
|
||||
6. 取消:`exec.signal`(来自 agent loop 的取消)透传给 MCP SDK 的 `callTool`、确切模型查询和存储前门禁。
|
||||
|
||||
@@ -157,6 +157,8 @@ SDK 接受符合协议的工具,并执行现代 HTTP header 声明检查。注
|
||||
|
||||
每个实例的连接监督器在连接丢失后以有界指数退避和单次故障尝试预算自动重连,成功后重新执行发现流程;尝试耗尽则注销该服务器的工具并停止,直到重新加载。[自动重连 Agent Note](../../archived/feature/2026-08-06-mcp-client-auto-reconnect.md) 拥有该决策,包括 `reconnect` 配置块和恢复手动 HMR/重启恢复的 `reconnect.enabled: false` opt-out。
|
||||
|
||||
图片在结果策略之前进入内容的原因见[图文结果保留决策](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.zh.md)。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### MCP Server 端(将 harness 工具暴露给外部 MCP 客户端)
|
||||
|
||||
+2
-2
@@ -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/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.md
|
||||
2026-08-11-background-job-completion-wakes-an-idle-owner.md: b8a10a2150ead02036cb97506a25c3a51202adcf
|
||||
2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md: adc0770cd00adeb3b13c95dcc386464493dcd943
|
||||
2026-08-11-background-job-completion-wakes-an-idle-owner.md: f13f09d326dc7b4e7b2998f3124f70c185c50922
|
||||
2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md: f9b563ebbf90e2ca19d1fc5ca9fa5b028e4429f1
|
||||
+7
-7
@@ -28,11 +28,11 @@ For a driver that is genuinely running, `steer()` and `inject()` are the same de
|
||||
|
||||
Injection is correct there. A cancelled turn is a user pressing stop, and reopening one on their behalf launders an interrupt into a model request they did not ask for. The turn loop already covers the ordinary case: it cannot close while the next-step inbox holds anything, so a notice arriving before that check extends the current turn, and several tasks settling together cost one step rather than one turn each.
|
||||
|
||||
### Waking is bounded, and the bound is not time
|
||||
### The cap on consecutive wakes is opt-in, and it is not time
|
||||
|
||||
`maxConsecutiveWakes` (default 3) caps the turns one owner may open this way; beyond it a notice degrades to injection and waits for the next turn. Claiming any user-authored message restores the budget — claiming, not arrival, because that is the point human input actually enters a step. Notices this plugin queued never refill it.
|
||||
Waking is unbounded unless `maxConsecutiveWakes` is set. With it set, the value caps the turns one owner may open this way; beyond it a notice degrades to injection and waits for the next turn. Claiming any user-authored message restores the budget — claiming, not arrival, because that is the point human input actually enters a step. Notices this plugin queued never refill it.
|
||||
|
||||
The bound exists because this chain is self-exciting in a way subagent settlement is not. Settlement is bounded by how many children the model spawned; a woken turn can start the background job whose completion wakes it again, with nobody watching. `dsh run` needs no separate policy: its one user message is claimed in the first turn and never repeats, so the budget is spent monotonically and the process terminates.
|
||||
The cap exists because this chain is self-exciting in a way subagent settlement is not. Settlement is bounded by how many children the model spawned; a woken turn can start the background job whose completion wakes it again, with nobody watching. It has no default because a notice past the cap stalls the session invisibly; the [unbounded-by-default decision](2026-09-22-unbounded-completion-wakes-by-default.md) records that trade-off. `dsh run` needs no separate policy either way: headless exits once the task turn goes idle, so a completion after that point has no owner to wake.
|
||||
|
||||
`completionDelivery: quiet` restores the old lane for idle owners. It exists for deterministic transcripts; job completion independently retains `quiet | wakeup` because its bounded owner-turn policy differs from next-step subagent reports.
|
||||
|
||||
@@ -52,15 +52,15 @@ The bound exists because this chain is self-exciting in a way subagent settlemen
|
||||
|
||||
**A general unsolicited-input queue** with priority lanes, as Claude Code uses to merge background jobs, cron, MCP push, and hooks into one drain. DSH's inbox already is that queue — durable `agent/inbox/spliced` splices over `next-turn`/`next-step` — so this would add a layer above an existing one to decide a single bit.
|
||||
|
||||
**Refusing to reopen a turn that already produced a visible answer,** Codex's `MailboxDeliveryPhase` latch. That latch is the default this decision deliberately inverts: waking after the model has spoken is the entire point, and the wake budget is the bound instead.
|
||||
**Refusing to reopen a turn that already produced a visible answer,** Codex's `MailboxDeliveryPhase` latch. That latch is the default this decision deliberately inverts: waking after the model has spoken is the entire point, and the optional wake budget is the bound instead.
|
||||
|
||||
**A wall-clock window** on top of the counter. For an interactive agent the slow case is the wanted one — an hour-long build finishing and the agent resuming is the feature — and `dsh run` is already bounded by the counter it cannot refill. Worth revisiting only if an unattended long-lived deployment appears.
|
||||
**A wall-clock window** on top of the counter. For an interactive agent the slow case is the wanted one — an hour-long build finishing and the agent resuming is the feature — and `dsh run` exits once its task turn goes idle regardless. Worth revisiting only if an unattended long-lived deployment appears.
|
||||
|
||||
**Suppressing `onJobDone` entirely during owner drain,** symmetric with the service-wide `listenersClosed`. It reads cleaner and removes a signal that is not only for notices: the force-fail record and the runtime invariant both observe teardown settlements. The `reported` bit denies exactly the reporters and nothing else.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Default behavior changes: an idle owner now spends a model request per completion, capped at `maxConsecutiveWakes` per owner between user messages. Deployments that want the old behavior set `completionDelivery: quiet`.
|
||||
- Default behavior changes: an idle owner now spends a model request per completion, capped per owner between user messages only where `maxConsecutiveWakes` is set. Deployments that want the old behavior set `completionDelivery: quiet`.
|
||||
- The `tool-jobs` prompt section needs no edit; "You are notified in-session when a task finishes" became true rather than aspirational.
|
||||
- `JobSnapshot.reported` gains teardown as a fourth setter, documented at the Service Definition and in [the subsystem reference](../../../../docs/subsystems/jobs.md).
|
||||
- `settle()` announces completion after committing the record and publishing the visible-set change. Any listener relying on running before waiters were released or before `onJobsChanged` now runs after both.
|
||||
@@ -69,7 +69,7 @@ The bound exists because this chain is self-exciting in a way subagent settlemen
|
||||
|
||||
### Accepted risks
|
||||
|
||||
A spent budget is restored only by user input. An unattended agent that exhausts it collects its remaining notices whenever something else opens a turn, and nothing re-arms it in the meantime.
|
||||
Under a configured `maxConsecutiveWakes`, a spent budget is restored only by user input. An unattended agent that exhausts it collects its remaining notices whenever something else opens a turn, nothing re-arms it in the meantime, and the client shows nothing while the notice waits. This is why the cap has no default.
|
||||
|
||||
A notice pending on an idle owner under `quiet` still dies with that owner's disposal, unchanged from before: the disposal cancel clears the unclaimed inbox and the log keeps the insert/cancel pair as the record. The [settlement delivery note](2026-08-06-manager-owned-subagent-settlement-delivery.md) owns the offline-mailbox discussion this would need.
|
||||
|
||||
|
||||
+7
-7
@@ -28,11 +28,11 @@ Update:本文依赖的 `reported` 位与 `onJobDone` 已随 [jobs seam 收敛]
|
||||
|
||||
在那里注入才是对的。轮次被取消意味着用户按了停止,替他们重新开一轮等于把一次中断洗成了他们没有要求的模型请求。普通情形已由轮次循环覆盖:只要 next-step inbox 还有内容,轮次就无法结束,因此在该检查之前抵达的通知会延长当前轮次,同时结算的多个任务只花掉一步而不是各占一轮。
|
||||
|
||||
### 唤醒有界,且该界不是时间
|
||||
### 连续唤醒上限是可选的,且该界不是时间
|
||||
|
||||
`maxConsecutiveWakes`(默认 3)限制一个所有者由此开启的轮数;超出后通知降级为注入,等待下一轮。领取任何用户撰写的消息都会恢复预算——是领取而非抵达,因为那才是人类输入真正进入某一步的时刻。本插件自己排队的通知永远不会补充它。
|
||||
除非设置了 `maxConsecutiveWakes`,唤醒没有上限。设置后,该值限制一个所有者由此开启的轮数;超出后通知降级为注入,等待下一轮。领取任何用户撰写的消息都会恢复预算——是领取而非抵达,因为那才是人类输入真正进入某一步的时刻。本插件自己排队的通知永远不会补充它。
|
||||
|
||||
设界是因为这条链会自激,而 subagent 结算不会。结算受限于模型派生了多少子 agent;被唤醒的一轮却可能启动某个后台任务,而它的完成又会唤醒同一个所有者,且无人旁观。`dsh run` 不需要单独策略:它唯一的用户消息在第一轮就被领取且不会重复,因此预算单调消耗,进程必然终止。
|
||||
设上限是因为这条链会自激,而 subagent 结算不会。结算受限于模型派生了多少子 agent;被唤醒的一轮却可能启动某个后台任务,而它的完成又会唤醒同一个所有者,且无人旁观。它没有默认值,因为超出上限的通知会让会话不可见地停摆;[默认无上限的决策](2026-09-22-unbounded-completion-wakes-by-default.zh.md)记录了这项取舍。`dsh run` 无论如何都不需要单独策略:headless 在任务轮次空闲后即退出,此后的完成没有可唤醒的所有者。
|
||||
|
||||
`completionDelivery: quiet` 为空闲所有者恢复旧通道。它的存在是为了确定性 transcript;后台任务完成会独立保留 `quiet | wakeup`,因为其有界的所有者轮次策略不同于 next-step subagent 报告。
|
||||
|
||||
@@ -52,15 +52,15 @@ Update:本文依赖的 `reported` 位与 `onJobDone` 已随 [jobs seam 收敛]
|
||||
|
||||
**一个通用的非请求输入队列**并带优先级通道,正如 Claude Code 用来把后台任务、cron、MCP 推送与 hook 合并进同一次排空。DSH 的 inbox 本身就是那个队列——`next-turn`/`next-step` 之上的持久 `agent/inbox/spliced` splice——因此这等于在既有层之上再加一层,只为决定一个 bit。
|
||||
|
||||
**拒绝重开一个已经产出可见答复的轮次**,即 Codex 的 `MailboxDeliveryPhase` 闩锁。那条闩锁正是本决策刻意反转的默认值:在模型已经说完话之后唤醒它就是本特性的全部意义,界由唤醒预算来承担。
|
||||
**拒绝重开一个已经产出可见答复的轮次**,即 Codex 的 `MailboxDeliveryPhase` 闩锁。那条闩锁正是本决策刻意反转的默认值:在模型已经说完话之后唤醒它就是本特性的全部意义,界由可选的唤醒预算来承担。
|
||||
|
||||
**在计数之上再加墙钟窗口**。对交互式 agent 而言,慢的那种情形恰恰是想要的——一小时的构建结束、agent 接着干下去,这就是特性本身——而 `dsh run` 已被它无法补充的计数封顶。只有当出现无人值守的长生命周期部署时才值得重新考虑。
|
||||
**在计数之上再加墙钟窗口**。对交互式 agent 而言,慢的那种情形恰恰是想要的——一小时的构建结束、agent 接着干下去,这就是特性本身——而 `dsh run` 无论如何都在任务轮次空闲后退出。只有当出现无人值守的长生命周期部署时才值得重新考虑。
|
||||
|
||||
**在 owner 排空期间整体压制 `onJobDone`**,与服务级的 `listenersClosed` 对称。它读起来更干净,但会移走一个不只服务于通知的信号:强制失败记录与运行时不变量都会观察 teardown 结算。`reported` 位恰好只否决报告方,别的什么也不否决。
|
||||
|
||||
## 影响
|
||||
|
||||
- 默认行为改变:空闲所有者现在每次完成会花掉一次模型请求,按所有者、在两次用户消息之间由 `maxConsecutiveWakes` 封顶。想要旧行为的部署设置 `completionDelivery: quiet`。
|
||||
- 默认行为改变:空闲所有者现在每次完成会花掉一次模型请求,只有设置了 `maxConsecutiveWakes` 时才按所有者、在两次用户消息之间封顶。想要旧行为的部署设置 `completionDelivery: quiet`。
|
||||
- `tool-jobs` 的提示词段落无需改动;「任务完成时你会在会话内收到通知」从愿景变成了事实。
|
||||
- `JobSnapshot.reported` 新增 teardown 作为第四个置位方,记录在 Service Definition 与[子系统参考](../../../../docs/subsystems/jobs.zh.md)中。
|
||||
- `settle()` 在提交记录并发布可见集变更之后才宣布完成。任何依赖「在释放等待方之前或在 `onJobsChanged` 之前运行」的监听器现在都排在两者之后。
|
||||
@@ -69,7 +69,7 @@ Update:本文依赖的 `reported` 位与 `onJobDone` 已随 [jobs seam 收敛]
|
||||
|
||||
### 已接受的风险
|
||||
|
||||
已花掉的预算只由用户输入恢复。耗尽预算的无人值守 agent 要等到其他原因开启轮次时才收走剩余通知,在此期间没有任何机制为它重新充能。
|
||||
在设置了 `maxConsecutiveWakes` 的情况下,已花掉的预算只由用户输入恢复。耗尽预算的无人值守 agent 要等到其他原因开启轮次时才收走剩余通知,在此期间没有任何机制为它重新充能,通知等待时客户端也毫无显示。这正是上限没有默认值的原因。
|
||||
|
||||
在 `quiet` 下待领于空闲所有者的通知仍会随该所有者释放而消亡,与此前一致:释放时的取消会清空未领取的 inbox,日志保留插入/取消这一对作为记录。[结算交付 note](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md) 承载这需要的离线信箱讨论。
|
||||
|
||||
|
||||
@@ -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/feature/2026-08-26-generic-file-upload.md
|
||||
2026-08-26-generic-file-upload.md: ddaf92a396ade59d65a81300531be9bce2aeeaba
|
||||
2026-08-26-generic-file-upload.zh.md: 27a73536ec3632f40ee49a59d932406e77b5591d
|
||||
2026-08-26-generic-file-upload.md: 196d048da9ffc1342cd62cb9c25674b7cecaffac
|
||||
2026-08-26-generic-file-upload.zh.md: fce7cef76cf36060f55165d009c568480abb0f16
|
||||
@@ -52,3 +52,5 @@ Unit and integration coverage pins verbatim and streamed storage, name sanitizat
|
||||
## Related
|
||||
|
||||
The [Trajectory attachment presentation decision](../bug-fix/2026-09-15-trajectory-attachment-presentation.md) owns ledger counts and inspector attachment layouts.
|
||||
|
||||
The [input echo admission decision](../bug-fix/2026-09-22-input-echo-admission-ownership.md) owns placement-specific retirement and delayed Inbox suppression for submissions with or without attachments.
|
||||
@@ -52,3 +52,5 @@ Status: implemented
|
||||
## 相关记录
|
||||
|
||||
[Trajectory 附件展示决定](../bug-fix/2026-09-15-trajectory-attachment-presentation.zh.md)负责列表计数与详情面板附件布局。
|
||||
|
||||
[输入回显入档决策](../bug-fix/2026-09-22-input-echo-admission-ownership.zh.md)负责有无附件提交的按展示归属回收和迟到 Inbox 排除。
|
||||
@@ -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/feature/2026-09-14-composer-selection-keyboard.md
|
||||
2026-09-14-composer-selection-keyboard.md: fb353a7072e7c2245aeeb22af9018edd9a6009f0
|
||||
2026-09-14-composer-selection-keyboard.zh.md: f51e5ca5096785ae6d059e3c8497292483490437
|
||||
2026-09-14-composer-selection-keyboard.md: 675e7239108604c51f4e9a82b98e164833ff9173
|
||||
2026-09-14-composer-selection-keyboard.zh.md: 2b296a609cdd76a407fd4ddcc5855eef2fcfa047
|
||||
@@ -20,6 +20,8 @@ The model seat's dropdown (`ModelSelect`, whose Effort row opens the reasoning-l
|
||||
|
||||
`ModelSelect` keeps `↑`/`↓` as its focus walk over the rows of the shown pane (wrapping; a step taken while the trigger still holds focus enters at the near end). Tab settles: it activates the row the keyboard is on, and with focus still on the trigger it enters the menu at the row of the value in use instead. Escape and Shift+Tab leave a drilled pane first and otherwise close back to the trigger. A pane switch replaces the row that had focus, so each switch names where the keyboard lands: drilling focuses the row of the value in use (its checked row, or the first row when none is marked), and returning to the root pane focuses the cell that opened the pane left. Without that handoff the focus the unmounted row left on the page body sits outside the card's subtree, where its key handling never sees a keystroke.
|
||||
|
||||
Mouse presses on the `ModelSelect` trigger or portaled menu buttons prevent the browser’s default focus transfer: WebKit can otherwise blur the focused row and close the menu before `click`. Native `click` still owns selection and cancellation. Opening the menu or closing it through the trigger puts focus on the trigger. Selection requests return focus there before disabling the option rows, so a rejected request leaves keyboard navigation available.
|
||||
|
||||
`Menu`, the shared anchored dropdown behind the composer's permission seat, the preset chip, file cards, and the settings rows, carries the same pairing: while its list is open, Tab settles the focused row — from the anchor's button, Tab enters the list instead — and Escape or Shift+Tab close it and return focus to the anchor's first button. Only a keyboard already on the anchor or inside the list is intercepted, so Tab presses elsewhere on the page keep the browser's traversal even while a menu is open. The row's focus fill is the row's own indication: the browser's default ring would double it, and the fill is the same one the row shows under the pointer, so the visual language stays one per state. Selecting a row hands the keyboard back to the anchor unless the owner moved it itself — a card that focuses its own preview keeps it. The arrows, Home, and End walk the list whether or not `autoFocus` is set — that option now decides only whether opening focuses the first row — the row the keyboard is on carries the same fill a hovered row gets, and closing on Escape or Shift+Tab returns focus to the anchor whenever the menu held it.
|
||||
|
||||
A dismissed menu stays dismissed: `InputTriggerController` records the identity of the hit the user closed — Escape or Shift+Tab, a pointer dismissal, or a settling pick — and a re-track of that same token with the same query leaves the menu closed, so restoring the caret after a dismissal (or a settled command closing the surface it opened) cannot revive it. A new query or another token re-arms it.
|
||||
@@ -42,7 +44,7 @@ The row contracts are unchanged. Producers already mark their current row `activ
|
||||
|
||||
## Verification
|
||||
|
||||
[Seat browser evidence](../../../../apps/web/tests/declared-reasoning.e2e.ts) drives a real Chromium against the shipped bundles: it opens the drilled level pane, walks it with `↑`, settles a level with Tab and asserts the saved effort, then reopens the pane and asserts the keyboard lands on that level rather than the top of the list before Shift+Tab walks back to the drilled cell and closes. [Seat keyboard tests](../../../../packages/client/ui-model-selection/tests/model-select.client.spec.tsx) cover the drilled-pane handoff onto the value in use (and onto the first row when no row is marked), the handoff back onto the cell, the `↑`/`↓` walk with both wraps, Tab settling a focused row and entering the menu from the trigger, Shift+Tab leaving a pane then closing, and Tab staying native while the menu is closed. [Popup view tests](../../../../packages/client/ui-commands/tests/popup-view.client.spec.tsx) cover Tab settling the parked highlight with consumption and refocus, Shift+Tab dismissing without settling, Tab consumed while rows load without leaving the card, the arrows staying distinct from the native left/right caret, and the wrap on `↑`/`↓`. [Trigger tests](../../../../packages/client/ui-input-trigger/tests/service.client.spec.ts) cover a dismissal sticking to its hit (Escape, a pointer dismissal, and a settling pick), the same hit re-tracking closed, and a changed query re-arming the menu. [Menu tests](../../../../packages/client/ui-primitives/tests/atoms.client.spec.tsx) cover the arrow walk without `autoFocus` (entering at the near end, wrapping, and stepping over a disabled row), Escape returning focus to the anchor, Tab settling the focused row, Shift+Tab closing back to the anchor, Tab entering the list from the anchor, and an unrelated Tab staying native while the list is open. [Popup controller tests](../../../../packages/client/ui-commands/tests/popup.client.spec.ts) cover the parked highlight, its top-row fallback without a marked row, the re-park inside a search retained across a retry, and the rebase to the top row on typing. The popup panel's Tab has unit coverage only: no Web scenario drives `/model` or `/permission` yet.
|
||||
[Seat browser evidence](../../../../apps/web/tests/declared-reasoning.e2e.ts) drives real Chromium and WebKit against the shipped bundles: both verify native mouse selection, cancellation, focus after rejected requests, and keyboard navigation. Each opens the drilled level pane, walks it with `↑`, settles a level with Tab and asserts the saved effort, then reopens the pane and asserts the keyboard lands on that level rather than the top of the list before Shift+Tab walks back to the drilled cell and closes. [Seat keyboard tests](../../../../packages/client/ui-model-selection/tests/model-select.client.spec.tsx) cover the drilled-pane handoff onto the value in use (and onto the first row when no row is marked), the handoff back onto the cell, the `↑`/`↓` walk with both wraps, Tab settling a focused row and entering the menu from the trigger, Shift+Tab leaving a pane then closing, and Tab staying native while the menu is closed. [Popup view tests](../../../../packages/client/ui-commands/tests/popup-view.client.spec.tsx) cover Tab settling the parked highlight with consumption and refocus, Shift+Tab dismissing without settling, Tab consumed while rows load without leaving the card, the arrows staying distinct from the native left/right caret, and the wrap on `↑`/`↓`. [Trigger tests](../../../../packages/client/ui-input-trigger/tests/service.client.spec.ts) cover a dismissal sticking to its hit (Escape, a pointer dismissal, and a settling pick), the same hit re-tracking closed, and a changed query re-arming the menu. [Menu tests](../../../../packages/client/ui-primitives/tests/atoms.client.spec.tsx) cover the arrow walk without `autoFocus` (entering at the near end, wrapping, and stepping over a disabled row), Escape returning focus to the anchor, Tab settling the focused row, Shift+Tab closing back to the anchor, Tab entering the list from the anchor, and an unrelated Tab staying native while the list is open. [Popup controller tests](../../../../packages/client/ui-commands/tests/popup.client.spec.ts) cover the parked highlight, its top-row fallback without a marked row, the re-park inside a search retained across a retry, and the rebase to the top row on typing. The popup panel's Tab has unit coverage only: no Web scenario drives `/model` or `/permission` yet.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ composer 内有两个选择界面在打开期间持有焦点,两者都把 Tab
|
||||
|
||||
`ModelSelect` 的 `↑`/`↓` 仍是在所显示面板行间移动焦点(循环;焦点仍在触发器上时从近端进入)。Tab 执行接受:它激活键盘所在的那一行;焦点仍在触发器上时,改为进入菜单并把焦点放在正在使用的那一行。Escape 与 Shift+Tab 都先退出已下钻的面板,否则关闭并回到触发器。面板切换会替换持有焦点的那一行,因此每次切换都指明键盘的落点:下钻落在正在使用的值那一行(其选中行;没有标记行时为首行),返回根面板则落在打开该面板的那个格子上。没有这一步,已卸载的行留在页面 body 上的焦点就位于卡片自身子树之外,挂在那里的按键处理永远收不到键盘事件。
|
||||
|
||||
在 `ModelSelect` 触发按钮或 portal 菜单按钮上按下鼠标时,阻止浏览器的默认焦点转移:否则 WebKit 可能先让聚焦行失焦,并在 `click` 前关闭菜单。选择和取消仍由原生 `click` 决定。打开菜单或通过触发按钮关闭菜单时,焦点落在触发按钮上。选择请求在禁用选项行前将焦点交回该按钮,因此请求被拒绝后仍可使用键盘导航。
|
||||
|
||||
`Menu`——权限档位、预设芯片、文件卡与设置行背后的共享锚定下拉——沿用同一组配对:列表打开期间,Tab 选定聚焦行(键盘在锚点按钮上时,Tab 改为进入列表),Escape 或 Shift+Tab 关闭并把焦点还给锚点的第一个按钮。只拦截已经位于锚点或列表内的键盘,因此页面其它位置按下的 Tab 即使在菜单打开时也仍归浏览器。行的焦点填充就是行自己的焦点提示:浏览器默认轮廓会与之叠加,而这一填充与指针悬停时用的是同一个,视觉语言因此一态一义。选定一行会把键盘还给锚点——除非拥有者自己移动了焦点(例如文件卡把自己的预览按钮设为焦点)。`↑`/`↓` 与 Home/End 无论是否设置 `autoFocus` 都在列表中走位——该选项如今只决定打开时是否聚焦首行——键盘所在的那一行使用与悬停行相同的填充(否则键盘走位看不出当前行),而 Escape 或 Shift+Tab 关闭时,只要键盘原本在菜单里就把焦点归还锚点。
|
||||
|
||||
被关闭的菜单保持关闭:`InputTriggerController` 记下用户关闭的那个命中(Escape/Shift+Tab、指针关闭或一次落定选定)的身份,同一 token 以同一查询重新 track 时菜单保持关闭,因此关闭后恢复光标、或已选定命令关闭它打开的界面,都无法把它召回来;新的查询或另一个 token 才会重新武装。
|
||||
@@ -42,7 +44,7 @@ composer 内有两个选择界面在打开期间持有焦点,两者都把 Tab
|
||||
|
||||
## 验证
|
||||
|
||||
[模型位浏览器证据](../../../../apps/web/tests/declared-reasoning.e2e.ts) 用真实 Chromium 驱动已发布的 bundle:打开已下钻的等级面板,用 `↑` 走位,用 Tab 选定一档并断言保存下来的推理等级,随后重新打开该面板、断言键盘落在那一档而不是列表顶部,再用 Shift+Tab 退回被下钻的格子并关闭。[模型位键盘测试](../../../../packages/client/ui-model-selection/tests/model-select.client.spec.tsx) 覆盖下钻交接落在正在使用的值上(没有标记行时落在首行)、交回给该格子、`↑`/`↓` 的双向循环走位、Tab 选定聚焦行以及从触发器进入菜单、Shift+Tab 先退层再关闭,以及菜单关闭时 Tab 保持原生。[弹窗视图测试](../../../../packages/client/ui-commands/tests/popup-view.client.spec.tsx) 覆盖 Tab 接受停放的高亮并完成消费与焦点归还、Shift+Tab 关闭且不接受、行仍在加载时 Tab 被消费且焦点不离开卡片、方向键与原生左右光标互不干扰,以及 `↑`/`↓` 的循环。[触发流水线测试](../../../../packages/client/ui-input-trigger/tests/service.client.spec.ts) 覆盖关闭粘在它自己的命中上(Escape、指针关闭、落定选定三种)、同一命中重新 track 时保持关闭,以及查询改变后菜单重新武装。[Menu 测试](../../../../packages/client/ui-primitives/tests/atoms.client.spec.tsx) 覆盖不带 `autoFocus` 的方向键走位(从近端进入、两端循环、跳过禁用行)、Escape 把焦点还给锚点、Tab 选定聚焦行、Shift+Tab 关闭并把焦点还给锚点、Tab 从锚点进入列表,以及列表打开期间无关位置的 Tab 保持原生。[弹窗控制器测试](../../../../packages/client/ui-commands/tests/popup.client.spec.ts) 覆盖高亮的停放、没有标记行时退回首行、重试时在保留的搜索内重新停放,以及输入时重定到首行。弹窗面板的 Tab 只有单元覆盖:目前还没有 Web 场景驱动 `/model` 或 `/permission`。
|
||||
[模型位浏览器证据](../../../../apps/web/tests/declared-reasoning.e2e.ts) 用真实 Chromium 与 WebKit 驱动已发布的 bundle:两者都验证原生鼠标选择、取消、请求被拒绝后的焦点以及键盘导航。每种引擎都会打开已下钻的等级面板,用 `↑` 走位,用 Tab 选定一档并断言保存下来的推理等级,随后重新打开该面板、断言键盘落在那一档而不是列表顶部,再用 Shift+Tab 退回被下钻的格子并关闭。[模型位键盘测试](../../../../packages/client/ui-model-selection/tests/model-select.client.spec.tsx) 覆盖下钻交接落在正在使用的值上(没有标记行时落在首行)、交回给该格子、`↑`/`↓` 的双向循环走位、Tab 选定聚焦行以及从触发器进入菜单、Shift+Tab 先退层再关闭,以及菜单关闭时 Tab 保持原生。[弹窗视图测试](../../../../packages/client/ui-commands/tests/popup-view.client.spec.tsx) 覆盖 Tab 接受停放的高亮并完成消费与焦点归还、Shift+Tab 关闭且不接受、行仍在加载时 Tab 被消费且焦点不离开卡片、方向键与原生左右光标互不干扰,以及 `↑`/`↓` 的循环。[触发流水线测试](../../../../packages/client/ui-input-trigger/tests/service.client.spec.ts) 覆盖关闭粘在它自己的命中上(Escape、指针关闭、落定选定三种)、同一命中重新 track 时保持关闭,以及查询改变后菜单重新武装。[Menu 测试](../../../../packages/client/ui-primitives/tests/atoms.client.spec.tsx) 覆盖不带 `autoFocus` 的方向键走位(从近端进入、两端循环、跳过禁用行)、Escape 把焦点还给锚点、Tab 选定聚焦行、Shift+Tab 关闭并把焦点还给锚点、Tab 从锚点进入列表,以及列表打开期间无关位置的 Tab 保持原生。[弹窗控制器测试](../../../../packages/client/ui-commands/tests/popup.client.spec.ts) 覆盖高亮的停放、没有标记行时退回首行、重试时在保留的搜索内重新停放,以及输入时重定到首行。弹窗面板的 Tab 只有单元覆盖:目前还没有 Web 场景驱动 `/model` 或 `/permission`。
|
||||
|
||||
## 影响
|
||||
|
||||
|
||||
@@ -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/feature/2026-09-16-browser-excel-preview.md
|
||||
2026-09-16-browser-excel-preview.md: 208bcdcd69d8ac30fcb5a7776271942424a92a05
|
||||
2026-09-16-browser-excel-preview.zh.md: c48d9156e44befaa265d5c1f4116d86efe099fda
|
||||
2026-09-16-browser-excel-preview.md: 15db888330b2a1a6d6d78dad46689962466d531d
|
||||
2026-09-16-browser-excel-preview.zh.md: 6b3a6a617a0cd18dcd3da1682a31f26bd7e70386
|
||||
@@ -8,12 +8,16 @@ English | [中文](2026-09-16-browser-excel-preview.zh.md)
|
||||
|
||||
PDF conversion loses spreadsheet navigation and the relationship between a formula and its saved value. Spreadsheet preview also needlessly depends on the Host Office engine when the browser can read the workbook.
|
||||
|
||||
Unsupported drawing content can also prevent cell preview. ExcelJS 4.4.0 expects prefixed DrawingML element names and fails on valid default-namespace drawings emitted by Openpyxl. A chart that the viewer cannot display must not make otherwise readable worksheets unavailable.
|
||||
|
||||
## Decision
|
||||
|
||||
The [document preview](../../../../packages/client/ui-sidebar-documentpreview/README.md#excel-preview) opens XLSX, XLS, CSV, and TSV with FortuneSheet and package-local adapters for ExcelJS, SheetJS CE, and PapaParse. The adapter accepts bytes, format, and limits and returns worksheet data; it has no React state setters, timers, or component references. Dependencies own file parsing, while the adapters share display formatting and initial selection without introducing another workbook model. CSV/TSV retain literal strings and a plain-text option; implicit type inference could otherwise discard leading zeros or interpret source text as formulas. The shared document reader calls `remote.workspaceFiles.readBytes(sessionId, path, {}, signal)` and receives native bytes through the binary Remote, retaining Session authorization, file versions, reload, and transient bytes without base64 decoding. Workspace Files owns bounded file reads; format parsing stays in the Client. The registration marks only XLSX/XLS as `binaryExtensions`, so the shared document container offers Plain text for CSV/TSV. Each renderer registers its body under its own id.
|
||||
|
||||
Preview is read-only and retains formula text alongside saved results. It does not recalculate, so unsupported functions cannot replace a valid saved result with an error. A formula without a saved result remains blank. Workbooks containing formulas show a compact warning beside the formula icon instead of consuming worksheet height with a notice row. The spreadsheet is neither an authoring tool nor a full Excel rendering engine.
|
||||
|
||||
The package-local `XlsxPreviewArchive` inspects XLSX content inside the parser Worker and removes DrawingML parts and their relationship parts from a temporary ZIP. ExcelJS also ignores worksheet drawing references; ignoring those references alone still leaves its archive-level drawing parser exposed to the failure. The source bytes and retained worksheet XML remain unchanged. A separate notice lists detected charts, images, shapes, and conditional formatting that the preview does not display and recommends a system application. Formula warnings remain independent, and files without detected unsupported content have no notice. Other formats return no unsupported-content categories.
|
||||
|
||||
A disposable browser Worker contains parser CPU work. A source-byte limit precedes parsing, a combined rectangular-area limit precedes FortuneSheet matrix allocation, and a timeout terminates stalled parsing. These limits do not claim a hard memory sandbox. Styles are scoped to the renderer; external hyperlinks are text only.
|
||||
|
||||
The pinned FortuneSheet React dependency carries a [patch](../../../../patches/@[email protected]) that skips the input box's read-only layout state update unless formula-reference highlighting is enabled. The unconditional update otherwise causes a maximum-update-depth failure when switching worksheets under the shipped React 18.3.1 runtime. Sheet activation also retains the adapter's complete initial selection instead of clearing it and leaving an empty or invalid address. The browser scenario covers these failures through the bundled ESM entry.
|
||||
@@ -28,8 +32,14 @@ The ESM and CommonJS entries receive the same patches. Recheck worksheet switchi
|
||||
|
||||
**Write an OOXML parser or create a new public package.** ExcelJS already owns the container and file semantics. One preview consumer does not justify another public API, Cordis service, or generic workbook abstraction; a second independent consumer can motivate extraction.
|
||||
|
||||
**Patch ExcelJS's drawing namespace handling.** Correct parsing would still leave drawings undisplayed. Filtering the unsupported parts prevents them from blocking the supported cell preview without maintaining drawing behavior that the viewer cannot use.
|
||||
|
||||
**Replace ExcelJS or require users to rewrite their files.** A parser replacement puts retained fonts, borders, merges, and frozen panes at risk. Rewriting a source file makes a viewer defect the user's responsibility. A temporary preview copy preserves both the existing adapter and the user's file.
|
||||
|
||||
**Keep PDF as an Excel fallback or calculate every formula on import.** PDF retains neither spreadsheet interaction nor formula inspection, while recalculation can change saved results. The [Office engine decision](../architecture/2026-09-11-node-office-kit.md) remains applicable to Word/PowerPoint previews and independent conversion consumers; its conversion capability still supports spreadsheets.
|
||||
|
||||
## Consequences
|
||||
|
||||
The renderer trades complete Excel fidelity for browser-local navigation and reusable conversion code. The package README owns supported formats, resource defaults, and unsupported features. Real XLSX/XLS fixtures and delimited-text cases cover conversion, Worker tests cover cancellation and cleanup, and the shipped-profile browser scenario opens a workbook with Office conversion disabled and copies a saved XLOOKUP result under read-only settings. It also exercises XLS sheet switching and CSV/TSV literal copying, text-view switching, and reload. SheetJS CE is pinned to its official release tarball because the public npm registry is stale; its Apache-2.0 license follows the existing permissive-license and bundled-notice policy.
|
||||
|
||||
Drawing filtering adds ZIP inspection and, when parts are omitted, temporary uncompressed ZIP allocation. Existing Worker lifetime and source/matrix limits still apply; they do not bound decompressed memory. Drawing fixtures cover default and alternate prefixes, exact retained XML and input bytes, images, shapes, and comment VML. The browser scenario checks the notice, saved-value copying, and notice removal on file replacement. Adding drawing rendering requires revisiting both filtering and notice classification; a namespace fix alone does not restore that capability.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note:显示已保存公式结果的浏览器 Excel 预览
|
||||
# Agent Note: 显示已保存公式结果的浏览器 Excel 预览
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -8,12 +8,16 @@ Status: implemented
|
||||
|
||||
PDF 转换会丢失工作表导航以及公式与已保存值之间的关系。浏览器能够读取工作簿时,表格预览也没有必要依赖 Host Office 引擎。
|
||||
|
||||
不支持的绘图内容也可能阻断单元格预览。ExcelJS 4.4.0 要求 DrawingML 元素名带前缀,遇到 Openpyxl 生成的合法默认命名空间绘图时会失败。查看器无法展示的图表不应使原本可读的工作表无法打开。
|
||||
|
||||
## 决策
|
||||
|
||||
[文档预览](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md#excel-preview)使用 FortuneSheet 与包内 ExcelJS、SheetJS CE 和 PapaParse 适配层打开 XLSX、XLS、CSV 和 TSV。适配层接收字节、格式和限制并返回工作表数据,不包含 React 状态设置器、计时器或组件引用。依赖库负责文件解析,适配层复用显示格式化和初始选区,不引入另一种工作簿模型。CSV/TSV 保留字面字符串和纯文本选项;隐式类型推断可能丢失前导零或把源文本解释为公式。共享文档读取器调用 `remote.workspaceFiles.readBytes(sessionId, path, {}, signal)`,并通过二进制 Remote 直接接收原生字节,保留 Session 授权、文件版本、重新载入和临时字节,无需 base64 解码。Workspace Files 负责有界文件读取,格式解析留在 Client。注册仅将 XLSX/XLS 标为 `binaryExtensions`,共享文档容器因此为 CSV/TSV 提供纯文本选项。每个渲染器以自己的 id 注册正文。
|
||||
|
||||
预览为只读,同时保留公式文本与已保存结果。它不重新计算,因此不受支持的函数不会用错误覆盖有效的已保存结果。没有已保存结果的公式保持为空。包含公式的工作簿会在公式图标旁显示紧凑提示,不再用整行提示占用工作表高度。该表格既不是编写工具,也不是完整的 Excel 渲染引擎。
|
||||
|
||||
包内 `XlsxPreviewArchive` 在解析 Worker 中检查 XLSX 内容,从临时 ZIP 中移除 DrawingML 部件及其关系部件。ExcelJS 同时忽略工作表绘图引用;仅忽略这些引用仍会让其归档级绘图解析器触发该故障。源字节和保留的工作表 XML 均不改变。独立提示列出实际检测到但预览未展示的图表、图片、形状和条件格式,并建议使用系统应用。公式提示保持独立,未检测到不支持内容的文件不显示此提示。其他格式不返回不支持内容分类。
|
||||
|
||||
独立可释放的浏览器 Worker 承担解析 CPU 工作。解析前检查源文件字节限制,FortuneSheet 分配矩阵前检查矩形区域合计限制,并通过超时终止停滞的解析。这些限制不代表严格的内存沙箱。样式限定在渲染器区域内,外部超链接仅显示为文本。
|
||||
|
||||
固定版本的 FortuneSheet React 依赖带有一个[补丁](../../../../patches/@[email protected]):只读输入框仅在启用公式引用高亮时才提交布局状态更新。无条件更新会在仓库交付的 React 18.3.1 运行时下切换工作表时触发最大更新深度错误。激活工作表时还会保留适配层提供的完整初始选区,避免清空后留下空白或无效地址。浏览器场景通过打包后的 ESM 入口覆盖这些故障。
|
||||
@@ -28,8 +32,14 @@ ESM 和 CommonJS 入口应用相同补丁。升级 FortuneSheet 时需要重新
|
||||
|
||||
**自行编写 OOXML 解析器或创建新的公共包。** ExcelJS 已经负责容器和文件语义。单个预览使用方不足以支持另一套公共 API、Cordis 服务或通用工作簿抽象;出现第二个独立使用方时再考虑提取。
|
||||
|
||||
**修补 ExcelJS 的绘图命名空间处理。** 正确解析仍不能使查看器展示绘图。过滤不支持的部件可防止其阻断已支持的单元格预览,无需维护查看器无法使用的绘图行为。
|
||||
|
||||
**替换 ExcelJS 或要求用户重写文件。** 更换解析器会使现有字体、边框、合并及冻结窗格的保留面临风险。重写源文件则把查看器缺陷交给用户处理。临时预览副本同时保留现有适配器和用户文件。
|
||||
|
||||
**保留 PDF 作为 Excel 兜底,或在导入时计算所有公式。** PDF 无法保留表格交互与公式查看,而重新计算可能改变已保存结果。[Office 引擎决策](../architecture/2026-09-11-node-office-kit.zh.md)仍适用于 Word/PowerPoint 预览与独立转换使用方;转换能力仍支持表格。
|
||||
|
||||
## 影响
|
||||
|
||||
渲染器用部分 Excel 保真度换取浏览器内导航和可复用的转换代码。包 README 负责说明支持的格式、资源默认值和不支持的功能。真实 XLSX/XLS fixture 和分隔文本用例覆盖转换,Worker 测试覆盖取消和清理,发布 profile 的浏览器场景在禁用 Office 转换后打开工作簿,并在只读设置下复制已保存的 XLOOKUP 结果。该场景还验证 XLS 工作表切换,以及 CSV/TSV 字面值复制、纯文本切换和重新载入。由于公共 npm 仓库版本过旧,SheetJS CE 固定使用官方发布的 tarball;其 Apache-2.0 许可证沿用现有宽松许可证及打包声明规则。
|
||||
|
||||
绘图过滤增加 ZIP 检查,并在移除部件时分配临时未压缩 ZIP。现有 Worker 生命周期和源文件/矩阵限制仍然适用,但不约束解压后的内存。绘图 fixture 覆盖默认及替代前缀、保留 XML 和输入字节的逐字节一致性、图片、形状及批注 VML。浏览器场景检查提示、已保存值的复制,以及文件替换后的提示移除。增加绘图渲染时,必须同时重新审视过滤和提示分类;仅修复命名空间不能恢复该能力。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/feature/2026-09-22-unbounded-completion-wakes-by-default.md
|
||||
2026-09-22-unbounded-completion-wakes-by-default.md: 8760154054de423240e16d93e3db5ae01521820a
|
||||
2026-09-22-unbounded-completion-wakes-by-default.zh.md: e5560bc7ced5a8ca9ab55ea56ba309676d78b568
|
||||
@@ -0,0 +1,36 @@
|
||||
# Agent Note: Background job completion wakes are unbounded by default
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-22-unbounded-completion-wakes-by-default.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [idle-owner wake decision](2026-08-11-background-job-completion-wakes-an-idle-owner.md) shipped `maxConsecutiveWakes` with a default of 3: an idle owner could be woken three times by job completions before further notices degraded to injection, and only a claimed user message refilled the budget. The counter was per owner and shared by every job kind — background `bash`/`pwsh`/PTY commands and one-shot `subagent_*` children.
|
||||
|
||||
A session that chained more than three of them without a user message hit the cap (issue #4944, reported as [dsh-external/issues#641](https://github.com/dsh-external/issues/issues/641)). The fourth notice was injected into the next-step inbox of an owner nothing would wake; the client renders no pending `context` message, so the session sat with a notice it could not see. The `tool-jobs` prompt promises the model "you are notified in-session when a job finishes — do not busy-poll", which encourages exactly the start-and-end-turn pattern that exhausts the cap. The stall arrived when the user was away and looked like a hang until they typed something.
|
||||
|
||||
## Decision
|
||||
|
||||
`maxConsecutiveWakes` has no default. Unset, every completion reaching an idle owner under `wakeup` delivery opens a turn. The `agent/inbox/claimed` refill listener and the per-owner ledger exist only when a cap is configured; a configured value must still be a whole number of turns, and `Infinity` is rejected because omitting the field already means unbounded.
|
||||
|
||||
The self-exciting chain the cap was built for — a woken turn starting the job whose completion wakes it again — is still real, and deployments that want the bound set the field. The trade-off is that the chain spends model requests the user can watch and stop, while a notice parked past a cap is invisible: nothing in the client shows it, nothing re-arms the budget, and the model's promise becomes false. An observable cost beats an unobservable stall.
|
||||
|
||||
`dsh run` is unaffected: headless exits once the task turn goes idle, so there is no idle owner left to wake.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the default cap and make the parked notice visible** — a session-level pending indicator or an unread mark on the jobs list. This keeps the runaway bound but still breaks the prompt's promise: the model ended its turn expecting a notice and receives nothing until the user acts. Visibility is worth adding for a configured cap; it does not justify a default one.
|
||||
|
||||
**Count only self-excited wakes** — a wake counts against the budget only when the woken turn starts a new background job. This bounds the runaway chain without cutting off a chain of independent jobs, but the distinction needs the plugin to correlate job starts with the turn a wake opened, state no current consumer needs. It remains the natural refinement if a configured cap proves too coarse.
|
||||
|
||||
**A cooldown or rate limit instead of a counter.** Time does not distinguish a wanted chain from a runaway one; the wanted case is often the slow one, and a rate limit still parks a notice invisibly when it triggers.
|
||||
|
||||
**Re-check the next-step inbox on every turn-close path** (abort, LLM error, rejected pre-step). This closes the other parking spots the issue lists, but they are reached only by an already-parked notice; with no default cap the reported scenario never parks one. It stays a separate `agent-loop` change.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An unattended session chaining background jobs is woken for every completion; the model request per wake is the cost, and the user can stop the session.
|
||||
- The cap is opt-in configuration. Its README, config-catalog, and JSDoc state the silent-stall cost of setting it.
|
||||
- Unit coverage pins four unattended completions each opening a turn under the default config; the cap, refill, and quiet-delivery tests keep explicit `maxConsecutiveWakes` values.
|
||||
- No recorded-session snapshot changes: no shipped profile sets the field, and no existing scenario chains more than three idle completions.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Agent Note: Background job completion wakes are unbounded by default
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-22-unbounded-completion-wakes-by-default.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
[空闲所有者唤醒决策](2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md)发布时 `maxConsecutiveWakes` 默认为 3:空闲所有者最多被任务完成唤醒三次,此后的通知降级为注入,只有被领取的用户消息才补充预算。计数器按所有者记录,并由所有任务类型共用——后台 `bash`/`pwsh`/PTY 命令与一次性 `subagent_*` 子级。
|
||||
|
||||
一个会话在没有用户消息的情况下串起超过三个这样的任务就会触顶(issue #4944,来源为 [dsh-external/issues#641](https://github.com/dsh-external/issues/issues/641))。第四条通知被注入到一个无人会唤醒的所有者的 next-step inbox;客户端不渲染待领的 `context` 消息,会话就带着一条自己看不见的通知停在那里。`tool-jobs` 的提示词向模型承诺「任务完成会在会话内通知你,不要轮询」,恰恰鼓励了会耗尽上限的「启动后结束本轮」模式。停摆发生在用户离开时,看起来像挂死,直到用户输入点什么。
|
||||
|
||||
## 决策
|
||||
|
||||
`maxConsecutiveWakes` 没有默认值。未设置时,`wakeup` 投递下抵达空闲所有者的每次完成都会开启一轮。`agent/inbox/claimed` 补充监听与按所有者的台账只在配置了上限时存在;配置的值仍必须是整数轮数,`Infinity` 被拒绝,因为省略该字段已经表示无上限。
|
||||
|
||||
上限所针对的自激链——被唤醒的一轮启动某个任务,其完成又唤醒它——依然真实存在,想要该界的部署自行设置字段。取舍在于:这条链花掉的是用户看得见、能停下的模型请求,而停在上限之外的通知是不可见的:客户端不显示,没有机制重新充能预算,模型得到的承诺也成了假话。可观察的代价胜过不可观察的停摆。
|
||||
|
||||
`dsh run` 不受影响:headless 在任务轮次空闲后即退出,没有可唤醒的空闲所有者。
|
||||
|
||||
## 被否决的替代方案
|
||||
|
||||
**保留默认上限,让停驻的通知可见**——会话级的待处理指示或任务列表上的未读标记。这保住了失控约束,但仍然违背提示词的承诺:模型结束本轮等着通知,却要等到用户动手才收到。可见性值得为已配置的上限补上;它不能为默认上限辩护。
|
||||
|
||||
**只统计自激的唤醒**——只有被唤醒的一轮又启动了新的后台任务时才计入预算。这约束了失控链而不切断一串独立任务,但这种区分需要插件把任务启动与唤醒所开启的轮次关联起来,是当前没有消费者需要的状态。若配置的上限被证明过粗,它仍是自然的细化方向。
|
||||
|
||||
**用冷却或速率限制代替计数器。** 时间无法区分想要的链与失控的链;想要的情形往往正是慢的那种,而速率限制触发时同样会把通知不可见地停驻。
|
||||
|
||||
**在每条轮次结束路径上复查 next-step inbox**(abort、LLM 错误、被拒绝的 pre-step)。这关闭了 issue 列出的其他停驻点,但它们只会被一条已停驻的通知触及;没有默认上限时,报告的场景根本不会停驻通知。它仍是独立的 `agent-loop` 改动。
|
||||
|
||||
## 影响
|
||||
|
||||
- 串起后台任务的无人值守会话会为每次完成被唤醒;每次唤醒一次模型请求是其代价,用户可以停止会话。
|
||||
- 上限是可选配置。其 README、config-catalog 与 JSDoc 说明了设置它带来的静默停摆代价。
|
||||
- 单元覆盖钉住默认配置下连续四次无人值守完成各开启一轮;上限、补充与 quiet 投递的测试保留显式的 `maxConsecutiveWakes` 值。
|
||||
- 录制会话快照无变化:没有任何内置 profile 设置该字段,也没有既有场景串起超过三次空闲完成。
|
||||
@@ -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/process/2026-07-26-ci-failover-runbook.md
|
||||
2026-07-26-ci-failover-runbook.md: c2fd2fa6cf673ec4d202702c16a9a85a3a609b8d
|
||||
2026-07-26-ci-failover-runbook.zh.md: 7fa5887bb552afd98cebd7502202fb5e9f37b1ee
|
||||
2026-07-26-ci-failover-runbook.md: d46f9d140860580011e06aa3b51bb8618a23a114
|
||||
2026-07-26-ci-failover-runbook.zh.md: a229d46f16d959356e9108842edd557420e699ae
|
||||
@@ -20,7 +20,7 @@ The [superseded-CI cancellation policy](2026-09-09-cancel-superseded-ci.md) gove
|
||||
|
||||
### What the in-house pool is
|
||||
|
||||
`vm-backup`: one shared VM with multiple always-on systemd-managed runner instances. Registrations share its CPU, memory, and disk; their count is not a count of independent machines. Its image must preinstall Playwright Chromium's Linux system packages; CI downloads the lockfile-selected browser but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite.
|
||||
`vm-backup`: one shared VM with multiple always-on systemd-managed runner instances. Registrations share its CPU, memory, and disk; their count is not a count of independent machines. Its image must preinstall the Linux system packages for Playwright Chromium and WebKit; CI downloads the lockfile-selected browsers but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite.
|
||||
|
||||
#### Windows pool
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Status: implemented
|
||||
|
||||
### 自有池是什么
|
||||
|
||||
`vm-backup`:一台共享虚拟机,运行多个常驻 systemd 管理的运行器实例。注册实例共享 CPU、内存和磁盘;实例数量不代表独立机器数量。其镜像必须预装 Playwright Chromium 的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。
|
||||
`vm-backup`:一台共享虚拟机,运行多个常驻 systemd 管理的运行器实例。注册实例共享 CPU、内存和磁盘;实例数量不代表独立机器数量。其镜像必须预装 Playwright Chromium 和 WebKit 所需的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。
|
||||
|
||||
#### Windows 池
|
||||
|
||||
|
||||
@@ -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/process/2026-08-10-npm-release-sequences.md
|
||||
2026-08-10-npm-release-sequences.md: 6f52c00a31870939981994965b70504e8f8143ca
|
||||
2026-08-10-npm-release-sequences.zh.md: effa5538cefbc8f25b2e047ee40e0dc7306bb2fb
|
||||
2026-08-10-npm-release-sequences.md: 6a4f9d1ec23d1fd1c7b74630e106974b680fe41a
|
||||
2026-08-10-npm-release-sequences.zh.md: d33d48e4458a62951aa957e694dd70eedcf2a6c8
|
||||
@@ -78,9 +78,9 @@ Two registry behaviours shape how a publish is attempted. Writes are spaced by a
|
||||
|
||||
### Workspace-internal references use the `workspace:` protocol
|
||||
|
||||
Every reference to a workspace member uses `workspace:^`, so `pnpm pack` substitutes a range matching the target version: sibling `peerDependencies` follow the family version, and a reference to a vendored package follows that package's own line. The Landlock platform packages keep `workspace:*`, which publishes the exact version, because a platform package and its entry must agree exactly.
|
||||
Every reference to a workspace member uses the `workspace:` protocol. The [release-range policy](2026-09-22-workspace-release-ranges.md) requires exact `workspace:*` DSH references and `workspace:~` vendor/native references in every dependency section and consumer, including the native entry's optional platform packages. Local workspace linking is unchanged.
|
||||
|
||||
`scripts/check-workspace-constraints.ts` requires the protocol, so a new package cannot reintroduce a hand-written range; the invariant-companion rule requires `workspace:^` for `@deepseek-ai/dsh-invariants` for the same reason.
|
||||
`scripts/check-workspace-constraints.ts` reads every member declared in `pnpm-workspace.yaml` plus the root manifest and enforces ranges by dependency target, not consumer directory. The invariant-companion rule requires `workspace:*` for `@deepseek-ai/dsh-invariants`; the dependency repairer preserves vendor/native tilde ranges. Published DSH peers therefore require the matching release instead of admitting later compatible versions.
|
||||
|
||||
### Published dependency faces use an explicit policy
|
||||
|
||||
@@ -133,7 +133,7 @@ The installed-consumer probe captures npm's HTTP diagnostics and includes them w
|
||||
|---|---|
|
||||
| release-set manifests | `private: true` removed; `publishConfig.access` per sequence and `repository` with each package's `directory` added |
|
||||
| release-set boundary | every member of `packages/*/*`, `apps/*`, and `vendor/*` |
|
||||
| dependency protocol | workspace-internal references are `workspace:^`, with `check-workspace-constraints.ts` and the invariant-companion rule requiring it |
|
||||
| dependency protocol | every workspace consumer uses `workspace:*` for DSH targets and `workspace:~` for vendor/native targets |
|
||||
| root `AGENTS.md` | the convention that vendored packages are `private: true` no longer holds |
|
||||
| `vendor/README.md` | records `src` joining `cordis`'s `files` as a local modification |
|
||||
| the three native packages | `publishConfig.access: public`, and their workflow passes no `--access` |
|
||||
|
||||
@@ -78,9 +78,9 @@ registry 的两个行为决定了「怎么尝试一次发布」。写入之间
|
||||
|
||||
### workspace 内部引用走 `workspace:` 协议
|
||||
|
||||
所有指向 workspace 成员的引用都用 `workspace:^`,由 `pnpm pack` 替换成匹配目标版本的范围:兄弟包的 `peerDependencies` 跟随族版本,指向 vendored 包的引用跟随那个包自己的版本线。Landlock 平台包保留 `workspace:*`(发布成精确版本),因为平台包与它的入口必须版本完全一致。
|
||||
所有指向 workspace 成员的引用都用 `workspace:` 协议。[发布范围策略](2026-09-22-workspace-release-ranges.zh.md)要求每个消费者的所有依赖区段对 DSH 使用精确的 `workspace:*` 引用,对 vendor/native 使用 `workspace:~` 引用,包括原生入口的可选平台包。本地 workspace 链接方式不变。
|
||||
|
||||
`scripts/check-workspace-constraints.ts` 要求这个协议,所以新包无法再引入硬写的范围;同理,invariant companion 规则要求 `@deepseek-ai/dsh-invariants` 用 `workspace:^`。
|
||||
`scripts/check-workspace-constraints.ts` 读取 `pnpm-workspace.yaml` 声明的全部成员及根清单,按依赖目标而非消费者目录校验范围。Invariant companion 规则要求 `@deepseek-ai/dsh-invariants` 使用 `workspace:*`;依赖修复器保留 vendor/native tilde 范围。因此,发布的 DSH peer 要求匹配的发布版本,而不接纳后续兼容版本。
|
||||
|
||||
### 发布依赖门面使用显式策略
|
||||
|
||||
@@ -133,7 +133,7 @@ dsh 的验证会一并安装 vendored 族的 pack 产物。harness 的包把 ven
|
||||
|---|---|
|
||||
| 发布集 manifest | 去掉 `private: true`;按序列补 `publishConfig.access` 与带各自 `directory` 的 `repository` |
|
||||
| 发布集边界 | `packages/*/*`、`apps/*`、`vendor/*` 的全部成员 |
|
||||
| 依赖协议 | workspace 内部引用为 `workspace:^`,由 `check-workspace-constraints.ts` 与 invariant companion 规则强制 |
|
||||
| 依赖协议 | 每个 workspace 消费者对 DSH 目标使用 `workspace:*`,对 vendor/native 目标使用 `workspace:~` |
|
||||
| 根 `AGENTS.md` | 「vendored 包是 `private: true`」这条约定不再成立 |
|
||||
| `vendor/README.md` | 记录「`src` 加入 `cordis` 的 `files`」这条本地修改 |
|
||||
| native 三包 | `publishConfig.access: public`,且其 workflow 不传 `--access` |
|
||||
|
||||
@@ -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/process/2026-08-26-published-dependency-faces.md
|
||||
2026-08-26-published-dependency-faces.md: 1a94ccedcf6f614c7853c96c329e88d037167460
|
||||
2026-08-26-published-dependency-faces.zh.md: 4ac3f461018c21047af6359ca5e27d050da5a2c4
|
||||
2026-08-26-published-dependency-faces.md: f188ccc0fc91b2899ccb618d3000a9b3cb94eb59
|
||||
2026-08-26-published-dependency-faces.zh.md: d59bb724ceb15f6eae65b509f8437a5dcfa05a23
|
||||
@@ -28,7 +28,7 @@ A workspace package reached by a runtime value import from the Host entry closur
|
||||
|
||||
An export whose constructor identity or module state must be shared appears in `peerRequiredHostExports`; importing one such export keeps the whole package edge in matching `peerDependencies` and `devDependencies`. Each export-table key is an exact module specifier and each value is a reviewed export set. The verifier follows runtime local imports from the Host entry, records named and default imports and re-exports, and rejects exports covered by neither the package list nor an export table; namespace, dynamic, and side-effect imports remain unbounded unless the complete exact entry is package-classified.
|
||||
|
||||
Workspace imports used by the Client bundle, type-only imports, module augmentations, `dsh.client.inject`, and existing metadata-only peers belong only in `devDependencies`. Host runtime imports, including additional Node entries, follow the Host classification. [Browser third-party build inputs](2026-09-08-browser-third-party-build-inputs.md) governs ordinary third-party declarations; it partially supersedes their preservation in this decision. Workspace references use `workspace:^`.
|
||||
Workspace imports used by the Client bundle, type-only imports, module augmentations, `dsh.client.inject`, and existing metadata-only peers belong only in `devDependencies`. Host runtime imports, including additional Node entries, follow the Host classification. [Browser third-party build inputs](2026-09-08-browser-third-party-build-inputs.md) governs ordinary third-party declarations; it partially supersedes their preservation in this decision. [Release ranges](2026-09-22-workspace-release-ranges.md) require `workspace:*` for DSH and `workspace:~` for vendor/native references.
|
||||
|
||||
Some development relationships exist only in `dsh.client.inject` or TypeScript project references. The policy's `configurationOnlyDevDependencies` table names only those reviewed edges and keeps them in `devDependencies`.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ Host 入口闭包中的运行期 value import 所到达的 workspace 包,只
|
||||
|
||||
constructor 身份或模块状态必须共享的导出列入 `peerRequiredHostExports`;一旦使用这类导出,整条包依赖边就保留在范围一致的 `peerDependencies` 与 `devDependencies` 中。每个导出表的 key 都是精确 module specifier,每个 value 都是经审查的导出集合。验证器从 Host 入口沿运行期本地 import 扫描,记录具名与默认 import 和 re-export,并拒绝既没有包级分类、也没有导出级分类的导出;除非完整的精确入口已按包分类,否则 namespace、dynamic 和 side-effect import 仍无法限定范围。
|
||||
|
||||
Client bundle 使用的 workspace import、纯类型 import、模块扩充、`dsh.client.inject` 和仅有元数据的现存 peer 只属于 `devDependencies`。Host 运行时 import(包括额外 Node 入口)遵循 Host 分类。[浏览器第三方构建输入](2026-09-08-browser-third-party-build-inputs.zh.md)规定普通第三方声明,部分取代本决策对它们原区段的保留。Workspace 引用使用 `workspace:^`。
|
||||
Client bundle 使用的 workspace import、纯类型 import、模块扩充、`dsh.client.inject` 和仅有元数据的现存 peer 只属于 `devDependencies`。Host 运行时 import(包括额外 Node 入口)遵循 Host 分类。[浏览器第三方构建输入](2026-09-08-browser-third-party-build-inputs.zh.md)规定普通第三方声明,部分取代本决策对它们原区段的保留。[发布范围](2026-09-22-workspace-release-ranges.zh.md)要求 DSH 引用使用 `workspace:*`,vendor/native 引用使用 `workspace:~`。
|
||||
|
||||
部分开发期关系只存在于 `dsh.client.inject` 或 TypeScript project reference 中。策略的 `configurationOnlyDevDependencies` 表只列出这些已评审的依赖边,并将它们保留在 `devDependencies` 中。
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/process/2026-09-22-workspace-release-ranges.md
|
||||
2026-09-22-workspace-release-ranges.md: 57c74df3eaeac16ba40f55cdc4069762ae8fe2ef
|
||||
2026-09-22-workspace-release-ranges.zh.md: f39ede4fb1941956af12d0391f5b88bd244db6f8
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Workspace dependency release ranges
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-22-workspace-release-ranges.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
DSH packages share one product release. Cordis, its vendored libraries, and Node Addon System have independent releases; consumers need their patch updates without automatically accepting a new minor version.
|
||||
|
||||
## Decision
|
||||
|
||||
Every workspace consumer uses `workspace:*` for DSH targets and `workspace:~` for targets under `vendor/` or in the `native/system` package family. This includes root tooling, apps, peers, development dependencies, and the native entry's optional platform dependencies. Directory placement never exempts a consumer.
|
||||
|
||||
Packing substitutes the target package's current version: `workspace:~` becomes `~4.0.3` for Cordis at `4.0.3` and `~0.1.2` for Node Addon System at `0.1.2`. DSH references remain exact. Native family members still share one release version, while an installed entry may resolve a later platform patch.
|
||||
|
||||
This range policy refines [npm release sequences](2026-08-10-npm-release-sequences.md) and [published dependency faces](2026-08-26-published-dependency-faces.md). Their independent release families and dependency-section classification remain unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Caret vendor ranges.** For stable packages such as Cordis, caret ranges also admit later minor versions.
|
||||
|
||||
**Exact vendor and native ranges.** Exact ranges require a consumer declaration change to admit each patch release.
|
||||
|
||||
## Consequences
|
||||
|
||||
Vendor and native patch releases must preserve their consumer-facing APIs and binary interfaces. Local workspace linking and lockfile resolutions remain unchanged by the range conversion. The workspace gate rejects DSH tilde ranges and vendor/native caret or exact ranges; packed-manifest tests check the emitted versions.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Workspace 依赖发布范围
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-22-workspace-release-ranges.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
DSH 包共享同一次产品发布。Cordis、其 vendored 库和 Node Addon System 独立发布;消费者需要接收它们的补丁更新,而不自动接纳新的次版本。
|
||||
|
||||
## Decision
|
||||
|
||||
每个 workspace 消费者对 DSH 目标使用 `workspace:*`,对 `vendor/` 下或 `native/system` 包族中的目标使用 `workspace:~`。规则覆盖根目录工具、应用、peer、开发依赖以及原生入口的可选平台依赖。消费者不因目录位置而获得豁免。
|
||||
|
||||
打包时替换为目标包的当前版本:Cordis 为 `4.0.3` 时,`workspace:~` 转为 `~4.0.3`;Node Addon System 为 `0.1.2` 时转为 `~0.1.2`。DSH 引用仍为精确版本。原生包族的成员仍以同一版本发布,但已安装的入口可以解析到后续的平台包补丁版本。
|
||||
|
||||
这套范围策略细化了 [npm 发布序列](2026-08-10-npm-release-sequences.zh.md)和[发布依赖门面](2026-08-26-published-dependency-faces.zh.md)。它们的独立发布族和依赖区段分类保持不变。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Vendor 使用 caret 范围。** 对 Cordis 这类稳定版本的包,caret 范围也会接纳后续次版本。
|
||||
|
||||
**Vendor 和 native 使用精确范围。** 每次接纳补丁发布都需要修改消费者的依赖声明。
|
||||
|
||||
## Consequences
|
||||
|
||||
Vendor 和 native 的补丁发布必须保持面向消费者的 API 和二进制接口兼容。范围转换不改变本地 workspace 链接及锁文件解析结果。Workspace 门禁拒绝 DSH 的 tilde 范围,以及 vendor/native 的 caret 或精确范围;打包清单测试校验最终输出的版本。
|
||||
+2
-2
@@ -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/simplification/2026-09-09-nontransactional-loader.md
|
||||
2026-09-09-nontransactional-loader.md: 6a3cb1476e1f296199067a8d048c6d7c8bba4069
|
||||
2026-09-09-nontransactional-loader.zh.md: 4e91a31ed86be4e45313e1322db8af486477aea7
|
||||
2026-09-09-nontransactional-loader.md: 98b0b1ce0c1250e651c5280c9b918948b17cd963
|
||||
2026-09-09-nontransactional-loader.zh.md: 77ae1ce91aa0d20940f0c63f2a2d42a0f3c07edb
|
||||
@@ -18,7 +18,7 @@ Application consumers own their completion checks. The CLI waits for its fallbac
|
||||
|
||||
Fiber, Entry, and isolate keep their upstream update return behavior. App boot observes discarded restart promises through the existing `internal/update` waterfall and waits for fibers before auditing a patch reload. The detached import-completion observer handles both fiber outcomes; the fiber still retains its failure for an explicit audit. Durable Include writes drain before and after child removal so a later teardown write cannot erase an earlier terminal write failure.
|
||||
|
||||
Two #932-specific vendor changes remain: awaited initial-file creation and forced rereading in Include, and Schemastery conditional exports. Restoring the pre-#932 debounced write/read sequence reproduces `ENOENT` in the missing-file initialization test. Keeping these two lines preserves the existing `initial` option without an application-side file writer or a second YAML serializer. Removing Schemastery exports reproduces `ERR_REQUIRE_ESM_RACE_CONDITION` while the Web preset suite boots: Node falls back to the CJS entry during concurrent ESM imports. The HMR injection decorators, conditional patch cloning, and update return values use the pre-#932 behavior. Explicit `workspace:^` dependencies make the #932 workspace-link switch and dedicated lockfile check unnecessary.
|
||||
Two #932-specific vendor changes remain: awaited initial-file creation and forced rereading in Include, and Schemastery conditional exports. Restoring the pre-#932 debounced write/read sequence reproduces `ENOENT` in the missing-file initialization test. Keeping these two lines preserves the existing `initial` option without an application-side file writer or a second YAML serializer. Removing Schemastery exports reproduces `ERR_REQUIRE_ESM_RACE_CONDITION` while the Web preset suite boots: Node falls back to the CJS entry during concurrent ESM imports. The HMR injection decorators, conditional patch cloning, and update return values use the pre-#932 behavior. Explicit `workspace:` dependencies make the #932 workspace-link switch and dedicated lockfile check unnecessary.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Loader 立即更改条目选项。EntryGroup 并发启动同级条目并记录
|
||||
|
||||
Fiber、Entry 和 isolate 保持上游的更新返回行为。App boot 通过现有的 `internal/update` waterfall 观察被丢弃的重启 promise,并在检查 patch 重载前等待 fiber。游离的导入完成观察器处理 fiber 的两种结果;fiber 仍保留失败信息供显式检查。Include 的持久写入在删除子条目前后均排空,防止后续拆卸写入掩盖更早的终止性写入失败。
|
||||
|
||||
保留两项 #932 专属 vendor 改动:Include 中等待初始文件创建并强制重新读取,以及 Schemastery 条件导出。恢复 #932 前的防抖写入和读取顺序,会在缺失文件初始化测试中复现 `ENOENT`。保留这两行可以维持已有 `initial` 选项,而无需在应用侧增加文件写入器或另一份 YAML 序列化逻辑。移除 Schemastery exports 后,Web preset 测试在启动时复现 `ERR_REQUIRE_ESM_RACE_CONDITION`:并发 ESM 导入使 Node 回退到 CJS 入口。HMR 注入装饰器、条件 patch 克隆及更新返回值采用 #932 前的行为。显式 `workspace:^` 依赖使 #932 的 workspace 链接开关与专用锁文件检查不再必要。
|
||||
保留两项 #932 专属 vendor 改动:Include 中等待初始文件创建并强制重新读取,以及 Schemastery 条件导出。恢复 #932 前的防抖写入和读取顺序,会在缺失文件初始化测试中复现 `ENOENT`。保留这两行可以维持已有 `initial` 选项,而无需在应用侧增加文件写入器或另一份 YAML 序列化逻辑。移除 Schemastery exports 后,Web preset 测试在启动时复现 `ERR_REQUIRE_ESM_RACE_CONDITION`:并发 ESM 导入使 Node 回退到 CJS 入口。HMR 注入装饰器、条件 patch 克隆及更新返回值采用 #932 前的行为。显式 `workspace:` 依赖使 #932 的 workspace 链接开关与专用锁文件检查不再必要。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -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/testing/2026-07-24-web-gui-browser-e2e-lane.md
|
||||
2026-07-24-web-gui-browser-e2e-lane.md: b08d99a7d750bdeabac3dcbb7007e922d32dbdcd
|
||||
2026-07-24-web-gui-browser-e2e-lane.zh.md: fbb92a094bbd14042d33e5ebd20d2d3ffd490d43
|
||||
2026-07-24-web-gui-browser-e2e-lane.md: eb79591e7b94fccb8eb92aec4024738825d214fc
|
||||
2026-07-24-web-gui-browser-e2e-lane.zh.md: f715f4ca93b202f72e632e5ddae0d13559a927a7
|
||||
@@ -10,7 +10,7 @@ The web GUI ships as a real assembled chain — chromium page → client plugin
|
||||
|
||||
## Decision
|
||||
|
||||
`pnpm run test:web` carries a keyless, deterministic browser e2e lane under `apps/web/tests/`: recorded session-log fixtures replay through `@deepseek-ai/dsh-llm-replay` against the real in-process web composition, with normalized aria goldens for user-visible states and in-process assertions for durable world state. The supporting product contracts are `dsh-llm-replay` pacing, consumption checks, and validated indexed override patches; cross-package `dsh-llm` failures retain validated provider facts through own data properties; and the shipped web composition mounts `llm-retry` for transient model failures.
|
||||
`pnpm run test:web` carries a keyless, deterministic browser e2e lane under `apps/web/tests/`: recorded session-log fixtures replay through `@deepseek-ai/dsh-llm-replay` against the real in-process web composition, with normalized aria goldens for user-visible states and in-process assertions for durable world state. Chromium runs the full lane; the [model and reasoning picker scenario](../../../../apps/web/tests/declared-reasoning.e2e.ts) also runs in WebKit to cover native mouse focus behavior. The supporting product contracts are `dsh-llm-replay` pacing, consumption checks, and validated indexed override patches; cross-package `dsh-llm` failures retain validated provider facts through own data properties; and the shipped web composition mounts `llm-retry` for transient model failures.
|
||||
|
||||
### Scaffold: `apps/web/tests/scaffold.ts`
|
||||
|
||||
@@ -32,7 +32,7 @@ No single-shot transient-DOM assertions: every hop from replay yield to React co
|
||||
|
||||
Pagination drivers wait for the interactive load row to leave its pending state and record the pre-request row count before scrolling. An immediately committed resident page therefore remains observable instead of becoming the baseline for a request that the scroll gesture does not repeat.
|
||||
|
||||
Every scenario fails on any pageerror and on the client's connection-loss/gap-repair console warnings: the reconnect machine plus history resync would otherwise self-heal a dead SSE path and the suite would certify a broken wire. Scaffold `close()` calls the `ReplayHandle.assertConsumed()` teardown check (every recorded script bound, every cursor drained), converting silent underruns and shifted bindings into crisp diagnostics. No vitest retry on the lane; one chromium per file, fresh context per scenario, one host per scenario; viewport pinned; interaction selectors anchor on roles, `data-*` attributes, and visible text, while the frame and conversation-region captures use the existing CSS-module local-name anchors. Standard scenarios open an `en-US` browser so localized role locators and goldens use one explicit language; scenarios asserting Chinese copy open a `zh-CN` browser instead, because the client derives its provisional locale from `navigator` when the Host settings document has no explicit preference ([browser-derived initial locale](../../archived/feature/2026-07-31-browser-derived-initial-locale.md)). `settings-chrome.e2e.ts` additionally covers both switch directions, a fresh English-browser default, and preference persistence across distinct ports sharing one DSH home.
|
||||
Every scenario fails on any pageerror and on the client's connection-loss/gap-repair console warnings: the reconnect machine plus history resync would otherwise self-heal a dead SSE path and the suite would certify a broken wire. Scaffold `close()` calls the `ReplayHandle.assertConsumed()` teardown check (every recorded script bound, every cursor drained), converting silent underruns and shifted bindings into crisp diagnostics. No vitest retry on the lane; one browser per file and engine, fresh context per scenario, one host per scenario; viewport pinned; interaction selectors anchor on roles, `data-*` attributes, and visible text, while the frame and conversation-region captures use the existing CSS-module local-name anchors. Standard scenarios open an `en-US` browser so localized role locators and goldens use one explicit language; scenarios asserting Chinese copy open a `zh-CN` browser instead, because the client derives its provisional locale from `navigator` when the Host settings document has no explicit preference ([browser-derived initial locale](../../archived/feature/2026-07-31-browser-derived-initial-locale.md)). `settings-chrome.e2e.ts` additionally covers both switch directions, a fresh English-browser default, and preference persistence across distinct ports sharing one DSH home.
|
||||
|
||||
### Expected outputs
|
||||
|
||||
@@ -50,7 +50,7 @@ The lane covers three behavior families. Live-turn scenarios pin ordinary tool e
|
||||
|
||||
### CI stance
|
||||
|
||||
The lane is a required compare-only gate for Linux pull requests under the [browser snapshot CI decision](2026-07-30-web-browser-snapshot-ci-gate.md). The `node 24 / snapshots and artifacts` consumer job owns the [single Linux build](../../archived/process/2026-07-30-independent-ci-consumer-build.md), installs the lockfile-selected Chromium, restores its OS-and-lockfile-keyed cache, and runs the lane with `DSH_SNAPSHOT=replay`. This is an intentional plane split: the host and specs use the [tsx source-launch contract](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md), while the browser consumes `apps/web/dist` and package `lib/client.js` artifacts, so the gate depends on `built-package-invariants` for those client artifacts. The self-hosted default-branch Linux serial standby runs the same gate; there is no hosted Linux serial producer of the browser cache consumed by pull requests, and the persistent self-hosted pool needs no hosted cache. CI never records or refreshes goldens. Scenarios remain POSIX-oriented and stay outside the Windows and macOS matrices.
|
||||
The lane is a required compare-only gate for Linux pull requests under the [browser snapshot CI decision](2026-07-30-web-browser-snapshot-ci-gate.md). The `node 24 / snapshots and artifacts` consumer job owns the [single Linux build](../../archived/process/2026-07-30-independent-ci-consumer-build.md), installs the lockfile-selected Chromium and WebKit, restores their OS-and-lockfile-keyed cache, and runs the lane with `DSH_SNAPSHOT=replay`. This is an intentional plane split: the host and specs use the [tsx source-launch contract](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md), while the browser consumes `apps/web/dist` and package `lib/client.js` artifacts, so the gate depends on `built-package-invariants` for those client artifacts. The self-hosted default-branch Linux serial standby runs the same gate; there is no hosted Linux serial producer of the browser cache consumed by pull requests, and the persistent self-hosted pool needs no hosted cache. CI never records or refreshes goldens. Scenarios remain POSIX-oriented and stay outside the Windows and macOS matrices.
|
||||
|
||||
High-cardinality performance diagnostics use the separate opt-in `apps/web/tests/**/*.perf.ts` inventory selected only by `vitest.web.perf.config.ts`. The isolated `complex-history.perf.ts` cases reuse the real scaffold: the workspace case seeds 1,000 compact sessions plus one 500-turn history containing 500 tool calls, exhausts and remounts that history in Chat, and reports Chromium main-thread, DOM, listener, heap, paging, search, and Trajectory measurements. Two continuation cases seed the same long history but compare the default 24-turn Chat window with all 500 turns expanded before each continues eight identical turns through the real composer, agent loop, SSE wire, tools, and persistence; two turns execute a real `bash` call and assert its durable result, while the final turn fills an 8,232-character mixed-language prompt and replays 120 paced text deltas. A separate soak case starts from a blank session, drives 100 consecutive real composer turns with a `bash` call and result every tenth turn, forces GC every ten turns, and reports ten-turn latency windows plus retained browser state. It then submits a 101st text-only turn with a trusted browser click and measures browser-clock send-to-transcript-DOM and send-to-post-paint latency, excluding the composer's draft mirrors, separately from full-turn completion. Per-turn diagnostics cover composer fill, click-to-user-echo, click-to-first-chunk, completion, browser mutations, persisted chunks, and tool events; the synthetic replay model has enough context capacity to keep fixture cardinality stable instead of consuming scripted calls through compaction. Structural assertions pin the intended load, stream, and tool shapes, but timing remains threshold-free because machine speed is not a correctness contract. The required `vitest.web.config.ts` inventory remains limited to `*.e2e.ts` and `*.snapshot.ts`, so neither `test:web:built` nor its CI gate collects performance cases.
|
||||
|
||||
@@ -95,4 +95,4 @@ Surveyed AI-chat/agent web UIs and mocking layers (LibreChat, vercel/ai-chatbot
|
||||
|
||||
## Consequences
|
||||
|
||||
The web surface gains its record-once/replay-forever tier: the real chromium → SSE → apiproxy → loop → tools → persistence chain runs keylessly in ~10-30s, deterministic across repeat runs, with fixtures owned and re-recordable by the lane itself. Costs accepted: every intentional conversation-UI change ends with a keyless `DSH_SNAPSHOT=refresh` (golden churn is reviewed diff, anchors keep semantic green); the aria format is Playwright-owned — the one committed snapshot format the repo does not control — so playwright version bumps must be deliberate bump-and-refresh commits (the dependency floats `^1.49.0` in `apps/web/package.json`; pin exactly if churn bites); replay's first-call-order binding constrains scenarios to one prompting session each, with the consumption assertion as the tripwire; `compaction-basic` shares the session's replay cursor and stays inert only under the published 128k catalog window; and the required consumer job pays for Chromium provisioning and one browser run so the PR that changes the assembled UI owns its expected-output diff. The opt-in performance lane preserves a repeatable, threshold-free diagnostic workload whose measurements require manual interpretation. The separate required [frontend performance benchmarks](2026-09-06-frontend-performance-budgets.md) enforce calibrated budgets in the isolated benchmark CI job; they do not add thresholds to the manual inventory.
|
||||
The web surface gains its record-once/replay-forever tier: the real chromium → SSE → apiproxy → loop → tools → persistence chain runs keylessly in ~10-30s, deterministic across repeat runs, with fixtures owned and re-recordable by the lane itself. Costs accepted: every intentional conversation-UI change ends with a keyless `DSH_SNAPSHOT=refresh` (golden churn is reviewed diff, anchors keep semantic green); the aria format is Playwright-owned — the one committed snapshot format the repo does not control — so playwright version bumps must be deliberate bump-and-refresh commits (the dependency floats `^1.49.0` in `apps/web/package.json`; pin exactly if churn bites); replay's first-call-order binding constrains scenarios to one prompting session each, with the consumption assertion as the tripwire; `compaction-basic` shares the session's replay cursor and stays inert only under the published 128k catalog window; and the required consumer job pays for Chromium and WebKit provisioning and the browser lane so the PR that changes the assembled UI owns its expected-output diff. The opt-in performance lane preserves a repeatable, threshold-free diagnostic workload whose measurements require manual interpretation. The separate required [frontend performance benchmarks](2026-09-06-frontend-performance-budgets.md) enforce calibrated budgets in the isolated benchmark CI job; they do not add thresholds to the manual inventory.
|
||||
@@ -10,9 +10,7 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu
|
||||
|
||||
## 决策
|
||||
|
||||
布局断言先等待字体加载、框架过渡完成及 Conversation 宽度发布,再读取尺寸;触发合成 resize 后,还需等待其调度的 React 更新,才能测量通过 portal 挂载的面板。目录树的子目录和根目录读取使用相同的 Remote 等待预算。工作区重载场景在打开另一个路径编辑器前,等待 Session 选择恢复及输入框获得焦点,因为迟到的聚焦会取消路径草稿。响应式文件标签场景将实测通道宽度放在容器查询档位内部,并为平台字体度量保留明确余量。
|
||||
|
||||
`pnpm run test:web` 携带 `apps/web/tests/` 下的无密钥、确定性浏览器 e2e 车道:录制的会话日志 fixture 经 `@deepseek-ai/dsh-llm-replay` 对真实进程内 web 组合回放;用户可见状态使用规范化的 aria 预期输出,持久化的世界状态则使用进程内断言。配套的产品约定包括 `dsh-llm-replay` 的节奏控制、消费检查与已校验的索引式覆写 patch;跨包的 `dsh-llm` 失败通过自有数据属性保留经校验的提供方信息;已交付的 web 组合挂载 `llm-retry`,以处理瞬态模型失败。
|
||||
`pnpm run test:web` 携带 `apps/web/tests/` 下的无密钥、确定性浏览器 e2e 车道:录制的会话日志 fixture 经 `@deepseek-ai/dsh-llm-replay` 对真实进程内 web 组合回放;用户可见状态使用规范化的 aria 预期输出,持久化的世界状态则使用进程内断言。Chromium 运行整个车道;[模型与推理强度选择场景](../../../../apps/web/tests/declared-reasoning.e2e.ts) 还在 WebKit 中运行,以覆盖原生鼠标焦点行为。配套的产品约定包括 `dsh-llm-replay` 的节奏控制、消费检查与已校验的索引式覆写 patch;跨包的 `dsh-llm` 失败通过自有数据属性保留经校验的提供方信息;已交付的 web 组合挂载 `llm-retry`,以处理瞬态模型失败。
|
||||
|
||||
### Scaffold:`apps/web/tests/scaffold.ts`
|
||||
|
||||
@@ -28,11 +26,13 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu
|
||||
|
||||
回放模式下浏览器断言的屏障栈,按序:(1)host 侧 `await agent.whenIdle()` 加超时,以进程内 `turn/end` 为锚——空闲翻转发生在持久化落盘之后,一次等待同时覆盖轮次完成与持久性;(2)浏览器安定轮询(流式输出节点已卸载、最终文本可见)。录制模式下,日志采收在 `whenIdle()` 之后、scaffold 释放之前进行,此时运行中的会话仍然可用。单独监听进程内 `turn/end` 是错误屏障(它先于 SSE 帧到达浏览器、先于 fsync 触发);禁止轮询持久化文件来充当轮次完成或持久性屏障(NFS 上慢,且被 `whenIdle` 取代),但工具控制的临时就绪标记可以仅作为该完成屏障之前的交互门控进行轮询;`networkidle` 被彻底禁止(SSE 流保持打开时它永不解析)。导航断言会在页面加载前同时监听 `session.list` 和 `workspace.list` 的初始响应,随后等待播种数据投影到 DOM;仅凭 shell 已挂载不能判定就绪,因为较晚完成的 bootstrap 可能替换受控状态。
|
||||
|
||||
布局断言先等待字体加载、框架过渡完成及 Conversation 宽度发布,再读取尺寸;触发合成 resize 后,还需等待其调度的 React 更新,才能测量通过 portal 挂载的面板。目录树的子目录和根目录读取使用相同的 Remote 等待预算。工作区重载场景在打开另一个路径编辑器前,等待 Session 选择恢复及输入框获得焦点,因为迟到的聚焦会取消路径草稿。响应式文件标签场景将实测通道宽度放在容器查询档位内部,并为平台字体度量保留明确余量。
|
||||
|
||||
不做单次瞬态 DOM 断言:从 replay yield 到 React commit 的每一跳都可能合并 chunk,采样 `[data-streaming]` 天然就是竞态。流式增量性通过有序 `agent/assistant-stream` follow path 断言,最终持久 `assistant/message` 或 `assistant/attempt` 则嵌入 replay 使用的精确 stream。`dsh-llm-replay` 的可选 `paceMs`(默认缺省 = burst)只是让浏览器观察到真正增量 SSE 的真实感旋钮;正确性绝不依赖它,且 pace wait 期间 abort 会即时取消。
|
||||
|
||||
分页驱动会等待交互式加载行退出 pending 状态,并在滚动前记录请求前的行数。因此,即时提交的常驻页仍然可观测,不会变成一次滚动手势不会重复触发的请求基线。
|
||||
|
||||
每个场景都会因任何 pageerror 或客户端的连接丢失/间隙修复控制台警告而失败:否则重连机制加历史重同步会把一条死掉的 SSE 通路自愈掉,套件反而认证了坏 wire。Scaffold 的 `close()` 调用 `ReplayHandle.assertConsumed()` 收尾检查(每个已录脚本都被绑定、每个游标都耗尽),把静默的少放与错绑变成清晰诊断。车道不设 vitest 重试;每文件一个 chromium、每场景一个新 context、每场景一个 host;视口固定;交互选择器锚定 role、`data-*` 属性和可见文本,而 frame 与会话区采集则使用既有的 CSS 模块局部类名锚点。常规场景开启 `en-US` 浏览器,使本地化的 role 定位器和预期输出统一采用明确指定的语言;断言中文文案的场景则开启 `zh-CN` 浏览器,因为 Host settings 文档没有显式偏好时,客户端的暂定 locale 由 `navigator` 推导([由浏览器推导初始 locale](../../archived/feature/2026-07-31-browser-derived-initial-locale.md))。`settings-chrome.e2e.ts` 还额外覆盖双向切换、全新英文浏览器默认态,以及共享同一 DSH home 的不同端口之间的偏好持久化。
|
||||
每个场景都会因任何 pageerror 或客户端的连接丢失/间隙修复控制台警告而失败:否则重连机制加历史重同步会把一条死掉的 SSE 通路自愈掉,套件反而认证了坏 wire。Scaffold 的 `close()` 调用 `ReplayHandle.assertConsumed()` 收尾检查(每个已录脚本都被绑定、每个游标都耗尽),把静默的少放与错绑变成清晰诊断。车道不设 vitest 重试;每个文件中的每种引擎各启动一个浏览器、每场景一个新 context、每场景一个 host;视口固定;交互选择器锚定 role、`data-*` 属性和可见文本,而 frame 与会话区采集则使用既有的 CSS 模块局部类名锚点。常规场景开启 `en-US` 浏览器,使本地化的 role 定位器和预期输出统一采用明确指定的语言;断言中文文案的场景则开启 `zh-CN` 浏览器,因为 Host settings 文档没有显式偏好时,客户端的暂定 locale 由 `navigator` 推导([由浏览器推导初始 locale](../../archived/feature/2026-07-31-browser-derived-initial-locale.md))。`settings-chrome.e2e.ts` 还额外覆盖双向切换、全新英文浏览器默认态,以及共享同一 DSH home 的不同端口之间的偏好持久化。
|
||||
|
||||
### 预期输出
|
||||
|
||||
@@ -50,7 +50,7 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu
|
||||
|
||||
### CI 立场
|
||||
|
||||
根据[浏览器快照 CI 决策](2026-07-30-web-browser-snapshot-ci-gate.zh.md),该车道是 Linux 拉取请求必需的只比较门禁。`node 24 / snapshots and artifacts` 消费方任务在[消费方独立构建](../../archived/process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,安装锁文件选定的 Chromium,恢复以操作系统和锁文件为键的缓存,并用 `DSH_SNAPSHOT=replay` 运行该车道。这是有意的平面切分:host 与 spec 使用 [tsx 源码启动约定](../architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md),浏览器则消费 `apps/web/dist` 和包的 `lib/client.js` 产物,因此门禁依赖 `built-package-invariants` 提供这些客户端产物。自托管的默认分支 Linux 串行热备运行同一门禁;不存在托管 Linux 串行生产者生成供 PR 消费的浏览器缓存,持久化自托管池则不需要托管侧缓存。CI 从不录制或刷新预期输出。场景仍面向 POSIX,并继续置于 Windows 和 macOS 矩阵之外。
|
||||
根据[浏览器快照 CI 决策](2026-07-30-web-browser-snapshot-ci-gate.zh.md),该车道是 Linux 拉取请求必需的只比较门禁。`node 24 / snapshots and artifacts` 消费方任务在[消费方独立构建](../../archived/process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,安装锁文件选定的 Chromium 和 WebKit,恢复以操作系统和锁文件为键的缓存,并用 `DSH_SNAPSHOT=replay` 运行该车道。这是有意的平面切分:host 与 spec 使用 [tsx 源码启动约定](../architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md),浏览器则消费 `apps/web/dist` 和包的 `lib/client.js` 产物,因此门禁依赖 `built-package-invariants` 提供这些客户端产物。自托管的默认分支 Linux 串行热备运行同一门禁;不存在托管 Linux 串行生产者生成供 PR 消费的浏览器缓存,持久化自托管池则不需要托管侧缓存。CI 从不录制或刷新预期输出。场景仍面向 POSIX,并继续置于 Windows 和 macOS 矩阵之外。
|
||||
|
||||
高基数性能诊断使用单独按需启用的 `apps/web/tests/**/*.perf.ts` 清单,并且只由 `vitest.web.perf.config.ts` 选中。`complex-history.perf.ts` 的隔离用例复用真实 scaffold:工作区用例播种 1,000 个紧凑会话以及一份包含 500 次工具调用的 500 轮次历史,在 Chat 中穷尽并重新挂载该历史,并报告 Chromium 主线程、DOM、监听器、堆内存、分页、搜索和 Trajectory 测量结果。两个续聊用例播种同一份长历史,但比较默认的 24 轮次 Chat 窗口与展开全部 500 轮次的状态,然后各自通过真实输入框、agent loop、SSE wire、工具和持久化继续进行 8 个相同轮次;其中两轮执行真实 `bash` 调用并断言其持久化结果,最后一轮则填入一条包含 8,232 个字符的混合语言提示词,并回放 120 个带节奏的文本增量。一个单独的 soak 用例从空白会话开始,通过真实输入框连续驱动 100 轮,每第 10 轮执行一次 `bash` 调用并产生结果,每 10 轮强制执行一次 GC,并报告每 10 轮的延迟窗口及保留的浏览器状态。随后它通过受信任的浏览器点击提交第 101 个纯文本轮次,并使用浏览器时钟分别测量发送到 transcript DOM 和发送到绘制后的延迟,排除输入框的草稿镜像,并与完整轮次完成时间分开。逐轮诊断涵盖输入框填入、点击到用户消息回显、点击到首个分片、完成、浏览器变更、持久化分片和工具事件;合成回放模型拥有足够的上下文容量,可使 fixture 基数保持稳定,而不会因压缩(compaction)消耗脚本化调用。结构性断言钉住预期的负载、流和工具形状,但时间仍不设阈值,因为机器速度不属于正确性约定。必需的 `vitest.web.config.ts` 清单仍仅限 `*.e2e.ts` 和 `*.snapshot.ts`,因此 `test:web:built` 及其 CI 门禁都不会收集性能用例。
|
||||
|
||||
@@ -95,4 +95,4 @@ Web GUI 以一条真实组装链交付——chromium 页面 → client 插件 bu
|
||||
|
||||
## 后果
|
||||
|
||||
Web 表面获得了录制一次/永久回放的层级:真实 chromium → SSE → apiproxy → 循环 → 工具 → 持久化的链路以约 10-30 秒无密钥运行,重复运行结果确定,fixture 由车道自身持有并可重录。接受的成本:每次有意的会话 UI 变更都以一次无密钥 `DSH_SNAPSHOT=refresh` 收尾(预期输出变动是受评审的 diff,锚断言保住语义绿色);aria 格式归 Playwright 所有——仓库唯一不受自己控制的提交快照格式——因此 playwright 版本升级必须是刻意的升级加刷新提交(依赖在 `apps/web/package.json` 中浮动为 `^1.49.0`;若变动伤人则改为精确锁定);回放的首次调用顺序绑定把每个场景限制为至多一个发起提示的会话,消费断言是绊线;`compaction-basic` 与会话共享回放游标,仅在目录中发布的 128k 上下文窗口下保持闲置;必需的消费方任务承担 Chromium 供给与一次浏览器运行的成本,使改动组装后 UI 的 PR(Pull Request)持有相应的预期输出 diff。按需启用的性能车道保留可重复、无阈值的诊断工作负载,其测量需要人工解读。独立的必需[前端性能基准](2026-09-06-frontend-performance-budgets.zh.md)在隔离的基准 CI job 中执行经校准的预算;它们不向手动清单添加阈值。
|
||||
Web 表面获得了录制一次/永久回放的层级:真实 chromium → SSE → apiproxy → 循环 → 工具 → 持久化的链路以约 10-30 秒无密钥运行,重复运行结果确定,fixture 由车道自身持有并可重录。接受的成本:每次有意的会话 UI 变更都以一次无密钥 `DSH_SNAPSHOT=refresh` 收尾(预期输出变动是受评审的 diff,锚断言保住语义绿色);aria 格式归 Playwright 所有——仓库唯一不受自己控制的提交快照格式——因此 playwright 版本升级必须是刻意的升级加刷新提交(依赖在 `apps/web/package.json` 中浮动为 `^1.49.0`;若变动伤人则改为精确锁定);回放的首次调用顺序绑定把每个场景限制为至多一个发起提示的会话,消费断言是绊线;`compaction-basic` 与会话共享回放游标,仅在目录中发布的 128k 上下文窗口下保持闲置;必需的消费方任务承担 Chromium 与 WebKit 供给及浏览器车道的成本,使改动组装后 UI 的 PR(Pull Request)持有相应的预期输出 diff。按需启用的性能车道保留可重复、无阈值的诊断工作负载,其测量需要人工解读。独立的必需[前端性能基准](2026-09-06-frontend-performance-budgets.zh.md)在隔离的基准 CI job 中执行经校准的预算;它们不向手动清单添加阈值。
|
||||
@@ -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/testing/2026-07-30-web-browser-snapshot-ci-gate.md
|
||||
2026-07-30-web-browser-snapshot-ci-gate.md: d1c8dcdd859d0515804cc6aed0d2f3341beb6dd0
|
||||
2026-07-30-web-browser-snapshot-ci-gate.zh.md: 9d642819e09abc63384f4f4eb105d13261a9eb95
|
||||
2026-07-30-web-browser-snapshot-ci-gate.md: 352072bca6e1a9e1d5b72973d2c3c9e96bc58ad9
|
||||
2026-07-30-web-browser-snapshot-ci-gate.zh.md: cd04f33a5db6ddb3c88a235fb34ea31da0e5a869
|
||||
@@ -12,13 +12,13 @@ The [keyless web browser e2e lane](2026-07-24-web-gui-browser-e2e-lane.md) runs
|
||||
|
||||
For Linux PRs, the `node 24 / snapshots and artifacts` job must run the full web browser replay/compare suite. When `DSH_WEB_SNAPSHOT_WORKERS` is configured, `scripts/run-gates.ts` registers `test:web:ci` as the `ci-consumers` gate and explicitly injects `DSH_SNAPSHOT=replay`; CI never runs in `record` or `refresh` mode, so when the committed goldens disagree with the currently assembled application, the tests fail directly instead of silently rewriting them on the runner and then passing.
|
||||
|
||||
The consumer job owns the [single Linux build](../../archived/process/2026-07-30-independent-ci-consumer-build.md), so `apps/web/dist` and the package `lib/` directories remain in its workspace for the browser suite. On hosted runners, CI installs Chromium and its system dependencies at the Playwright version in the lockfile. On the persistent failover VM, the image owns the Linux system packages and CI installs only Chromium, avoiding per-run `apt` mutation. Pull requests restore the operating-system-and-lockfile-keyed browser cache without paying compression and upload on the required path, with an operating-system prefix fallback across lockfile changes. No master job produces these hosted caches, so restores hit archived entries until they evict. The self-hosted standby runs the same comparison without hosted cache actions.
|
||||
The consumer job owns the [single Linux build](../../archived/process/2026-07-30-independent-ci-consumer-build.md), so `apps/web/dist` and the package `lib/` directories remain in its workspace for the browser suite. Chromium runs the full suite; the model and reasoning picker also runs in WebKit to cover native mouse focus behavior. On hosted runners, CI installs both engines and their system dependencies at the Playwright version in the lockfile. On the persistent failover VM, the image owns both engines’ Linux system packages and CI installs only the browser binaries, avoiding per-run `apt` mutation. Pull requests restore the operating-system-and-lockfile-keyed browser cache without paying compression and upload on the required path, with an operating-system prefix fallback across lockfile changes. No master job produces these hosted caches, so restores hit archived entries until they evict. The self-hosted standby runs the same comparison without hosted cache actions.
|
||||
|
||||
Local `pnpm run test:web` continues to build first and then run the full browser suite serially; `test:web:built` is the serial entry point for existing build artifacts. Developers explicitly run `DSH_SNAPSHOT=refresh pnpm run test:web` only after confirming that user-visible output changed intentionally, review every expected-output diff, and then verify again in replay mode that no files are written.
|
||||
|
||||
CI’s `scripts/run-web-snapshots.ts` runs `hmr-live.e2e.ts` separately because it mutates built workspace state. One six-worker Vitest pool then runs every remaining file, including historical Cordis-card rendering. Every child inherits stdio, and `run-gates` streams its output.
|
||||
|
||||
For pull requests, the gate runs only in the Linux consumer job: these scenarios target POSIX, and the other PR jobs do not provision Chromium. The self-hosted default-branch Linux serial standby also includes the comparison, while the macOS and Windows serial jobs remain browser-free (there is no hosted Linux serial aggregate). A PR's `all checks passed` verdict already depends on the consumer job, so a browser compare failure blocks the merge without requiring a new branch-protection check name.
|
||||
For pull requests, the gate runs only in the Linux consumer job: these scenarios target POSIX, and the other PR jobs do not provision both engines. The self-hosted default-branch Linux serial standby also includes the comparison, while the macOS and Windows serial jobs remain browser-free (there is no hosted Linux serial aggregate). A PR's `all checks passed` verdict already depends on the consumer job, so a browser compare failure blocks the merge without requiring a new branch-protection check name.
|
||||
|
||||
Completed local replays measured the six-worker browser command at about 65–71 seconds. A twelve-worker comparison completed in about 50 seconds, so halving the browser worker budget adds about 15–20 seconds rather than doubling wall time. The gate scheduler starts browser snapshots as soon as `built-package-invariants` succeeds and runs independent gates concurrently, so it needs neither a dedicated job timeout nor a manual YAML ordering rule.
|
||||
|
||||
@@ -36,4 +36,4 @@ Completed local replays measured the six-worker browser command at about 65–71
|
||||
|
||||
## Consequences
|
||||
|
||||
Before merge, every PR proves that the current web assembly matches all committed browser expected outputs; a missing refresh fails in the same PR that changes the assembly. The cost is Chromium provisioning, one serial scenario, and one bounded six-worker pool in the consumer job; the consumer-owned build and browser cache avoid duplicate builds and downloads on reruns. Parallel-file failures stream immediately, but a worker-budget change still requires a completed end-to-end measurement rather than an elapsed-time guess. The gate makes no claim of cross-platform browser consistency, and if a Playwright/Chromium upgrade changes the ARIA format, the upgrade PR must explicitly refresh the expected outputs and review the churn.
|
||||
Before merge, every PR proves that the current web assembly matches all committed browser expected outputs; a missing refresh fails in the same PR that changes the assembly. The cost is Chromium and WebKit provisioning, one serial scenario, and one bounded six-worker pool in the consumer job; the consumer-owned build and browser cache avoid duplicate builds and downloads on reruns. Parallel-file failures stream immediately, but a worker-budget change still requires a completed end-to-end measurement rather than an elapsed-time guess. The gate makes no claim of cross-platform browser consistency, and if a Playwright or browser upgrade changes the ARIA format, the upgrade PR must explicitly refresh the expected outputs and review the churn.
|
||||
@@ -12,13 +12,13 @@ Status: implemented
|
||||
|
||||
Linux PR 的 `node 24 / snapshots and artifacts` 必须运行完整 Web 浏览器 replay/compare。配置 `DSH_WEB_SNAPSHOT_WORKERS` 后,`scripts/run-gates.ts` 把 `test:web:ci` 登记为 `ci-consumers` 门禁,并显式注入 `DSH_SNAPSHOT=replay`;CI 永不以 `record` 或 `refresh` 模式运行,因此提交的预期输出与当前组装应用不一致时测试直接失败,不会在 runner 内静默改写后通过。
|
||||
|
||||
消费方 job 在[消费方独立构建](../../archived/process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,因此 `apps/web/dist` 和包的 `lib/` 目录会保留在其工作区中,供浏览器套件使用。在托管运行器上,CI 按锁文件中的 Playwright 版本安装 Chromium 及其系统依赖。在持久化故障切换 VM 上,镜像负责预装 Linux 系统软件包,CI 只安装 Chromium,避免每次运行都通过 `apt` 改动系统。PR 恢复以操作系统和锁文件为键的浏览器缓存,使必需路径无需承担压缩和上传开销,并可在锁文件变化时按操作系统前缀回退。没有任何 master 作业生成这些 hosted 缓存,因此恢复只能命中仍有归档的旧条目,直至其被逐出。自托管热备运行相同的比较,但不执行托管缓存操作。
|
||||
消费方 job 在[消费方独立构建](../../archived/process/2026-07-30-independent-ci-consumer-build.md)中负责唯一一次 Linux 构建,因此 `apps/web/dist` 和包的 `lib/` 目录会保留在其工作区中,供浏览器套件使用。Chromium 运行完整套件;模型与推理强度选择场景还在 WebKit 中运行,以覆盖原生鼠标焦点行为。在托管运行器上,CI 按锁文件中的 Playwright 版本安装这两种引擎及其系统依赖。在持久化故障切换 VM 上,镜像负责预装两种引擎的 Linux 系统软件包,CI 只安装浏览器程序,避免每次运行都通过 `apt` 改动系统。PR 恢复以操作系统和锁文件为键的浏览器缓存,使必需路径无需承担压缩和上传开销,并可在锁文件变化时按操作系统前缀回退。没有任何 master 作业生成这些 hosted 缓存,因此恢复只能命中仍有归档的旧条目,直至其被逐出。自托管热备运行相同的比较,但不执行托管缓存操作。
|
||||
|
||||
本地 `pnpm run test:web` 仍先构建,再串行运行完整浏览器套件;`test:web:built` 是已有构建产物的串行执行入口。开发者只在确认用户可见输出有意变化后显式运行 `DSH_SNAPSHOT=refresh pnpm run test:web`,评审每一处预期输出 diff,再以 replay 模式复验不再写文件。
|
||||
|
||||
CI 的 `scripts/run-web-snapshots.ts` 单独运行会修改构建产物的 `hmr-live.e2e.ts`。随后由一个六 worker Vitest 池运行其余文件,包括历史 Cordis 卡片渲染。所有子进程继承 stdio,由 `run-gates` 流式转发输出。
|
||||
|
||||
对 PR 而言,门禁仅在 Linux 消费方 job 中运行:这些场景面向 POSIX,其他 PR job 不安装 Chromium。自托管的默认分支 Linux 串行热备也包含该比较,而 macOS 和 Windows 串行 job 仍不使用浏览器(不存在托管的 Linux 串行聚合)。PR 的 `all checks passed` 已依赖消费方 job,因此浏览器比较失败会阻止合并,无需新增 branch-protection check 名称。
|
||||
对 PR 而言,门禁仅在 Linux 消费方 job 中运行:这些场景面向 POSIX,其他 PR job 不会同时安装这两种引擎。自托管的默认分支 Linux 串行热备也包含该比较,而 macOS 和 Windows 串行 job 仍不使用浏览器(不存在托管的 Linux 串行聚合)。PR 的 `all checks passed` 已依赖消费方 job,因此浏览器比较失败会阻止合并,无需新增 branch-protection check 名称。
|
||||
|
||||
完整本地 replay 中,6-worker 浏览器命令耗时约 65–71 秒。12-worker 对比约为 50 秒,因此把浏览器 worker 预算减半只增加约 15–20 秒,而不是让墙钟时间翻倍。门禁调度器会在 `built-package-invariants` 成功后立即启动浏览器快照,并发运行彼此独立的门禁,因此既不需要专用 job 超时,也不需要手动制定 YAML 顺序规则。
|
||||
|
||||
@@ -36,4 +36,4 @@ CI 的 `scripts/run-web-snapshots.ts` 单独运行会修改构建产物的 `hmr-
|
||||
|
||||
## 后果
|
||||
|
||||
每个 PR 都在合并前证明当前 Web 组装与所有已提交的浏览器预期输出一致;漏刷会在改变该组装的同一个 PR 中失败。成本是消费方 job 需要安装 Chromium、串行运行 2 个场景并执行 1 个有界 6-worker 池;消费方独立构建与浏览器缓存避免重跑时重复构建和下载。并行文件的失败会立即流式显示,但 worker 预算的任何变化仍需要完整端到端测量,而不能依据运行中耗时猜测。门禁不声称跨平台浏览器一致性,Playwright/Chromium 升级若改变 ARIA 格式,升级 PR 必须显式 refresh 并评审 churn。
|
||||
每个 PR 都在合并前证明当前 Web 组装与所有已提交的浏览器预期输出一致;漏刷会在改变该组装的同一个 PR 中失败。成本是消费方 job 需要安装 Chromium 和 WebKit、串行运行 1 个场景并执行 1 个有界 6-worker 池;消费方独立构建与浏览器缓存避免重跑时重复构建和下载。并行文件的失败会立即流式显示,但 worker 预算的任何变化仍需要完整端到端测量,而不能依据运行中耗时猜测。门禁不声称跨平台浏览器一致性,Playwright 或浏览器升级若改变 ARIA 格式,升级 PR 必须显式 refresh 并评审 churn。
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/proposed/architecture/2026-09-20-chat-presentation-policy-and-step-groups.md
|
||||
2026-09-20-chat-presentation-policy-and-step-groups.md: 65fbb313d57a34a792ed2ec08d0adfb2fdd5f03d
|
||||
2026-09-20-chat-presentation-policy-and-step-groups.zh.md: 7ac3442476ea34d5127376cfc40d42892403ce47
|
||||
-288
@@ -1,288 +0,0 @@
|
||||
# Agent Note: Chat presentation policy channel and process-group architecture
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-20-chat-presentation-policy-and-step-groups.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
At the start of this proposal, Chat's display setting had two modes: `normal` did not fold content, while `compact` folded process rows behind a whole-Turn control after completion. `TranscriptViewPolicy.mode` was injected into ChatView as an observable. ChatView read it through `useTranscriptView`, converted it to the boolean `compactTranscript`, and passed that prop through `ChatNodeList` to every `ChatNodeSeat`. The global `hasMore` boolean followed the same path as `historyIncomplete`. Either change re-rendered every seat, even when only a few foldable rows changed their output.
|
||||
|
||||
The product requires three modes—Compact, Detailed, and Expanded—with consecutive process rows between replies collected into collapsible groups in Compact and Detailed. PR #4565 implemented the full feature, but was reverted eight minutes after merging because of performance regressions. The recorded local before/after comparison found long-session pagination +27%, DOM nodes +23%, heap +14%, and Trajectory switching +10%, all within CI budgets. None of the six existing repository benchmarks covered the new hot path: reasoning or tool chunks entering an active group during streaming, group reprojection, and seat re-rendering.
|
||||
|
||||
The review identified implementation choices, not the product requirements, as the problem:
|
||||
|
||||
- The mode became an input to list structure. `ChatNodeList` subscribed to the entire `layout` and generated wrapper components from ranges; Expanded removed those wrappers. Switching modes remounted the list, and regrouping rebuilt wrapper keys.
|
||||
- The mode entered `ChatNodeOwnerProps`. Every seat's owner object changed with the mode, re-rendering all business renderers, including unrelated user messages, tool rows, and compaction markers.
|
||||
- A seat split one Assistant node into derived reasoning and response objects solely to wrap the former in the DOM. Master's `AssistantMarkdown` already had `reasoningHidden` and `revealProcess`, which could hide reasoning independently.
|
||||
- Consumers repeated infrastructure-row filtering in four places, although nodes already had a Definition-owned `visibility` field.
|
||||
|
||||
The deeper difficulty is information ownership: a group header must exist before its members as soon as the group opens, but its content requires a view of the entire Turn. Each event-driven Context receives only its own events. Only a turn-level Context can observe the whole Turn, and it can produce only one node.
|
||||
|
||||
This proposal supplies an architecture for three modes and grouping that satisfies the constraints below and confines later product changes to a small set of nodes.
|
||||
|
||||
The three settings, presentation policy channel, and per-Turn folding eligibility are implemented; step-group remains proposed. The [migration progress table](../feature/2026-09-20-chat-work-details-migration.md) owns current implementation, committed scope, and incomplete items. The grouping state machine and change inventory below do not describe delivered work.
|
||||
|
||||
## Goals and constraints
|
||||
|
||||
The [implemented Definition-owned Build Group foundation](../../implemented/architecture/2026-09-21-conversation-build-groups.md) supersedes this note's event fold, Builder-materialized group headers, Step-range membership, and changedLocationData proposal. This note retains the presentation-policy and per-Turn eligibility rationale; the grouping implementation below records the superseded route, not the current implementation plan.
|
||||
|
||||
The following decisions were confirmed during design review and are inputs to this proposal.
|
||||
|
||||
| Number | Decision | Meaning |
|
||||
|---|---|---|
|
||||
| 1 | Definitions own group structure | A Definition matching multiple events in the Turn folds group opening, closing, member ranges, and summaries; renderers do not derive them by scanning nodes |
|
||||
| 2 | Limit seat re-rendering | Mode changes re-render only seats whose presentation changes; streaming chunks re-render only their seat and group header; user messages, replies, and turn-tail do not re-render for either change |
|
||||
| 3 | One node per Context | Do not introduce multiple nodes per Context; most Contexts have one element, and arrays would fragment memory further |
|
||||
| 4 | Modes do not toggle Definitions | Registry changes rebuild and replay all Contexts, refreshing every seat; Definitions must be mode-independent |
|
||||
| 5 | Decide folding per Turn | A Turn can fold when its own `turn/start` is loaded; the oldest partial Turn stays open and is not optimized here |
|
||||
| 6 | ui-conversation may change | Engine changes are allowed only where needed for this design to work correctly |
|
||||
| 7 | Initial implementation scope | Initially change only code and this note; add tests and other documentation during CI repair |
|
||||
|
||||
Accepting a full-page browser layout on mode changes does not mean accepting every seat re-rendering. Layout is a geometric result; a seat re-render executes a React component function. This proposal constrains the latter.
|
||||
|
||||
## Viable approaches
|
||||
|
||||
There are five approaches to group-header identity and content ownership.
|
||||
|
||||
**Approach A: the first member's seat renders the header.** Keep the list unchanged. The builder derives membership and tells the first member's seat to render a title above itself; other member seats use the existing `hidden` mechanism. This adds no new concept, but creates a third seat state—visible with only a title—and attaches the header to a business node's seat.
|
||||
|
||||
**Approach α: one Context per step supplies a header.** Add a Definition identified by `turn:step`. During construction, read the preceding `assistant-step` data to decide whether this is a new group start and emit a header node if so. Another turn-level Definition publishes the group's content as Turn data for the renderer.
|
||||
|
||||
**Approach β: one Context emits multiple nodes.** A turn-level Definition emits the whole-Turn control and N group headers.
|
||||
|
||||
**Approach γ: a parent Context derives child Contexts.** When a group opens, the turn-level fold creates a `step-group` child Context and forwards later events to it. Each child owns one node. The engine needs forwarding and cascaded replay primitives.
|
||||
|
||||
**Approach δ: Definitions own structure; the builder materializes rows.** A turn-level Definition folds the group list and publishes Turn data. The Chat builder materializes header nodes and assigns membership. Seats use combined selectors for visibility. `ViewBuilder.apply` receives the Location data whose references changed in the flush.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**One Context per group cannot satisfy the matching rules.** The engine requires `match(event)` to derive an id from one event. A group number depends on whether a reply preceded the event; one event cannot determine it. This is incompatible with the matching contract, not merely difficult to implement.
|
||||
|
||||
**Approach α conflicts with decision 2.** Header identity is derivable, but every header reads the same Turn data, so one group's change refreshes all headers in that Turn. Reading later steps through `turn.steps` instead leaves stale references because a header node is not rebuilt when later steps arrive.
|
||||
|
||||
**Decision 3 rules out approach β.** See the table above.
|
||||
|
||||
**Approach γ is not justified yet.** It has the clearest ownership, but forwarding and cascaded replay would add hundreds of lines to the assembler core. It remains an alternative under the conditions at the end of this note.
|
||||
|
||||
**Approach A remains the fallback.** If builder-owned materialization is rejected in practice, use A. Both share the same structural source and differ only in what carries the header row.
|
||||
|
||||
Choose approach δ.
|
||||
|
||||
## Proposal
|
||||
|
||||
### Data flow
|
||||
|
||||
```
|
||||
事件流
|
||||
──▶ step-groups Definition(turn 级 fold,多重匹配本轮事件)
|
||||
──▶ Turn 数据 step-groups:组列表,每组一个对象
|
||||
──▶ Chat builder
|
||||
├─ 物化组头节点:一组一个 key,data 即组对象
|
||||
└─ 成员归属:按 step 映射到 key,写进 per-key presentation
|
||||
──▶ seat:hidden = f(策略字段, 角色, 展开状态)
|
||||
──▶ 组头渲染器:读自己节点的 data
|
||||
展示策略
|
||||
──▶ 渲染器:按 kind 注入的 hooks,选单个字段
|
||||
──▶ seat:组合 selector,选出的是自己的结论
|
||||
```
|
||||
|
||||
The only engine change passes the Location data whose references changed during a flush to the builder through `apply`.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
| Layer | Owns | Explicitly does not own |
|
||||
|---|---|---|
|
||||
| `step-groups` Definition | Opening and closing groups, `headStep`, `closeStep`, tool counts and names, running tools; publishes Turn data | View nodes or identification of member nodes |
|
||||
| Chat builder | Header-node identity and lifecycle, membership, `turnStarted`, identity comparison and publication of per-key presentation | Reading events or deriving business structure |
|
||||
| Presentation policy object | Mapping the enum to capability fields | Node or seat state |
|
||||
| Seat | Combining policy fields, role, and store state into hidden state; supplying group open/close actions to the header renderer | Determining group boundaries |
|
||||
| Header renderer | Title, chevron, click-to-toggle interaction | The source of group content |
|
||||
| assistant-step renderer | Settled reasoning first-line preview; hiding reasoning when closing a group | Group boundaries |
|
||||
|
||||
### Header-node identity and lifecycle
|
||||
|
||||
A header node's key is `step-group:${turn}:${headStep}`. `headStep` is fixed when a group opens, and later events cannot change its first member. The header exists and is reused from that point onward; its key does not change during streaming. Closing the group only changes `closed` in its data from false to true.
|
||||
|
||||
The header anchor is the first member's anchor minus a synthetic offset, placing it immediately before that member. A streaming Assistant is anchored at its first visible chunk, then at its message event after persistence. Its anchor moves once; the header follows in the same structural update without adding another update.
|
||||
|
||||
A header is part of the whole-Turn process and hides with its members when the Turn folds. It is not in `TURN_PROCESS_INDEPENDENT_KINDS`.
|
||||
|
||||
Header data is the group object published by the fold. Closed group objects never change references, so their header sources never notify. Each event that changes a running group replaces only that group's object and re-renders only its header.
|
||||
|
||||
### Fold state machine
|
||||
|
||||
The `step-groups` Definition uses the Turn number as its id, starts on `turn/start`, and matches the same event set as `turn-process`. Its State is `{ turn, groups, open }`: `groups` is an array of group objects, and `open` is the active group's index or empty.
|
||||
|
||||
| Event | Transition |
|
||||
|---|---|
|
||||
| `tool/call` | If no group is open, open one at the event's step; increment the count, record distinct tool names, and add callId to the running set |
|
||||
| Appended `tool/result` | Remove callId from the running set |
|
||||
| `assistant/live-chunk` reasoning delta with visible characters | Open a group at the chunk's step if none is open |
|
||||
| `assistant/live-chunk` text delta or text-block end with visible characters | Close an open group at the chunk's step |
|
||||
| Appended `assistant/message` | Apply the reasoning rule for visible reasoning and the text rule for a visible reply, in reasoning-then-text order |
|
||||
| `turn/end` | Mark an open group as closed by the Turn end |
|
||||
| `step/start`, `step/end`, `llm/retry` | No state change; retry rows belong to no group |
|
||||
|
||||
Three invariants apply:
|
||||
|
||||
- Idempotence: opening an already-open group and closing an already-closed group do nothing. Live chunks and persisted messages describing the same content do not open or close twice; replay with only persisted messages derives the same group list.
|
||||
- Stable references: `update` returns the original object when State is unchanged; the `groups` array changes reference only when the group list changes; closed group objects are never replaced.
|
||||
- Forward-only folding: do not read neighboring Contexts, nodes, or later history.
|
||||
|
||||
Turn-data publication: `buildLocationData` returns a value only for the turn phase and reuses its previous value when the `groups` reference is unchanged, preventing republication. A history window without `turn/start` uses the same fallback as `turn-process`: fold State from the available matches.
|
||||
|
||||
Publication timing: batch live chunks at animation frames; publish other changes immediately.
|
||||
|
||||
### Membership rules
|
||||
|
||||
The builder assigns membership by step, not by seq ranges, because live-chunk seqs and persisted-event seqs use different scales.
|
||||
|
||||
| Node | Rule |
|
||||
|---|---|
|
||||
| `tool-call` at step s | Belongs to the last group with `headStep ≤ s`. If text closes the preceding group in the same step, later tool calls open a new group at that step, so the last group is the applicable one |
|
||||
| `assistant-step` at step s where a group has `closeStep === s` | Closer role: the node remains visible; folding the group hides only its reasoning through the existing `reasoningHidden` mechanism |
|
||||
| Other `assistant-step` at step s | Member role: belongs to the group with `headStep ≤ s` whose `closeStep` has not been passed |
|
||||
| `model-retry`, `turn-error`, `turn-max-tokens`, `turn-tail`, `user`, `steering` | Belong to no group |
|
||||
|
||||
Recompute membership only when the group list changes, with cost proportional to that Turn's node count. Streaming chunks that leave the group list unchanged do not publish member presentation.
|
||||
|
||||
The header anchors before the earliest member. If a group contains only its closer's reasoning and no members, such as reasoning followed by a reply in one step, anchor the header before the closer so the hidden reasoning still has a control that reveals it.
|
||||
|
||||
### Delivering group changes to the builder
|
||||
|
||||
The fold publishes Turn data, not view nodes. When Location data changes, the engine's `flush` sets `timelineDirty` and calls `apply` on all active targets, but the builder cannot identify the changed Turn and key. Inferring them from an unrelated node update in the same Turn and flush is not a reliable guarantee.
|
||||
|
||||
Add optional `changedLocationData` to `ConversationViewBuilder.apply`: the Location values whose references changed in this flush, each carrying `kind`, `turn`, optional `step`, and `key`. The assembler already compares references in `applyDirtyLocationData`; it collects those results for the builder. Chat reprojects only Turns with `key === 'step-groups'`. When the optional input is absent, the builder falls back to Turns affected by the current upserts.
|
||||
|
||||
### Combining hidden state in a seat
|
||||
|
||||
Seats stop receiving `compactTranscript` and `historyIncomplete`. A seat reads policy through `usePresentation`, selecting its own result rather than the mode:
|
||||
|
||||
```ts
|
||||
declare function usePresentation<T>(select: (policy: {
|
||||
stepGrouping: 'collapsed' | 'none'
|
||||
foldCompletedTurns: boolean
|
||||
}) => T): T
|
||||
declare const group: { readonly headStep: number } | undefined
|
||||
|
||||
const grouping = usePresentation(p => group === undefined ? 'none' : p.stepGrouping)
|
||||
const foldCompleted = usePresentation(p => p.foldCompletedTurns)
|
||||
```
|
||||
|
||||
For a seat outside every group, `grouping` is `'none'` in all modes, so it does not re-render. `foldCompletedTurns` likewise stays true in all modes. The selector hook uses `useSyncExternalStoreWithSelector` and re-renders only when the selected value changes.
|
||||
|
||||
Three sources combine into `hidden`:
|
||||
|
||||
| Source | Condition | Reveal action |
|
||||
|---|---|---|
|
||||
| Whole-Turn folding | Existing rule: `turnClosed && turnStarted && has reply`, without manual expansion | Open the Turn |
|
||||
| Group folding | `grouping === 'collapsed'`, member role, group not manually expanded | Open the group |
|
||||
| Group header in Expanded | Header role and `grouping === 'none'` | None |
|
||||
|
||||
The closer's reasoning is hidden independently: the seat passes `stepGroup.reasoningHidden` through owner props to the assistant-step renderer.
|
||||
|
||||
The chat store keeps group expansion under a key combining `turn` and `headStep`, alongside whole-Turn manual expansion.
|
||||
|
||||
### Re-render scope
|
||||
|
||||
The following guarantees also define the next round of render-count assertions. Each row names an exact set, not an upper bound.
|
||||
|
||||
| External change | May re-render |
|
||||
|---|---|
|
||||
| A running group receives a reasoning chunk | That assistant seat |
|
||||
| A running group receives `tool/call` | That tool seat and the group's header |
|
||||
| A reply appears and closes a group | That assistant seat and group header; membership recomputation occurs only when opening or closing groups |
|
||||
| A new group opens | Insert the new header into `order` with one structural layout; no other seat refreshes |
|
||||
| Switch Compact ↔ Detailed | Only assistant-step renderers with settled reasoning |
|
||||
| Switch to or from Expanded | Only member and header seats |
|
||||
| Change `performanceUsage` | Only turn-tail |
|
||||
| Load an earlier page | New seats and historical seats whose presentation changes |
|
||||
| Change locale | All seats, as a control case |
|
||||
|
||||
### Presentation policy object
|
||||
|
||||
Separate the persisted enum from runtime capabilities. `TranscriptViewMode` contains `compact`, `detailed`, and `expanded`; reading the legacy saved value `normal` maps it to `detailed` without writing it back. Runtime capabilities are the derived `ChatPresentationPolicy`:
|
||||
|
||||
| Field | compact | detailed | expanded |
|
||||
|---|---|---|---|
|
||||
| `foldCompletedTurns` | true | true | true |
|
||||
| `stepGrouping` | collapsed | collapsed | none |
|
||||
| `settledReasoningPreview` | false | true | true |
|
||||
|
||||
The mapping has one owner. Each mode maps to a constant object with a stable reference, so the derived observable needs no subscription bookkeeping: `getSnapshot` performs a lookup and `subscribe` forwards to the mode observable.
|
||||
|
||||
Renderers select needed fields instead of comparing enum values. Policy is injected by kind: the assistant-step registration receives it through `inject.hooks.presentation`, just as turn-tail receives `performanceUsage`. Adding a fourth mode changes the table, not the renderers.
|
||||
|
||||
### Per-Turn folding
|
||||
|
||||
Add `turnStarted` to per-key presentation, derived from whether the Turn's `turn/start` is in the loaded window. Folding eligibility uses it instead of global `!historyIncomplete`. Completing pagination changes no global seat prop; only new nodes and presentation in their Turns change.
|
||||
|
||||
### Complexity requirements
|
||||
|
||||
| Scenario | Fold | Builder | Publication |
|
||||
|---|---|---|---|
|
||||
| Content chunk without a group-boundary change | O(1) transition, original object returned | No reprojection | None |
|
||||
| Event that changes a group boundary | O(1) | Reproject only the owning Turn, proportional to its node count | Keys whose facts changed in that Turn |
|
||||
| Structural changes and pagination | Proportional to matched events | One full pass | Keys whose facts changed |
|
||||
| Mode change | None | None | None; seat and renderer selectors determine re-renders |
|
||||
|
||||
## Change inventory
|
||||
|
||||
| Package | File | Change |
|
||||
|---|---|---|
|
||||
| ui-conversation | `contract/conversation.ts` | Add `ConversationLocationDataUpdate` and optional `changedLocationData` to `ConversationViewBuilder.apply` |
|
||||
| ui-conversation | `conversation/assembler.ts` | Collect changed Location references in `applyDirtyLocationData` and pass them through `apply` |
|
||||
| ui-conversation | `index.ts` | Export the new type |
|
||||
| ui-chat | `chat-settings.ts` | Three-mode enum; schema accepts legacy `normal` |
|
||||
| ui-chat | `transcript-view.ts` | Map `normal` to `detailed` on read |
|
||||
| ui-chat | New `presentation-policy.ts` | Policy objects, mapping table, derived observable |
|
||||
| ui-chat | `settings/TranscriptViewRow.tsx` | Three options |
|
||||
| ui-chat | `locale.ts` | Mode labels and group-header copy |
|
||||
| ui-chat | `contract/slots.ts` | `ChatViewInjected.hooks.presentation`, `PresentationInjected`, `ChatNodeOwnerProps.stepGroup`, `StepGroupOwnerProps` |
|
||||
| ui-chat | `contract/snapshot.ts` | `ChatTurnProcessPresentation.turnStarted`, `ChatNodeGroupPresentation`; combine `processSource` values per key |
|
||||
| ui-chat | `contract/chat-nodes.ts` | `StepGroupChatData` |
|
||||
| ui-chat | `contract/store.ts`, `stores.ts` | Manual group-expansion state and actions |
|
||||
| ui-chat | `conversation-nodes/common.ts` | Header anchor offset |
|
||||
| ui-chat | New `conversation-nodes/step-groups.ts` | Fold Definition and Turn-data declaration merging |
|
||||
| ui-chat | New `conversation-nodes/step-group-nodes.ts` | Materialize headers and project membership |
|
||||
| ui-chat | `conversation-nodes/turn-process-presentation.ts` | `turnStarted` and combined per-key presentation |
|
||||
| ui-chat | `conversation-nodes/chat-snapshot-builder.ts` | Integrate materialization, consume `changedLocationData`, support key deletion in the store |
|
||||
| ui-chat | `conversation-nodes/register.ts` | Register the fold Definition |
|
||||
| ui-chat | `chat/ChatView.tsx` | Stop passing `compactTranscript` and `historyIncomplete`; pass `usePresentation` |
|
||||
| ui-chat | `chat/ChatNodeSeat.tsx` | Combined selector, three hiding sources, `stepGroup` owner props |
|
||||
| ui-chat | `chat/AssistantNodeView.tsx`, `chat/AssistantMarkdown.tsx`, `chat/ReasoningRow.tsx` | Inject policy, toggle settled reasoning previews, hide closer reasoning |
|
||||
| ui-chat | New `chat/StepGroupNodeView.tsx` | Header renderer |
|
||||
| ui-chat | `chat/register-node-renderers.ts` | Register header renderer and inject policy into assistant-step |
|
||||
| ui-chat | `apply.ts`, `index.ts` | Create and inject policy observable; export types |
|
||||
|
||||
This inventory retains the complete grouping proposal. Independent features have updated READMEs, i18n pairing files, and the slot catalog. The migration record tracks tests and snapshots; those updates do not complete the outstanding grouping work.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
Grouping validation remains unimplemented; the migration record tracks independent presentation-feature evidence.
|
||||
|
||||
**Render-count isolation.** Add counters to the existing `chat-view.client.spec` mounting approach: React Profiler `onRender` counts per seat, and test renderers registered in `conversation.chat.node` count by kind. Assert the exact sets in the re-render-scope table for each stimulus.
|
||||
|
||||
**Publication counts.** Count notifications from each key's source under the same stimuli and assert the exact set.
|
||||
|
||||
**Fold state-machine tests.** Feed event sequences and assert opening, closing, a new group after text then tools in one step, Turn-end closure, idempotence, unchanged closed-group references, and equivalent replay using only persisted messages.
|
||||
|
||||
**Membership tests.** Given the group list and Turn nodes, assert each key's role.
|
||||
|
||||
**Benchmark coverage.** Add incremental apply with reasoning and tools to `conversation-fold`, and a streaming fixture with tools and reasoning to `long-session-browser`. These provide wall-clock evidence, not a gate.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Builder-owned node creation.** This is the first proposed node creation by a view builder. A builder may only materialize rows from Turn or Step data; it must not read events or derive business structure. Synthetic keys use `step-group:`, while Context keys begin with digits, preventing collisions. Fall back to approach A if this ownership is rejected in practice.
|
||||
- **Header anchor moves once with its first member.** Persisting a streaming Assistant moves its anchor from the first visible chunk to the message event. The header follows in the Assistant's structural update.
|
||||
- **Actual mode differences in this phase.** Compact and Detailed currently differ only in settled reasoning first-line previews. Running detail titles and category summaries are outside this phase; the policy table leaves room for them.
|
||||
- **Pagination and partial Turns.** The oldest partial Turn neither folds nor groups. Membership uses steps and is independent of pagination.
|
||||
- **Header copy.** This phase proposes only processing/completed labels and tool-call counts; category summaries remain product work.
|
||||
- **Streaming chunk step.** `assistant/live-chunk` carries `step`, as used by `turn-process`; a chunk without a step does not open a group.
|
||||
|
||||
## Retained alternative: parent-derived child Contexts
|
||||
|
||||
If builder materialization exposes more ownership problems than expected, add an engine primitive: a Context's `update` may derive a child Context with an explicit kind and id, then forward later matches. Each child owns its node, publication timing, and Location. Parent replay aligns children by id; children no longer derived become hidden under the existing withdrawal rules.
|
||||
|
||||
This returns row identity and content to Definitions with one Context per group and no array fragmentation. It requires assembler forwarding and cascaded replay, and introduces a Context creation path outside `match`. Reconsider it if more than two business features exceed the builder's materialization limits, or if membership requires cases that steps cannot express.
|
||||
-288
@@ -1,288 +0,0 @@
|
||||
# Agent Note: Chat 展示策略通道与过程分组的基础架构
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-20-chat-presentation-policy-and-step-groups.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Chat 的「对话显示」设置在本提案开始时只有两档:`normal` 不折叠、`compact` 在轮次结束后把过程行折进一个整轮控件。设置值的流向是:`TranscriptViewPolicy.mode` 观察量注入 ChatView,ChatView 用 `useTranscriptView` 读成一个布尔值 `compactTranscript`,再作为 prop 传给 `ChatNodeList`,再传给每一个 `ChatNodeSeat`。另一个全局布尔值 `hasMore` 走同一条路,以 `historyIncomplete` 的名字进每个 seat。两个值一变,全部 seat 重渲染一遍,哪怕只有可折叠的那几个输出真的变了。
|
||||
|
||||
产品要把这个设置改成三档:简洁、详细、完全展开;并在简洁和详细两档里把回复之间的连续过程行归成可折叠的组。PR #4565 做过一次完整实现,合并八分钟后因性能回退被整体撤回。合并前后在本机的对照实测:长会话分页耗时 +27%,DOM 节点 +23%,堆内存 +14%,Trajectory 切换 +10%,全部仍在 CI 预算内;仓库现有六个基准没有任何一个覆盖「流式期间 reasoning 或工具 chunk 进入活动分组、分组重投影、seat 重渲染」这条它新增的热路径。
|
||||
|
||||
审阅那次实现,问题不在业务需求,在选型:
|
||||
|
||||
- 模式被当成列表结构的输入。`ChatNodeList` 订阅整表 `layout`,按 range 生成 wrapper 组件;完全展开模式下 wrapper 消失。模式切换等于整棵列表重挂载,任何一次重新分组都重建 wrapper 的 key。
|
||||
- 模式进了 `ChatNodeOwnerProps`。每个 seat 的 owner 对象随模式变,所有业务渲染器跟着重渲染,包括与模式无关的用户消息、工具行、压缩标记。
|
||||
- 同一个 Assistant 节点在 seat 里被拆成 reasoning 与 response 两份派生对象,只为了让 wrapper 在 DOM 上包住前者不包住后者。master 的 `AssistantMarkdown` 早有 `reasoningHidden` 与 `revealProcess`,靠隐藏状态就能只藏 reasoning。
|
||||
- 基础设施行的 Chat 可见性由消费方过滤函数在四处各判一遍,而节点本身有 `visibility` 字段由 Definition 决定。
|
||||
|
||||
再往下一层,是一个信息流上的两难:分组的行要出现在成员之前,从组打开那一刻就该存在;但它要显示的内容,只有看到整轮所有事件的人才知道。事件驱动的 Context 模型里每个 Context 只收自己的事件,能看到整轮的只有 turn 级 Context,而它只能产出一个节点。
|
||||
|
||||
本记录给出一套基础架构,让三档模式与分组落地时满足下面的约束,并让后续产品变化只落在少数节点上。
|
||||
|
||||
三档设置、展示策略通道与按轮折叠资格已实现;step-group 仍是提案。当前实现、已提交范围和未完成项以[迁移进度表](../feature/2026-09-20-chat-work-details-migration.zh.md)为准,下面的分组状态机与改动清单不表示已经交付。
|
||||
|
||||
## 目标与约束
|
||||
|
||||
[已实现的 Definition 分组基础机制](../../implemented/architecture/2026-09-21-conversation-build-groups.zh.md)取代本记录中的事件 fold、Builder 物化组头、Step 范围成员推断及 changedLocationData 提案。本记录保留展示策略与按轮折叠资格的理由;下文分组实现记录已被取代的路线,不是当前实施方案。
|
||||
|
||||
下表是评审阶段已经确认的裁决,本方案不再重新讨论它们。
|
||||
|
||||
| 编号 | 裁决 | 含义 |
|
||||
|---|---|---|
|
||||
| 1 | 分组结构在 Definition 层定义 | 组的开合、成员范围、摘要由一个多重匹配本轮事件的 Definition 折叠出来,不由渲染层扫描节点得到 |
|
||||
| 2 | 有限刷 seat | 切模式只重渲染 presentation 真正变化的 seat;流式 chunk 只重渲染所属 seat 和它所在组的组头;用户消息、回复、turn-tail 在这两种变化下零重渲染 |
|
||||
| 3 | 一个 Context 一个节点 | 不引入「一个 Context 产出多个节点」,理由是绝大多数 Context 只会有一个元素,数组化让内存更零散 |
|
||||
| 4 | 模式不开关 Definition | 注册变化会触发 registry 重建与全量重放,所有 seat 必刷;Definition 必须与模式无关 |
|
||||
| 5 | 折叠按轮判定 | 一轮只要自己的 `turn/start` 已加载就可以折叠;被分页切开的最老一轮保持展开,本轮不优化 |
|
||||
| 6 | 允许改 ui-conversation | 引擎可以改,但只做让方案正确成立所必需的小改动 |
|
||||
| 7 | 初始实现范围 | 初始阶段只写代码与本记录;测试与其他文档在 CI 修复阶段补齐 |
|
||||
|
||||
有一条前置澄清:接受「切模式全屏重排版」不等于接受「所有 seat 重渲染」。浏览器重排是几何结果,seat 重渲染是 React 组件函数重跑,后者才是这里要限制的量。
|
||||
|
||||
## 可行的路线
|
||||
|
||||
围绕「组头这一行的身份从哪来、内容从哪来」,可行的路线有五条。
|
||||
|
||||
**路线 A,头成员的 seat 画组头。** 列表不变,builder 算出分组后告诉第一个成员的 seat「你是组头」,该 seat 在自己上方画标题,其余成员 seat 用现有 `hidden` 机制藏起来。零新概念。缺点是 seat 里出现「自身可见但只显示标题」的第三种状态,且标题寄生在一个业务节点的 seat 里。
|
||||
|
||||
**路线 α,每步一个 Context 当组头。** 增加一个 id 为 `turn:step` 的 Definition,在构建时读上一步的 `assistant-step` 数据判断自己是否新组开头,是则产出组头节点。组的内容由另一个 turn 级 Definition 发布成 Turn 数据供渲染器读取。
|
||||
|
||||
**路线 β,一个 Context 产出多个节点。** turn 级 Definition 一个人产出整轮控件加 N 个组头。
|
||||
|
||||
**路线 γ,父 Context 派生子 Context。** turn 级 fold 在开组时生成一个 `step-group` 子 Context 并把后续事件转发给它,子 Context 有自己的单个节点。引擎需要新增转发原语与重放级联。
|
||||
|
||||
**路线 δ,Definition 拥有结构,builder 物化行。** turn 级 Definition 折叠出组列表并发布为 Turn 数据;Chat builder 据此物化组头节点并给成员写归属;seat 用组合 selector 决定隐藏;引擎给 `ViewBuilder.apply` 的入参加一份「本次 flush 哪些 Location 数据换了引用」。
|
||||
|
||||
## 备选方案与取舍
|
||||
|
||||
**每组一个 Context 无法成立。** 引擎要求 `match(event)` 只看单个事件算出 id,「第几组」取决于前面有没有回复,单个事件算不出。这不是实现难度,是契约上的不可能。
|
||||
|
||||
**路线 α 与裁决 2 冲突。** 组头身份能算出,但内容要读整轮一份 Turn 数据,任何一组变化全轮组头都重渲染。改成去读 `turn.steps` 里后面几步的数据,又碰上组头节点不会因为后面的步到来而重建,读到的是陈旧引用。
|
||||
|
||||
**路线 β 被裁决 3 否决。** 见上表。
|
||||
|
||||
**路线 γ 现在不值。** 它在所有权上最纯,但要给 assembler 加转发与重放级联,是几百行的引擎核心改动。本记录把它保留为备选,条件写在末尾。
|
||||
|
||||
**路线 A 作为退路保留。** 如果 builder 物化节点在实践中被否决,退到 A;两者的结构来源相同,只差组头这一行的承载。
|
||||
|
||||
选择路线 δ。
|
||||
|
||||
## 方案
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
事件流
|
||||
──▶ step-groups Definition(turn 级 fold,多重匹配本轮事件)
|
||||
──▶ Turn 数据 step-groups:组列表,每组一个对象
|
||||
──▶ Chat builder
|
||||
├─ 物化组头节点:一组一个 key,data 即组对象
|
||||
└─ 成员归属:按 step 映射到 key,写进 per-key presentation
|
||||
──▶ seat:hidden = f(策略字段, 角色, 展开状态)
|
||||
──▶ 组头渲染器:读自己节点的 data
|
||||
展示策略
|
||||
──▶ 渲染器:按 kind 注入的 hooks,选单个字段
|
||||
──▶ seat:组合 selector,选出的是自己的结论
|
||||
```
|
||||
|
||||
引擎改动只有一处:assembler 在 flush 时把「哪些 Location 数据换了引用」随 `apply` 入参交给 builder。
|
||||
|
||||
### 各层职责
|
||||
|
||||
| 层 | 拥有 | 明确不拥有 |
|
||||
|---|---|---|
|
||||
| `step-groups` Definition | 组的开合、`headStep`、`closeStep`、工具计数与名称、运行中的工具;作为 Turn 数据发布 | 任何视图节点;成员是哪些节点 |
|
||||
| Chat builder | 组头节点的身份与生命周期;成员归属;`turnStarted`;per-key presentation 的身份比较发布 | 读事件;自己算业务结构 |
|
||||
| 展示策略对象 | 枚举到能力字段的映射 | 任何节点或 seat 状态 |
|
||||
| seat | 用策略字段、角色、store 状态合成 hidden;把组的开合能力交给组头渲染器 | 判断组的边界 |
|
||||
| 组头渲染器 | 标题、箭头、点击开合 | 组的内容来源 |
|
||||
| assistant-step 渲染器 | 是否显示已完成推理的首行预览;作为组的关闭者时隐藏推理部分 | 组的边界 |
|
||||
|
||||
### 组头节点的身份与生命周期
|
||||
|
||||
组头节点的 key 是 `step-group:${turn}:${headStep}`。`headStep` 在组打开那一刻就确定,之后任何事件都改不了「谁是第一个」,所以组头节点从打开起就存在并被复用,运行中不会换 key。组关闭只是它 data 里的 `closed` 从 false 变 true。
|
||||
|
||||
组头节点的锚点是该组头成员节点锚点减去一个合成偏移,位置紧贴在第一个成员之前。头成员的 Assistant 节点在流式期间锚在第一个可见 chunk,落盘后锚在消息事件,锚点会移动一次;组头随之移动一次,这是一次结构性更新,与 Assistant 节点自己的结构性更新同时发生,不额外增加次数。
|
||||
|
||||
组头节点属于整轮过程的一部分:整轮折叠时它随成员一起隐藏。它不在 `TURN_PROCESS_INDEPENDENT_KINDS` 里。
|
||||
|
||||
组头节点的 data 就是 fold 发布的组对象。已关闭的组对象引用永不改变,因此已关闭组头的 source 永不通知;正在运行的组每来一个改变它的事件换一个对象,只有这一组的组头重渲染。
|
||||
|
||||
### fold 状态机
|
||||
|
||||
`step-groups` Definition 的 id 是 turn 号,在 `turn/start` 上启动,匹配与 `turn-process` 同一批事件。State 是 `{ turn, groups, open }`,`groups` 是组对象数组,`open` 是当前打开组的下标或空。
|
||||
|
||||
| 事件 | 转移 |
|
||||
|---|---|
|
||||
| `tool/call` | 没有打开的组则以该事件的 step 开新组;计数加一;记录 distinct 工具名;把 callId 加入运行集合 |
|
||||
| `tool/result` 追加 | 从运行集合移除 callId |
|
||||
| `assistant/live-chunk` 推理 delta 含可见字符 | 没有打开的组则以该 chunk 的 step 开新组 |
|
||||
| `assistant/live-chunk` 文本 delta 或文本块结束含可见字符 | 有打开的组则以该 chunk 的 step 关闭它 |
|
||||
| `assistant/message` 追加 | 消息含可见推理则按推理规则处理,含可见回复则按文本规则处理;顺序是先推理后文本 |
|
||||
| `turn/end` | 有打开的组则标记为被轮次结束关闭 |
|
||||
| `step/start`、`step/end`、`llm/retry` | 不改变状态;重试行不属于任何组 |
|
||||
|
||||
三条不变量:
|
||||
|
||||
- 幂等。开组时已有打开的组则无操作;关组时组已关闭则无操作。live chunk 与落盘消息重复描述同一内容不会开两次或关两次,历史重放只有落盘消息也能得到同样的组列表。
|
||||
- 引用稳定。State 没变时 `update` 返回原对象;`groups` 数组只在组列表变化时换引用;已关闭的组对象永不替换。
|
||||
- 只向前折叠。不读邻居 Context,不读节点,不向后看。
|
||||
|
||||
Turn 数据发布:`buildLocationData` 只在 turn 阶段返回值,`groups` 引用不变时返回上一次发布的值,引擎据此不重新发布。没有 `turn/start` 的历史窗口用与 `turn-process` 相同的兜底:从已有匹配折叠出 State。
|
||||
|
||||
发布节奏:live chunk 走动画帧合并,其余立即。
|
||||
|
||||
### 成员归属规则
|
||||
|
||||
归属由 builder 按 step 判定,不用 seq 区间,因为 live chunk 的 seq 与落盘事件的 seq 不在一个尺度上。
|
||||
|
||||
| 节点 | 规则 |
|
||||
|---|---|
|
||||
| `tool-call`,step 为 s | 属于 `headStep ≤ s` 的最后一组。文本在同一 step 关闭了上一组之后的工具调用,会由 fold 以该 step 开出新组,所以「最后一组」总是正确的那一组 |
|
||||
| `assistant-step`,step 为 s,且某组 `closeStep === s` | 角色为关闭者。节点保持可见;组折叠时只隐藏它的推理部分,用现有 `reasoningHidden` 机制 |
|
||||
| `assistant-step`,step 为 s,其余情况 | 属于 `headStep ≤ s` 且 `s` 未越过 `closeStep` 的组,角色为成员 |
|
||||
| `model-retry`、`turn-error`、`turn-max-tokens`、`turn-tail`、`user`、`steering` | 不属于任何组 |
|
||||
|
||||
归属只在组列表变化时重算,成本是该轮节点数。流式 chunk 不改变组列表时,成员的 presentation 不发布。
|
||||
|
||||
组头行锚在最早的成员之前。一个组如果只有关闭者的推理而没有任何成员,比如一步里先推理后回复,组头行就锚在关闭者之前,这样被隐藏的推理仍然有一行可以点开。
|
||||
|
||||
### 分组变化怎样到达 builder
|
||||
|
||||
fold 只发布 Turn 数据,没有视图节点。engine 的 `flush` 在 Location 数据有变化时会把 `timelineDirty` 置真并对所有活跃目标调用 `apply`,但 builder 不知道是哪一轮的哪个 key 变了。依赖「同一轮总有别的节点也在这次 flush 里变了」去推断,是巧合不是契约。
|
||||
|
||||
所以给 `ConversationViewBuilder.apply` 的入参增加可选的 `changedLocationData`:本次 flush 里换了引用的 Location 数据清单,每项含 `kind`、`turn`、可选 `step`、`key`。assembler 在 `applyDirtyLocationData` 里本来就逐条比较了前后引用,只是把比较结果收集起来交给 builder。Chat builder 只取 `key === 'step-groups'` 的轮次做重投影。入参可选,不传时 builder 退化为按本次 upsert 涉及的轮次重投影。
|
||||
|
||||
### seat 的 hidden 合成
|
||||
|
||||
seat 不再接收 `compactTranscript` 与 `historyIncomplete` 两个 prop。它通过 `usePresentation` 读策略,但选出的不是模式,而是自己的结论:
|
||||
|
||||
```ts
|
||||
declare function usePresentation<T>(select: (policy: {
|
||||
stepGrouping: 'collapsed' | 'none'
|
||||
foldCompletedTurns: boolean
|
||||
}) => T): T
|
||||
declare const group: { readonly headStep: number } | undefined
|
||||
|
||||
const grouping = usePresentation(p => group === undefined ? 'none' : p.stepGrouping)
|
||||
const foldCompleted = usePresentation(p => p.foldCompletedTurns)
|
||||
```
|
||||
|
||||
不在任何组里的 seat,`grouping` 在三档下都是 `'none'`,值没变就不重渲染。`foldCompletedTurns` 在三档下都是 true,同理。selector hook 的底层是 `useSyncExternalStoreWithSelector`,只有选出的值变化才重渲染。
|
||||
|
||||
三个隐藏来源合成一个 `hidden`:
|
||||
|
||||
| 来源 | 条件 | 揭示动作 |
|
||||
|---|---|---|
|
||||
| 整轮折叠 | 现有规则,`turnClosed && turnStarted && 有回复` 且未手动展开 | 打开整轮 |
|
||||
| 组折叠 | `grouping === 'collapsed'`、角色为成员、组未手动展开 | 打开该组 |
|
||||
| 组头在完全展开档 | 角色为组头且 `grouping === 'none'` | 无 |
|
||||
|
||||
关闭者节点的推理部分单独隐藏:seat 通过 owner props 里的 `stepGroup.reasoningHidden` 告诉 assistant-step 渲染器。
|
||||
|
||||
组的展开状态进 chat store,key 是 `turn` 加 `headStep`,与整轮的手动展开状态并列。
|
||||
|
||||
### 刷新范围
|
||||
|
||||
下表是这套结构给出的保证,也是下一轮渲染次数测试的断言表。每一行断言的是精确集合,不是上限。
|
||||
|
||||
| 外部变化 | 允许重渲染的 |
|
||||
|---|---|
|
||||
| 运行中的组来一个推理 chunk | 该 assistant seat |
|
||||
| 运行中的组来一个 `tool/call` | 该 tool seat,加这一组的组头 |
|
||||
| 回复出现,组关闭 | 该 assistant seat,加这一组的组头;成员归属重算只发生在开关组时 |
|
||||
| 新组打开 | 新组头进 `order`,一次结构重排;其他 seat 不刷 |
|
||||
| 模式在简洁与详细之间切换 | 仅有已完成推理的 assistant-step 渲染器 |
|
||||
| 模式切到完全展开或切回 | 仅成员与组头 seat |
|
||||
| `performanceUsage` 切换 | 仅 turn-tail |
|
||||
| 加载更早一页 | 新增 seat,加 presentation 变了的历史 seat |
|
||||
| 语言切换 | 全部,作为对照 |
|
||||
|
||||
### 展示策略对象
|
||||
|
||||
持久化的枚举与运行时词汇分开。枚举 `TranscriptViewMode` 是 `compact`、`detailed`、`expanded`,存档里的旧值 `normal` 读取时映射为 `detailed`,不回写。运行时词汇是派生的 `ChatPresentationPolicy`:
|
||||
|
||||
| 字段 | compact | detailed | expanded |
|
||||
|---|---|---|---|
|
||||
| `foldCompletedTurns` | true | true | true |
|
||||
| `stepGrouping` | collapsed | collapsed | none |
|
||||
| `settledReasoningPreview` | false | true | true |
|
||||
|
||||
映射只存在于一处。每档对应一个常量对象,同一档永远同一引用,所以派生观察量不需要自己的订阅簿记:`getSnapshot` 是查表,`subscribe` 直接转给模式观察量。
|
||||
|
||||
渲染器不比较枚举,只 select 自己需要的字段。策略观察量按 kind 注入:assistant-step 的注册通过 `inject.hooks.presentation` 拿到它,与 turn-tail 注入 `performanceUsage` 是同一写法。以后加第四档只改这张表,渲染器一个不动。
|
||||
|
||||
### 按轮折叠
|
||||
|
||||
per-key presentation 增加 `turnStarted`,来源是该轮 `turn/start` 是否在加载窗口内。seat 的折叠资格由 `turnStarted` 替代原来的全局 `!historyIncomplete`。分页加载完成时不再有任何全局 prop 变化,只有新增节点与它们所在轮的 presentation 变化。
|
||||
|
||||
### 复杂度契约
|
||||
|
||||
| 场景 | fold | builder | 发布 |
|
||||
|---|---|---|---|
|
||||
| 内容型 chunk,不改变组边界 | O(1) 状态转移,返回原对象 | 无重投影 | 零 |
|
||||
| 改变组边界的事件 | O(1) | 只重投影所属轮,成本为该轮节点数 | 该轮内事实变化的 key |
|
||||
| 结构变化与分页 | 按匹配的事件数 | 全量一次 | 事实变化的 key |
|
||||
| 模式切换 | 无 | 无 | 无;重渲染由 seat 与渲染器的 selector 决定 |
|
||||
|
||||
## 改动清单
|
||||
|
||||
| 包 | 文件 | 改动 |
|
||||
|---|---|---|
|
||||
| ui-conversation | `contract/conversation.ts` | 新增 `ConversationLocationDataUpdate`;`ConversationViewBuilder.apply` 入参增加可选 `changedLocationData` |
|
||||
| ui-conversation | `conversation/assembler.ts` | `applyDirtyLocationData` 收集换了引用的 Location 数据并随 `apply` 传出 |
|
||||
| ui-conversation | `index.ts` | 导出新类型 |
|
||||
| ui-chat | `chat-settings.ts` | 三档枚举;schema 接受旧值 `normal` |
|
||||
| ui-chat | `transcript-view.ts` | 读取时 `normal` 映射为 `detailed` |
|
||||
| ui-chat | `presentation-policy.ts` 新增 | 策略对象、映射表、派生观察量 |
|
||||
| ui-chat | `settings/TranscriptViewRow.tsx` | 三个选项 |
|
||||
| ui-chat | `locale.ts` | 三档文案;组头文案 |
|
||||
| ui-chat | `contract/slots.ts` | `ChatViewInjected.hooks.presentation`;`PresentationInjected`;`ChatNodeOwnerProps.stepGroup`;`StepGroupOwnerProps` |
|
||||
| ui-chat | `contract/snapshot.ts` | `ChatTurnProcessPresentation.turnStarted`;`ChatNodeGroupPresentation`;`processSource` 的值改为 per-key 组合对象 |
|
||||
| ui-chat | `contract/chat-nodes.ts` | `StepGroupChatData` |
|
||||
| ui-chat | `contract/store.ts`、`stores.ts` | 组的手动展开状态与动作 |
|
||||
| ui-chat | `conversation-nodes/common.ts` | 组头锚点偏移 |
|
||||
| ui-chat | `conversation-nodes/step-groups.ts` 新增 | fold Definition 与 Turn 数据声明合并 |
|
||||
| ui-chat | `conversation-nodes/step-group-nodes.ts` 新增 | 组头节点物化与成员归属投影 |
|
||||
| ui-chat | `conversation-nodes/turn-process-presentation.ts` | `turnStarted`;组合 per-key presentation |
|
||||
| ui-chat | `conversation-nodes/chat-snapshot-builder.ts` | 接入物化投影;消费 `changedLocationData`;store 支持删除 key |
|
||||
| ui-chat | `conversation-nodes/register.ts` | 注册 fold Definition |
|
||||
| ui-chat | `chat/ChatView.tsx` | 删除 `compactTranscript` 与 `historyIncomplete` 的传递;传 `usePresentation` |
|
||||
| ui-chat | `chat/ChatNodeSeat.tsx` | 组合 selector;三个隐藏来源;`stepGroup` owner props |
|
||||
| ui-chat | `chat/AssistantNodeView.tsx`、`chat/AssistantMarkdown.tsx`、`chat/ReasoningRow.tsx` | 注入策略;已完成推理的预览开关;关闭者的推理隐藏 |
|
||||
| ui-chat | `chat/StepGroupNodeView.tsx` 新增 | 组头渲染器 |
|
||||
| ui-chat | `chat/register-node-renderers.ts` | 注册组头渲染器;给 assistant-step 注入策略 |
|
||||
| ui-chat | `apply.ts`、`index.ts` | 创建策略观察量并注入;导出类型 |
|
||||
|
||||
上述清单保留完整分组方案;独立功能已经更新 README、i18n 配对文件及 slot catalog。测试与快照进度见迁移记录,未完成的分组改动不随这些更新自动完成。
|
||||
|
||||
## 验收标准
|
||||
|
||||
分组相关验收仍待实现;独立展示功能的验证进度见迁移记录。
|
||||
|
||||
**渲染次数隔离测试。** 在现有 `chat-view.client.spec` 的挂载方式上加计数:React Profiler 的 `onRender` 按 seat 计数,测试专用的计数渲染器注册进 `conversation.chat.node` 按 kind 计数。刺激与期望按「刷新范围」一节的表逐行断言精确集合。
|
||||
|
||||
**发布次数测试。** 同样的刺激下统计每个 key 的 source 通知次数,断言精确集合。
|
||||
|
||||
**fold 状态机单测。** 喂事件序列,断言组列表:开组、关组、同 step 先文本后工具开新组、轮次结束关组、幂等性、已关闭组对象引用不变、只有落盘消息的历史重放得到相同组列表。
|
||||
|
||||
**成员归属单测。** 给定组列表与该轮节点,断言每个 key 的角色。
|
||||
|
||||
**基准补位。** `conversation-fold` 增加带推理与工具的增量 apply 场景;`long-session-browser` 增加带工具与推理的流式 fixture。这是墙钟兜底,不是门禁。
|
||||
|
||||
## 风险
|
||||
|
||||
- **builder 产出节点的边界。** 这是这套架构里第一次由视图 builder 产出节点。边界写死为:builder 只能从 Turn 或 Step 数据物化行,不能读事件,不能自己算业务结构。合成 key 用 `step-group:` 前缀,Context key 以数字开头,不会冲突。如果实践中被否决,退到路线 A。
|
||||
- **组头锚点随头成员移动一次。** 头成员是流式 Assistant 时,落盘让锚点从首个可见 chunk 移到消息事件,组头跟着移动一次。与 Assistant 自己的结构性更新同时发生。
|
||||
- **三档在本轮的实际差异。** 简洁与详细目前只差已完成推理的首行预览;运行中的详情标题、类别摘要等产品特性不在本轮。策略表已为它们留了位置。
|
||||
- **分页与半截轮。** 被分页切开的最老一轮不折叠、不分组;归属只按 step 计算,不受分页影响。
|
||||
- **组头文案。** 本轮只有「正在处理」与「已完成处理」加工具调用次数,类别摘要留给产品层。
|
||||
- **流式 chunk 的 step 字段。** `assistant/live-chunk` 事件携带 `step`,与 `turn-process` 的用法相同;若 chunk 缺少 step 则不开组。
|
||||
|
||||
## 备选保留:父 Context 派生子 Context
|
||||
|
||||
如果 builder 物化节点在使用中暴露出比预期更多的边界问题,替代方案是给引擎加一个原语:Context 在 `update` 中可以派生一个指定 kind 与 id 的子 Context,并把后续匹配转发给它;子 Context 有自己的单个节点、自己的发布节奏与 Location。父 Context 重放时子 Context 按 id 对齐,未再派生的子 Context 按现有撤回规则转为隐藏。
|
||||
|
||||
它的收益是行的身份与内容都回到 Definition 层,一组一个 Context,没有数组碎片。代价是 assembler 要新增转发与重放级联,且 `match` 之外多出一条 Context 创建路径。切换条件:builder 物化节点的边界在两个以上业务里被突破,或成员归属的按 step 规则出现无法用 step 表达的用例。
|
||||
@@ -1,6 +0,0 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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/proposed/feature/2026-09-20-chat-work-details-migration.md
|
||||
2026-09-20-chat-work-details-migration.md: 126b61d32d4a4dbf27cb5997149abc9820d7f881
|
||||
2026-09-20-chat-work-details-migration.zh.md: 20727c299307881b67d0662092d269afa9af176c
|
||||
@@ -1,176 +0,0 @@
|
||||
# Agent Note: Chat work-details migration progress
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-09-20-chat-work-details-migration.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
PR #4565 combines three display modes, process grouping, whole-Turn status, trigger notifications, tool-row visuals, and infrastructure-row hiding. Migrating it as one batch mixes independent features with list restructuring and makes individual performance changes difficult to assess. A feature-based progress table allows independent visuals to be reviewed first and Turn-related behavior to be considered with step-group later, without losing product requirements.
|
||||
|
||||
## Proposal
|
||||
|
||||
Completed independent features stay in `feat/chat-presentation-channel`. The remaining unfinished code continues in `.artifacts/worktrees/pr-4565-turnstep` under the repository root, based on the current channel and this progress record, with drafts kept unstaged for combined turn/step and grouping review. The `pr-4565` worktree remains the original PR's behavior and implementation reference; existing `feat/chat-step-groups` experiments are outside this transfer.
|
||||
|
||||
This record tracks feature scope, current implementation, and acceptance progress. The [presentation policy channel and process-group architecture](../architecture/2026-09-20-chat-presentation-policy-and-step-groups.md) retains the architecture and alternatives. This record neither replaces it nor treats its proposals as implemented. Because it does not fully supersede that decision, retain the architecture note without archival or deletion.
|
||||
|
||||
This record maintains an English/Chinese pair. Completion, implementation, staging or commit state, and acceptance are separate dimensions. Incomplete items remain marked incomplete even when drafts exist, and stay in the inventory. Tests and documentation are added during CI repair; browser snapshots and performance acceptance are tracked separately. Written code, staging, commits, and successful builds do not establish complete functional or performance acceptance.
|
||||
|
||||
### Status definitions
|
||||
|
||||
| Dimension | State | Meaning |
|
||||
|---|---|---|
|
||||
| Completion | Complete | Migration code has been reviewed feature by feature and committed to this channel; behavior and performance evidence are separate |
|
||||
| Completion | Incomplete | Outside the channel's completed migration scope; may have an unstaged draft or no implementation |
|
||||
| Implementation | Existing capability | Already present in this channel |
|
||||
| Implementation | Code written | Implementation exists in the relevant worktree; does not imply completion, staging, commit, or acceptance |
|
||||
| Implementation | Awaiting migration | Included in non-grouping scope but not yet implemented |
|
||||
| Implementation | Grouping phase | Requirement retained; depends on grouping structure or interactions and is not connected to this channel yet |
|
||||
| Staging or commit | Unstaged | Changes remain in the worktree, including untracked files |
|
||||
| Staging or commit | Partially staged | Only some feature changes are staged; the exact scope must be stated |
|
||||
| Staging or commit | Staged | All pending code changes for the feature are staged but not committed |
|
||||
| Staging or commit | Committed | Feature code is included in current HEAD |
|
||||
| Acceptance | Pending | Requires behavior, node identity, update-scope, or browser performance evidence; Git state and compilation cannot substitute |
|
||||
|
||||
Verify staging against the feature's diff hunks, not whole files. Inspect the worktree, index, and HEAD when updating status. Staging some fields in a shared file does not stage every feature depending on that file.
|
||||
|
||||
### Capabilities already in this channel
|
||||
|
||||
| Capability | compact | detailed | expanded | Completion | Implementation | Staging or commit | Acceptance |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Three settings and legacy-value reading | Compact | Detailed; legacy `normal` reads as this mode without write-back | Expanded | Complete | Existing capability | Committed | Pending |
|
||||
| Per-Turn folding eligibility | A closed Turn with its start loaded and a final reply can fold process content; cancellation/failure extensions are item 6 | Same | Same | Complete | Existing capability | Committed | Pending |
|
||||
| Collapsed settled-reasoning summary | Think title only | First line | First line | Complete | Existing capability | Committed | Pending |
|
||||
| Retain summary DOM across mode changes | CSS hides summary and separator | Show existing summary and separator | Same | Complete | Existing capability | Committed | Pending |
|
||||
| Local reasoning-preview subscription | `ReasoningRow` selects its own visibility | Same | Same | Complete | Existing capability | Committed | Pending |
|
||||
| Stable disclosure-row references | Callbacks, icons, and unchanged summaries retain references; full Markdown mounts only on expansion | Same | Same | Complete | Existing capability | Committed | Pending |
|
||||
|
||||
All three modes retain whole-Turn folding. `expanded` removes group-level folding, headers, and height limits while retaining group identity and member parents; it does not disable whole-Turn folding or automatically expand reasoning and tool-card bodies. Until grouping is connected, Chat presents `detailed` and `expanded` identically.
|
||||
|
||||
### Migration inventory without grouping dependencies
|
||||
|
||||
“All three modes” means the behavior applies in every mode, not that switching modes changes it. “Without grouping dependencies” means the draft introduces no step-group, not that every item must be accepted before grouping. Numbers follow the feature review inventory; item 0's compilation wiring is complete and committed, so it is not listed as a product feature. Incomplete drafts remain in `pr-4565-turnstep`, outside this channel's completed features. Behavior and performance acceptance remain pending for the features below.
|
||||
|
||||
| Number | Feature | compact | detailed | expanded | Completion | Implementation | Staging or commit | Destination and considerations |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | Chinese tool titles | All three modes | All three modes | All three modes | Complete | Code written | Committed | Bash/Pwsh: “运行命令”; Grep: “搜索文件内容”; Glob: “查找文件”; Skill: “加载技能”. Only UI dictionaries change; model tool names do not |
|
||||
| 2 | Tool-row interaction visuals | All three modes | All three modes | All three modes | Complete | Code written | Committed | Hover darkens titles and summaries in tool, Bash, and command rows; shared `DisclosureRow` and independent Bash rows point the expanded chevron upward |
|
||||
| 3 | Bash stopped-state screen-reader text | All three modes | All three modes | All three modes | Complete | Code written | Committed | Complete the stopped-state accessible name without changing execution or cancellation semantics |
|
||||
| 4 | Running tool/command text shimmer | All three modes | All three modes | All three modes | Complete | Code written | Both 4A shimmer and 4B accompanying optimizations committed | 4A includes a fixed `TextShimmer` span, CSS activation, tool/command integration, replacement of old sweep styles, and reduced-motion support, excluding step/turn. 4B adds business-row computation caches, stable callbacks, and JSX references |
|
||||
| 5 | Streaming reasoning summary | Update after a paragraph's first line completes | Same | Same | Complete | Code written | Committed | `ReasoningRow` retains the original PR's `latestCompletedParagraphFirstLine(text)` and computes from full text. Retain summary DOM, stable props, and right-edge fading; add no Definition parsing state or cross-layer summary fields |
|
||||
| 6 | Keep process expanded after cancellation/failure | All three modes | All three modes | All three modes | Incomplete | Draft code written | Unstaged | Draft uses `keepOpen` in Turn process projection; folding eligibility and stopped/failed behavior await Turn/grouping review |
|
||||
| 7 | Whole-Turn status and duration control | All three modes | All three modes | All three modes | Incomplete | Draft code written | Unstaged | Draft anchors the control to loaded `turn/start` after opening inputs, isolates timing in `LiveTurnProcessLabel`, and replaces the bottom activity bar; positioning and status await review |
|
||||
| 8 | Non-human trigger notifications | All three modes | All three modes | All three modes | Incomplete | Draft code written | Unstaged | Draft identifies waking inputs from next-turn and first-step next-step claims and projects `turn-trigger`; Turn starts, notification ownership, and expanded content await review |
|
||||
| 9 (permission/system) | Hide permission-command and system-prompt rows | All three modes | All three modes | All three modes | Complete | Code written | Committed | `isVisibleChatNode` centralizes filtering for the existing Chat list, navigation, and process presentation. Command and request-prompt Definitions do not own these filters; persistence and Trajectory are unchanged |
|
||||
| 9 (context) | Hide ordinary Context rows | All three modes | All three modes | All three modes | Incomplete | Draft code written | Unstaged | Paired with item 8's waking notifications to retain source explanations; final filter ownership awaits Turn/grouping review |
|
||||
| 10 | Turn action-bar positioning | All three modes | All three modes | All three modes | Incomplete | Draft code written | Unstaged | Draft anchors `turn-tail` after `turn/end`; reply ownership for feedback/copy and the real Turn-end seq for branching await combined review |
|
||||
| 11 | Action-bar persistent visibility | All three modes | All three modes | All three modes | Incomplete | Draft code written | Unstaged | Draft projects `footer` per Turn in the Chat builder: persistently visible when the latest Turn ends in a reply, otherwise on hover/focus; tail detection will be considered with grouping |
|
||||
|
||||
The current base already enables developer tools by default; do not migrate that again. Retain the protection that waits for Host acceptance of settings instead of applying the original PR's missing-value fallback. Its `Switch` change only affects a comment and is not a migrated feature.
|
||||
|
||||
### Subsequent review batches
|
||||
|
||||
Review batches are independent of implementation and staging status. Review remaining drafts in `pr-4565-turnstep`, kept unstaged; a draft does not complete a migration. Transfer only this channel's remaining code, without automatically incorporating `feat/chat-step-groups` experiments or installing dependencies and compiling in the destination worktree.
|
||||
|
||||
| Batch | Features | Review scope |
|
||||
|---|---|---|
|
||||
| Independent feature, committed | 4A: text shimmer | `TextShimmer`, row integration, and browser painting cost; performance acceptance pending |
|
||||
| Independent optimization, committed | 4B: accompanying memoization and reference stability | `ToolRow`, `BashRow`, and `GenericCommandCard` computation caches, callbacks, and JSX references; not required for shimmer itself; performance acceptance pending |
|
||||
| Independent feature, committed | Permission-command and system-prompt hiding from item 9 | Retain the original PR's centralized visibility decision; without step-group, the main list connects it in `orderedVisibleChatNodes`, independent of waking-message classification |
|
||||
| Incomplete, transferred to turnstep worktree | 6, 7, 10, 11 | Whole-Turn folding eligibility, status/clock, action-bar position, and tail detection, considered with Turn/group relationships |
|
||||
| Incomplete, transferred to turnstep worktree | 8 and ordinary Context hiding from item 9 | Trigger notifications depend on Turn starts and Inbox claims; Context hiding accompanies notifications so waking reasons remain visible |
|
||||
|
||||
### Requirements retained for the grouping phase
|
||||
|
||||
All items below remain incomplete and retain their full requirements for the turn/step and grouping phase. The [Definition-owned grouping foundation](../../implemented/architecture/2026-09-21-conversation-build-groups.md) is implemented; that does not establish completion of these product behaviors or performance requirements.
|
||||
|
||||
| Feature | compact | detailed | expanded | Current state |
|
||||
|---|---|---|---|---|
|
||||
| Consecutive process groups between replies | Group tools and reasoning, initially collapsed | Same | Show process rows without group-level folding; retain member parents | Incomplete (grouping phase) |
|
||||
| Independent reasoning/reply visibility within one Assistant | Reasoning may join the preceding group; reply stays visible | Same | No secondary group hiding | Incomplete; do not directly copy the original split into two render nodes |
|
||||
| Header category summary | Current category while running; aggregate main categories after completion | Same | No header | Incomplete (grouping phase) |
|
||||
| Running header details | Hidden | Show command, path, query, etc.; reasoning summary when no tool is running | No header | Incomplete (grouping phase) |
|
||||
| Header update cadence | Preserve category title's minimum display duration | Also handle detail updates | Not applicable | Incomplete (grouping phase) |
|
||||
| Height-limited scrolling within a group | Limit expanded height, scroll independently, fade edges | Same | Not applicable | Incomplete (grouping phase) |
|
||||
| Group and whole-Turn folding interaction | Hide groups when the Turn folds | Same | Whole-Turn folding only | Incomplete; retain node identity and separately decide whether internal expansion resets |
|
||||
| Pagination, streaming settlement, and group identity | Stable existing headers and members; handle partial Turns separately | Same | Show the same business nodes directly | Incomplete (grouping phase) |
|
||||
|
||||
Grouping details to verify: completed summaries aggregate at most three categories and use “etc.” beyond three; call counts determine category ordering rather than appearing as header numbers. Detailed-mode live details collapse whitespace and are capped at 160 characters. Titles remain for at least 150ms; retain only the latest update without replaying queued updates. Expanded height is at most the smaller of 400px and half the viewport. Scrollable edges use a 24px fade and preserve interaction with outer scrolling. Retry rows are outside secondary groups. These values describe the original PR's reference behavior, not completed grouping experiments.
|
||||
|
||||
### Definition-owned segmentation and presentation
|
||||
|
||||
Business State accepts each input, clears pending changes, and returns repeatable output. Replace builds the loaded window; apply separates structural changes from content updates and maintains affected segments or Turn-local indexes. It owns group opening/closing, stable keys, summaries, and invalidation; generic infrastructure never corrects those decisions by Node kind or Step number.
|
||||
|
||||
| Ordered visible input | Business action |
|
||||
|---|---|
|
||||
| Owning Turn changes | Close the pending group. |
|
||||
| No Turn ownership | Close the group and emit an independent Node; never merge across it. |
|
||||
| turn-process control | Emit independently without closing the pending process. |
|
||||
| User, steering, trigger, retry, error, max-tokens, or tail | Close the group and emit independently. |
|
||||
| Assistant reasoning | Append the reasoning part to the pending group. |
|
||||
| Assistant visible reply | Close the group and emit the response part independently. Later tools enter a later group. |
|
||||
| Other process Nodes | Append the Node to the pending group; an ordinary Step boundary does not close it. |
|
||||
| Turn end | Close that Turn's pending groups even without Node upserts. |
|
||||
|
||||
Content growth with unchanged classification does not resegment history; it may update only the current summary. A first reply, new tool, steering, or retry can change structure. Visibility/location repair handles both old and new ownership. Real structural changes may linearly merge root references; the interface does not promise all operations are constant-time.
|
||||
|
||||
Group headers select data independently from member arrays. All modes retain the same Group container and member parents:
|
||||
|
||||
| Mode | Group header | Body |
|
||||
|---|---|---|
|
||||
| Compact | Short activity summary | Group open/closed state controls visibility. |
|
||||
| Detailed | Summary plus live command/path/query details | Same group open/closed state. |
|
||||
| Expanded | Hidden | Visible without group-level collapse or height cap; wrapper remains. |
|
||||
|
||||
This assumes the outer Turn process is open. Expanded retains whole-Turn folding and individual tool/reasoning disclosure; it must not eagerly mount all full Markdown. Turn-close resets of internal state are explicit business actions, not key changes. Group headers, scrolling/fades, and Turn interaction belong to this business integration.
|
||||
|
||||
| Business implementation | Responsibility |
|
||||
|---|---|
|
||||
| process-groups.ts and register.ts | Definition-owned segmentation, membership, caches, summaries, and registration. |
|
||||
| AssistantNodeView, Group seat, stores, navigation | Interpret parts; group header/body presentation; local open state; Turn/group reveal, selection/copy and interruption ownership. |
|
||||
|
||||
## Performance constraints
|
||||
|
||||
| Scenario | Property to preserve | Required evidence |
|
||||
|---|---|---|
|
||||
| Switching among modes | Business-node keys do not change; modes do not re-register Definitions; only relevant visibility/presentation fields update | Node identity and update-scope checks |
|
||||
| Incremental reasoning/tool content | Update only the owning node without scanning full history; compute reasoning summary from current full text during rendering | Update scope and long-text cost; the current summary algorithm does not reduce `ReasoningRow` render count |
|
||||
| Whole-Turn running clock | Only live clock content updates each second, without refreshing members, historical rows, or list | Update scope and timer disposal |
|
||||
| Summary visibility | Keep lightweight summaries mounted; do not mount every full Markdown body for mode switching | DOM identity, initial loading, memory observations |
|
||||
| Pagination | Update new nodes and nodes with changed facts; completed Turns do not refresh together because of global pagination state | Multi-page interaction and update-scope checks |
|
||||
| State transitions | Stable text/icon/callback references when content is unchanged; do not clear internal state through remounting | Interaction state and component lifecycle checks |
|
||||
|
||||
Retaining DOM reduces mounting and unmounting cost, not style calculation, layout, or painting, and can increase retained memory. Changing display properties alone is not a guarantee against jank. The aim is lower switching peaks with acceptable initial-load, pagination, and streaming costs.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Replay the original PR as one batch.** Rejected: grouping, node splitting, whole-Turn status, and independent visuals would mix, obscuring each feature's performance effects and overwriting local subscriptions and node reuse already completed in this channel.
|
||||
|
||||
**Unmount and recreate summaries or process nodes on mode changes.** Rejected for summary mode changes. Stable mounts allow repeated switching to change only display state; initial costs for new nodes still require separate validation.
|
||||
|
||||
**Keep every Markdown body mounted for smooth switching.** Rejected: lightweight summaries and full bodies differ in memory, DOM, and rendering cost. Bodies remain mounted according to manual expansion.
|
||||
|
||||
**Remount renderers when folding a Turn to reset tool cards.** Do not migrate directly. It conflicts with stable nodes and preserved interaction state. If reset behavior remains necessary, define its state owner and trigger explicitly.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
Update implementation, staging, and commit status separately as work progresses. Completion follows feature review and commits; acceptance follows actual validation. Retain incomplete items across branch splits and worktree transfers. Before grouping work starts, check every product requirement and mode mapping above.
|
||||
|
||||
| Acceptance item | Current state |
|
||||
|---|---|
|
||||
| Complete behavior validation of existing channel capabilities | Relevant unit and browser regressions pass; this excludes unmigrated grouping features and does not establish performance acceptance |
|
||||
| Non-grouping feature migration | Completed scope is in the inventory; incomplete drafts await review in `pr-4565-turnstep`; functional and performance validation remain incomplete |
|
||||
| DOM identity and update scope during mode changes | Unit tests verify summary/expanded-body DOM identity and policy-field subscriptions; browsers verify persistence of all modes and whole-Turn folding; whole-page update scope and performance remain pending |
|
||||
| Long-session initial loading, pagination, streaming, and memory | Pending; the original PR's historical measurements do not validate this implementation |
|
||||
| Unit, browser, and recorded-snapshot coverage | 785 relevant unit tests pass; read-only replay of 35 browser files yields 178 passing tests and 4 conditional skips; UI expectations are updated and Session JSONL is unchanged |
|
||||
| Grouping and the Detailed/Expanded distinction | Incomplete (grouping phase) |
|
||||
|
||||
Validation evidence: after rebasing onto master, this channel passed full `pnpm run build`, lint, duplication checks, and all 41 documentation checks. New presentation policy, shimmer, and visibility filtering, plus the settings and Skill rows reported by CI, pass 100% coverage checks over their selected source scope. Test repairs, UI snapshots, and bilingual pairing are committed together as CI fixes. No install, build, or tests ran in `pr-4565-turnstep`; this channel's results do not establish acceptance for that worktree. A real-model GUI demo and performance measurements remain outstanding.
|
||||
|
||||
## Risks
|
||||
|
||||
- `expanded` and `detailed` currently present identically; the original PR's distinction exists only after grouping is implemented.
|
||||
- Whole-Turn duration replaces the existing tool/message-count title, changing product information. Migration must preserve status, accessible names, and Turns with no process content, not merely restyle the control.
|
||||
- Hiding ordinary Context before adding trigger notifications removes background waking reasons; connect both items in the inventory's order.
|
||||
- The original footer decision depends on grouping indexes. Rework ownership per feature rather than introducing the whole grouping infrastructure for a footer.
|
||||
- Group identity, membership, and builder materialization in the experiments still require evaluation; this record does not treat the experiment as final architecture.
|
||||
@@ -1,176 +0,0 @@
|
||||
# Agent Note: Chat 工作过程展示迁移进度
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-09-20-chat-work-details-migration.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
PR #4565 同时包含三档展示设置、过程分组、整轮状态、触发通知、工具行视觉和基础设施行隐藏。整批迁移会把独立功能与列表重组混在一起,也难以逐项判断性能变化。需要一份按功能维护的进度表,先评审独立视觉功能,再结合 step-group 统一考虑轮次相关行为,避免遗漏原有产品需求。
|
||||
|
||||
## 方案
|
||||
|
||||
已完成的独立功能保留在 `feat/chat-presentation-channel`。剩余未完成代码的后续工作区是仓库根目录下的 `.artifacts/worktrees/pr-4565-turnstep`,以当前通道及本进度记录为基线,草稿保持未暂存;后续结合 turn/step 与分组整体评审。`pr-4565` worktree 继续作为原 PR 的行为和实现参考;`feat/chat-step-groups` 的既有分组实验不在本次搬迁范围内。
|
||||
|
||||
本记录管理功能范围、当前实现和验收进度。[展示策略通道与过程分组的基础架构](../architecture/2026-09-20-chat-presentation-policy-and-step-groups.zh.md)保留架构路线及备选方案;本记录不替代它,也不把其中的方案视为已经实现。新记录没有完整取代既有架构决策,因此保留原记录,不做归档或删除。
|
||||
|
||||
本记录维护中英文配对。完成状态、实现状态、暂存或提交状态、验收状态分别记录。未完成项即使已有代码草稿,也继续标为未完成,不从清单移除。测试和文档随 CI 修复补齐,浏览器快照与性能验收单独记录;代码写好、进入暂存区、提交或构建通过,都不等同于完整功能或性能验收。
|
||||
|
||||
### 状态定义
|
||||
|
||||
| 维度 | 状态 | 含义 |
|
||||
|---|---|---|
|
||||
| 完成 | 已完成 | 功能迁移代码已逐项审阅并提交到当前通道;行为与性能证据另行记录 |
|
||||
| 完成 | 未完成 | 尚未纳入当前通道的已完成迁移范围;可以已有未暂存草稿,也可以尚未编写 |
|
||||
| 实现 | 当前代码已有 | 当前通道的既有基础能力 |
|
||||
| 实现 | 代码已写好 | 对应工作区中已有实现;不代表已完成、已暂存、已提交或通过验收 |
|
||||
| 实现 | 待迁移 | 已纳入非分组迁移范围,当前尚未实现 |
|
||||
| 实现 | 分组阶段 | 产品需求保留,依赖分组结构或分组交互,暂不接入当前通道 |
|
||||
| 暂存或提交 | 未暂存 | 该功能的改动仍在工作区,包括尚未跟踪的新文件 |
|
||||
| 暂存或提交 | 部分暂存 | 该功能只有部分改动进入暂存区,必须注明具体范围 |
|
||||
| 暂存或提交 | 已暂存 | 该功能当前待提交的代码改动均已进入暂存区,尚未提交 |
|
||||
| 暂存或提交 | 已提交 | 该功能代码已包含在当前 HEAD 中 |
|
||||
| 验收 | 待验收 | 需要行为、节点身份、刷新范围或浏览器性能证据;不能用 Git 状态或编译结果代替 |
|
||||
|
||||
暂存状态按产品功能对应的差异片段核对,不按整个文件推断。更新状态时同时查看工作区、暂存区和 HEAD;共享文件只暂存部分字段,不能将依赖该文件的所有功能都标为已暂存。
|
||||
|
||||
### 当前通道已有的能力
|
||||
|
||||
| 能力 | compact | detailed | expanded | 完成状态 | 实现状态 | 暂存或提交状态 | 验收状态 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 三档设置与旧值读取 | 简洁 | 详细;旧值 `normal` 读作本档,不回写 | 完全展开 | 已完成 | 当前代码已有 | 已提交 | 待验收 |
|
||||
| 按轮判断整轮折叠资格 | 已加载起点且存在最终回复的结束轮次,可收起过程内容;停止和失败的扩展行为见迁移项 6 | 相同 | 相同 | 已完成 | 当前代码已有 | 已提交 | 待验收 |
|
||||
| 已完成推理的收起摘要 | 只显示「思考」 | 显示首行 | 显示首行 | 已完成 | 当前代码已有 | 已提交 | 待验收 |
|
||||
| 模式切换时保留摘要 DOM | 用 CSS 隐藏摘要及分隔点 | 显示原有摘要及分隔点 | 相同 | 已完成 | 当前代码已有 | 已提交 | 待验收 |
|
||||
| 推理预览的局部订阅 | `ReasoningRow` 选择自己的可见性 | 相同 | 相同 | 已完成 | 当前代码已有 | 已提交 | 待验收 |
|
||||
| 折叠行引用稳定 | 回调、图标及未变化的摘要保持引用;完整 Markdown 仅在展开时挂载 | 相同 | 相同 | 已完成 | 当前代码已有 | 已提交 | 待验收 |
|
||||
|
||||
三档都保留整轮折叠。`expanded` 取消组级折叠、组头和限高,保留组身份与成员父级;并非禁止整轮折叠,也不是自动展开每个思考或工具卡的正文。分组尚未接入时,`detailed` 与 `expanded` 的 Chat 展示相同。
|
||||
|
||||
### 不依赖分组的迁移清单
|
||||
|
||||
下表的“三档共用”表示三档都应用该行为,不表示切换档位时会改变它。“不依赖分组”指实现草稿未引入 step-group,不代表都要先于分组接受。编号沿用逐项评审清单;编号 0 的编译接线修正已完成并提交,不作为产品功能另列。未完成项的草稿留在 `pr-4565-turnstep`,不是当前通道已完成的功能。下列功能的行为与性能均待验收。
|
||||
|
||||
| 编号 | 功能 | compact | detailed | expanded | 完成状态 | 实现状态 | 暂存或提交状态 | 迁移落点与注意事项 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | 工具标题中文化 | 三档共用 | 三档共用 | 三档共用 | 已完成 | 代码已写好 | 已提交 | Bash/Pwsh 为「运行命令」、Grep 为「搜索文件内容」、Glob 为「查找文件」、Skill 为「加载技能」;只改 UI 字典,不改模型工具名 |
|
||||
| 2 | 工具行交互视觉 | 三档共用 | 三档共用 | 三档共用 | 已完成 | 代码已写好 | 已提交 | 工具行、Bash 行和命令行悬停时标题及摘要加深;共享 `DisclosureRow` 和独立 Bash 行展开箭头朝上 |
|
||||
| 3 | Bash 停止状态的读屏提示 | 三档共用 | 三档共用 | 三档共用 | 已完成 | 代码已写好 | 已提交 | 补齐停止状态的可访问名称,不改变执行或取消语义 |
|
||||
| 4 | 工具及命令运行时文字扫光 | 三档共用 | 三档共用 | 三档共用 | 已完成 | 代码已写好 | 4A 扫光与 4B 附带优化均已提交 | 4A 包含 `TextShimmer` 固定 span 与 CSS 启停、工具及命令行接入、旧扫光样式替换和减少动态效果支持,不含 step/turn;4B 为业务行额外的计算缓存、稳定回调及 JSX 引用 |
|
||||
| 5 | 流式推理摘要 | 段落首行写完后才更新 | 相同 | 相同 | 已完成 | 代码已写好 | 已提交 | `ReasoningRow` 沿用原 PR 的 `latestCompletedParagraphFirstLine(text)`,直接从完整文本计算;摘要 DOM、稳定 props 和右侧渐隐保留,不增加 Definition 解析状态或跨层摘要字段 |
|
||||
| 6 | 取消、失败后保持过程展开 | 三档共用 | 三档共用 | 三档共用 | 未完成 | 代码草稿已写好 | 未暂存 | 草稿在 Turn 过程投影中使用 `keepOpen`;轮次与分组阶段再评审折叠资格和停止、失败行为 |
|
||||
| 7 | 整轮状态与用时控件 | 三档共用 | 三档共用 | 三档共用 | 未完成 | 代码草稿已写好 | 未暂存 | 草稿将控件锚定到已加载的 `turn/start`、排在开场输入之后,以 `LiveTurnProcessLabel` 单独计时,并替换底部运行条;整体定位与状态待评审 |
|
||||
| 8 | 非人工触发通知 | 三档共用 | 三档共用 | 三档共用 | 未完成 | 代码草稿已写好 | 未暂存 | 草稿使用 next-turn 及首步 next-step 领取事实识别唤醒输入,投影 `turn-trigger`;轮次起点、通知归属和展开内容待评审 |
|
||||
| 9(permission/系统) | 隐藏 permission 命令与系统提示词行 | 三档共用 | 三档共用 | 三档共用 | 已完成 | 代码已写好 | 已提交 | `isVisibleChatNode` 集中判断,现有 Chat 列表、导航和过程展示复用;命令与请求提示词 Definition 不承载这些过滤规则,持久化数据与 Trajectory 不变 |
|
||||
| 9(context) | 隐藏普通上下文行 | 三档共用 | 三档共用 | 三档共用 | 未完成 | 代码草稿已写好 | 未暂存 | 与 8 的唤醒通知配套,保留来源解释;过滤规则的最终归属留到轮次与分组阶段 |
|
||||
| 10 | 轮次操作栏位置 | 三档共用 | 三档共用 | 三档共用 | 未完成 | 代码草稿已写好 | 未暂存 | 草稿将 `turn-tail` 锚定到 `turn/end` 之后;反馈、复制的回复归属和分支的真实轮次结束序号待整体评审 |
|
||||
| 11 | 操作栏常显条件 | 三档共用 | 三档共用 | 三档共用 | 未完成 | 代码草稿已写好 | 未暂存 | 草稿由 Chat builder 按 Turn 投影 `footer`;最新轮以回复结尾时常显,其余悬停或聚焦显示,尾部判定与分组一起考虑 |
|
||||
|
||||
开发者工具默认开启已由当前基线提供,不重复迁移。当前实现保留等待 Host 接受设置后才启用相关能力的保护,不以原 PR 的缺值回退覆盖它。原 PR 的 `Switch` 改动只有注释,不作为功能迁移项。
|
||||
|
||||
### 后续评审批次
|
||||
|
||||
评审批次独立于实现和暂存状态。未完成草稿的后续评审统一在 `pr-4565-turnstep`,保持未暂存;已有草稿不代表迁移已完成。仅搬迁当前通道的剩余代码,不自动合入 `feat/chat-step-groups` 的实验,也不在目标工作区安装依赖或编译。
|
||||
|
||||
| 批次 | 功能 | 评审范围 |
|
||||
|---|---|---|
|
||||
| 独立功能,已提交 | 4A:文字扫光 | `TextShimmer` 本身、各行的调用方式和浏览器绘制成本;性能待验收 |
|
||||
| 独立优化,已提交 | 4B:附带的 memo 与引用稳定化 | `ToolRow`、`BashRow` 和 `GenericCommandCard` 的计算缓存、回调和 JSX 引用;不是扫光功能的必要条件,性能待验收 |
|
||||
| 独立功能,已提交 | 9 中的 permission 命令与系统提示词隐藏 | 沿用原 PR 的集中可见性判断;当前无 step-group,主列表改在 `orderedVisibleChatNodes` 接入,不依赖唤醒消息分类 |
|
||||
| 未完成,转入 turnstep 工作区 | 6、7、10、11 | 整轮折叠资格、状态与时钟、操作栏位置和尾部判定,统一考虑轮次与分组关系 |
|
||||
| 未完成,转入 turnstep 工作区 | 8、9 中的普通 context 隐藏 | 触发通知依赖轮次起点和 Inbox 领取判定;隐藏普通上下文与通知配套,避免先隐藏唤醒原因 |
|
||||
|
||||
### 分组阶段保留的需求
|
||||
|
||||
这些项目均未完成,继续保留全部需求,后续在 turn/step 与分组阶段处理。[Definition 拥有分组的基础机制](../../implemented/architecture/2026-09-21-conversation-build-groups.zh.md)已经实现,但不代表这些产品行为或性能要求已经完成。
|
||||
|
||||
| 功能 | compact | detailed | expanded | 当前状态 |
|
||||
|---|---|---|---|---|
|
||||
| 回复之间的连续过程分组 | 工具与推理归组,默认收起 | 相同 | 过程行不做组级折叠,保留成员父级 | 未完成(分组阶段) |
|
||||
| 同一 Assistant 中推理与回复的分别显隐 | 推理可归入前组,回复外露 | 相同 | 不做二级分组隐藏 | 未完成;不直接复制原版拆成两个渲染节点的实现 |
|
||||
| 组头类别摘要 | 运行时显示当前类别,完成后聚合主要类别 | 相同 | 无组头 | 未完成(分组阶段) |
|
||||
| 运行中组头详情 | 不显示 | 显示命令、路径、查询等详情,无运行工具时显示推理摘要 | 无组头 | 未完成(分组阶段) |
|
||||
| 组头内容更新节奏 | 保留类别标题的最短展示时间 | 同时处理详情更新 | 不适用 | 未完成(分组阶段) |
|
||||
| 组内限高滚动 | 展开内容限高、独立滚动、边缘渐隐 | 相同 | 不适用 | 未完成(分组阶段) |
|
||||
| 分组与整轮折叠联动 | 整轮收起时隐藏分组 | 相同 | 仅整轮折叠 | 未完成;保留节点身份,内部开合状态是否重置单独确定 |
|
||||
| 分页、流式结算与组身份 | 既有组头和成员稳定,半截轮单独处理 | 相同 | 同一批业务节点直接展示 | 未完成(分组阶段) |
|
||||
|
||||
分组产品细节的核对项:完成摘要最多聚合前三类,超过三类使用「等」;调用计数用于分类排序,不直接显示为组头数字。详细模式的实时详情合并空白并限制为 160 个字符;标题最短展示时间为 150ms,只保留最新更新,不补播积压项。展开区高度不超过 400px 与半屏中的较小值,可滚动方向显示 24px 渐隐,并保留与外层滚动的衔接。重试行不属于二级过程组。这些数值是原 PR 的行为参考,不代表当前分组实验已经实现。
|
||||
|
||||
### Definition 拥有的分段与展示
|
||||
|
||||
业务 State 接受输入、清空待输出变化并返回可重复读取的结果。replace 构建已加载窗口;apply 区分结构变化和正文更新,维护受影响段或轮次内索引。它拥有开组、关组、稳定 key、摘要和失效规则;通用层不按 Node kind 或 Step 编号再次修正业务结论。
|
||||
|
||||
| 有序可见输入 | 业务动作 |
|
||||
|---|---|
|
||||
| 所属 Turn 改变 | 关闭待完成组。 |
|
||||
| 没有 Turn 归属 | 关闭组并输出独立 Node,不跨越它合并。 |
|
||||
| turn-process 控件 | 独立输出,不关闭待完成过程。 |
|
||||
| 用户、steering、触发、重试、错误、最大 token 或尾部 | 关闭组并独立输出。 |
|
||||
| Assistant 推理 | 把 reasoning 部分加入待完成组。 |
|
||||
| Assistant 可见回复 | 关闭组,独立输出 response 部分;后续工具进入后面的组。 |
|
||||
| 其他过程 Node | 加入待完成组,普通 Step 边界不关闭组。 |
|
||||
| Turn 结束 | 即使没有节点 upsert,也关闭该轮次待完成的组。 |
|
||||
|
||||
分类未变的正文增长不重新分段历史,必要时只更新当前摘要。首次回复、新工具、steering 或重试可以改变结构。可见性或位置修正同时处理旧、新归属。真实结构变化允许线性合并根引用,接口不承诺全部操作常数时间。
|
||||
|
||||
组头独立于成员数组选择 data,所有模式保留同一 Group 容器及成员父级:
|
||||
|
||||
| 模式 | 组头 | 正文 |
|
||||
|---|---|---|
|
||||
| Compact | 简短活动摘要 | 按组开合状态显隐。 |
|
||||
| Detailed | 摘要及实时命令、路径、查询详情 | 使用同一组开合状态。 |
|
||||
| Expanded | 隐藏 | 显示,取消组级折叠及限高,容器保留。 |
|
||||
|
||||
以上以外层 Turn 过程已展开为前提。Expanded 保留整轮折叠和单个工具、推理行开合,不能提前挂载全部完整 Markdown。Turn 收起时的内部状态重置是明确业务动作,不通过更换 key 实现。组头、滚动渐隐及整轮联动属于本业务接入。
|
||||
|
||||
| 业务实现 | 职责 |
|
||||
|---|---|
|
||||
| process-groups.ts 与 register.ts | Definition 拥有分段、成员、缓存、摘要及注册。 |
|
||||
| AssistantNodeView、Group 容器、stores、navigation | 解释部分内容,组头和正文展示,本地开合状态,Turn/Group 揭示、选择复制及中断提示归属。 |
|
||||
|
||||
## 性能约束
|
||||
|
||||
| 场景 | 要保留的性质 | 所需证据 |
|
||||
|---|---|---|
|
||||
| 切换三档模式 | 不改变业务节点 key,不因模式切换重新注册 Definition;仅相关可见性或展示字段更新 | 节点身份和刷新范围检查 |
|
||||
| 推理或工具内容增量更新 | 只更新所属节点,不扫描完整历史;推理摘要在渲染时从当前完整文本计算 | 更新范围与长内容计算开销;当前摘要算法不减少 `ReasoningRow` 的渲染次数 |
|
||||
| 整轮运行时钟 | 只有运行中的时钟内容按秒更新,不带动整轮成员、历史行和列表 | 更新范围及计时器释放检查 |
|
||||
| 摘要显隐 | 轻量摘要常驻;完整 Markdown 正文不为模式切换而全部常驻 | DOM 身份、首次加载和内存观察 |
|
||||
| 分页 | 新增节点与事实变化的节点更新,已完成轮次不因全局分页状态一起刷新 | 多页历史交互与刷新范围检查 |
|
||||
| 状态切换 | 文字、图标、回调引用在内容未变时稳定;不通过重挂载清空内部状态 | 交互状态与组件生命周期检查 |
|
||||
|
||||
保留 DOM 只减少挂载和卸载成本,不消除样式计算、布局与绘制,也会增加常驻内存。不能将“只改显示属性”写成“不会卡顿”的保证。性能目标是降低切换峰值,并保持首次加载、分页和流式更新的成本可接受。
|
||||
|
||||
## 备选方案与取舍
|
||||
|
||||
**整批重放原 PR。** 不采用。分组、节点拆分、整轮状态和独立视觉功能混在一起,难以把性能变化对应到具体功能,也会覆盖当前通道已完成的局部订阅与节点复用。
|
||||
|
||||
**切换模式时卸载、重新创建摘要或过程节点。** 摘要模式切换不采用该方式。稳定挂载让来回切换只改变显示状态;新增节点的初始成本仍须单独检查。
|
||||
|
||||
**为了切换平滑,把所有 Markdown 正文常驻。** 不采用。轻量摘要与完整正文的内存、DOM 和渲染成本不同;正文继续按手动展开状态挂载。
|
||||
|
||||
**收起整轮时重挂载 renderer,以重置工具卡。** 不直接迁移。该实现与稳定节点及保留交互状态的方向冲突;若后续仍需要重置行为,应明确状态所有者和触发规则。
|
||||
|
||||
## 验收标准
|
||||
|
||||
每项代码写好、暂存或提交后,分别更新对应状态;完成状态按逐项评审和提交记录更新,验收只按实际验证记录更新。未完成项保留,不因拆分分支或搬迁工作区而删除。分组阶段开始前,逐行复核上面的产品需求和模式对应关系。
|
||||
|
||||
| 验收项 | 当前状态 |
|
||||
|---|---|
|
||||
| 当前通道已有能力的完整行为验证 | 已通过本次相关单元和浏览器回归;不包含未迁移的分组功能,也不代表性能验收 |
|
||||
| 非分组功能迁移 | 完成范围见迁移清单;未完成草稿留到 `pr-4565-turnstep` 继续评审,功能与性能验证仍未完成 |
|
||||
| 模式切换时的 DOM 身份和刷新范围 | 单元测试已验证摘要、手动展开正文的 DOM 身份和策略字段订阅;浏览器已验证三档持久化及整轮折叠,整页刷新范围与性能仍待验收 |
|
||||
| 长会话首次打开、分页、流式输入与内存 | 待验收;原 PR 的历史测量不代表本实现 |
|
||||
| 新行为的单元、浏览器与录制快照覆盖 | 相关单元测试 785 项通过;35 个浏览器文件只读回放,178 项通过、4 项按条件跳过;UI 预期输出已更新,Session JSONL 未改动 |
|
||||
| 分组功能与两档差异 | 未完成(分组阶段) |
|
||||
|
||||
验证记录:当前通道在 rebase master 后通过完整 `pnpm run build`、lint、重复代码检查和完整文档检查(41 项)。新增展示策略与扫光、可见性过滤,以及 CI 报告的设置行和 Skill 行覆盖率已通过对应源码范围的 100% 检查。测试修复、UI 快照和中英配对随 CI 修复一并提交;`pr-4565-turnstep` 未执行安装、构建或测试,当前通道的结果不代表该工作区已验收。真实模型 GUI 演示和性能测量仍待补。
|
||||
|
||||
## 风险
|
||||
|
||||
- 当前 `expanded` 与 `detailed` 的展示相同;只有分组阶段完成后,两档才具备原 PR 设计的区别。
|
||||
- 整轮用时控件替换现有工具和消息计数标题,属于产品信息变化;迁移时不能只换样式而遗漏状态、可访问名称或空过程轮次。
|
||||
- 普通上下文隐藏若先于触发通知,会让后台唤醒原因消失;两项需按清单顺序衔接。
|
||||
- 原 PR 的页脚判定与分组索引耦合,需要按功能重写归属,不能为页脚单独引入整套分组基础设施。
|
||||
- 分组实验中的组身份、成员归属和 builder 物化路线仍需继续评估;本记录不将实验实现列为最终架构。
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: agent-experience
|
||||
description: Use when designing agent tools, skills, context loading, or multi-step workflows to make information discoverable and use context efficiently.
|
||||
metadata:
|
||||
date: "2026-09-11"
|
||||
---
|
||||
|
||||
- **Start with minimal context:** expose purpose, available actions, and constraints first; load detailed instructions when needed.
|
||||
- **Make discovery explicit:** every deferred resource needs a clear description and a reliable way to retrieve it.
|
||||
- **Keep critical constraints visible:** permissions, destructive effects, and required validation should appear before the relevant action.
|
||||
- **Prefer bounded outputs:** return concise results with identifiers or paths for retrieving details; avoid dumping entire logs or documents.
|
||||
- **Use locality:** include a bounded amount of likely needed adjacent context with an operation’s result. For example, deliver a thread reply with a few preceding messages. Mark omissions and truncation explicitly, and provide a way to retrieve more.
|
||||
- **Evaluate total work:** saving context is useful only if it does not cause more searches, repeated reads, or mistakes.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: dsh-client-ui-ux
|
||||
description: Design and review DeepSeek Harness client UI changes — visual token discipline, reuse-before-adding, feedback surfaces (toast vs in-place notice vs empty state), overlay and menu safety, platform window adaptation, loading states, and error copy placement. Use when adding or changing product-user-visible GUI behavior in packages/client, or when reviewing such a PR.
|
||||
---
|
||||
|
||||
# DeepSeek Harness Client UI/UX
|
||||
|
||||
This skill is guidance, not a complete checklist. It covers judgment calls that lint, typecheck, and the i18n gate cannot make: where feedback appears, what gets reused, and how surfaces behave at window edges and across platforms. Copy quality itself follows [dsh-prose-standard](../dsh-prose-standard/SKILL.md); locale ownership follows the [locale-owned copy decision](../../notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md); styling ownership and token rules live in [docs/web-styling.md](../../../docs/web-styling.md) and the [ui-primitives component catalog](../../../packages/client/ui-primitives/README.md#component-catalog) — this skill adds judgment on top of those rules, not a second copy of them.
|
||||
|
||||
## Visual foundation
|
||||
|
||||
- **Use existing tokens for color, radius, and spacing.** Before introducing a literal value, search the target page and its nearest established sibling (the Plugins page is the usual reference for full-page surfaces) for the token or value already in use, and adopt it. The same applies to whole treatments — toasts, menus, in-message artifact previews, sidebar tab layout, colors, clearance rules, page padding: reuse the established component or rule wherever one exists instead of inventing a parallel one.
|
||||
- **Verify every color in both light and dark mode.** An ink/background pair must stay readable under both themes before it ships. Dimmed tones are the standing counter-example: they suit decorative surfaces, but on text they routinely fall below readability in one theme — never put a dimmed color on text without checking both.
|
||||
- **Font weight tops out at 500 in feature CSS you add or change.** Do not use 600+ for emphasis; differentiate with size or ink instead. The markdown typography tokens owned by ui-theme are the standing exception.
|
||||
- **Pick font sizes from the theme typography variables when an existing role matches, otherwise from the page's existing scale.** Parallel content shares one size; never invent a size for a single element, and keep the number of distinct sizes on a page small (13px is the established row/detail size). When the page itself has no precedent, borrow the size and spacing ratios of the most similar existing page or element.
|
||||
- **Icons, text, and neighbouring elements stay proportionate and aligned.** Every element lines up with its siblings along a deliberate axis (baseline, vertical center, or shared edge), and an icon's size stays in proportion to the text beside it; never ship an element that aligns to nothing or visibly outweighs its neighbours.
|
||||
|
||||
## Reuse before adding
|
||||
|
||||
- **Extend an existing component, container, or interaction before creating a new one.** A new capability that fits an existing surface (an extra menu item, a new prop on an existing primitive) beats a parallel element stacked beside it.
|
||||
- **Icons come from the existing icon library.** Pick the semantically closest existing icon; a new glyph requires designer approval before it lands — when reviewing a PR that adds one, suggest requesting a designer review.
|
||||
- **Right-sidebar content registers a slot in the existing sidebar**, never a separate sidebar. Every sidebar tab declares a tab icon matching its meaning.
|
||||
- **Icon-only actions whose meaning is not obvious get a Tooltip.** Informational content the pointer must rest on or select uses HoverCard instead. Prefer these shared primitives over ad-hoc title attributes.
|
||||
|
||||
## Feedback surfaces
|
||||
|
||||
Choose the surface by the lifetime of the message relative to the surface that produced it:
|
||||
|
||||
- **Transient operation outcomes use the app-wide Toast primitive**, not in-place notices, and the toast's state and rendering live in a host that outlives the reporting surface (a `shell.overlay` entry, as RowActionToast does) — a toast rendered by the panel itself unmounts with that panel. A deletion can close the panel or tab that requested it; only a toast held outside that surface survives the close. Report success and failure; do not report still-pending states.
|
||||
- **A failed operation keeps the data visible.** A failed deletion keeps its row; the toast announces the failure. Never blank content to show an error for a transient operation.
|
||||
- **In-place notices are for states tied to the surface itself**: a query failure with its Retry action, field validation on the form that owns the field. A populated panel keeps a compact notice beside its retained content; only an empty panel centers a full error state.
|
||||
- **Error copy is plain language, and short.** No internal jargon or technical nouns unless an error code must be exposed for debugging. Prefer one clear sentence; anything explainable in two sentences must not grow into paragraphs. A Chinese notice of at most two sentences omits the trailing 句号(。). The notice must not break the existing layout — reserve its space or overlay it; never shift neighbouring elements.
|
||||
|
||||
## Loading states
|
||||
|
||||
- Lists use their skeleton; every other page-level load centers a bare spinner. Never place a spinner in a corner of the page.
|
||||
- One loading treatment per page/component — concurrent regions must not each flash their own style.
|
||||
- No loading text unless the context needs it (narrow sidebars almost never do).
|
||||
|
||||
## Overlays and menus
|
||||
|
||||
Every menu, popover, and tooltip must be verified for all three before merge:
|
||||
|
||||
- **Dismissable**: outside click (and Escape where focusable) closes it.
|
||||
- **Viewport-fitting**: it slides or flips to stay inside the window with an edge margin, and flips only into a side that actually fits (no oscillation when neither side fits).
|
||||
- **Unclipped**: it escapes `overflow` ancestors and containing blocks (portal to body where needed) and is never covered by neighbouring elements.
|
||||
|
||||
Scrollbars stay inside their container: use the shared scrollbar styles rather than component-specific selectors, and verify the bar neither overflows the container nor gets clipped by its rounded corners or edges — keep a small inset instead of letting it sit flush against the container edge.
|
||||
|
||||
## Platform window adaptation
|
||||
|
||||
- Any new header element or full page must account for macOS and Windows window chrome. On macOS the titlebar clearance is `--dsh-frame-top-clearance`, consumed by Menu, Modal, and overlay primitives as their top safe margin — **never change the global variable to fix one page**. Add a scoped `[data-platform='darwin']` override on the page's own inset instead, and assert the override in the page's stylesheet spec.
|
||||
- State the intended total offset (clearance + page inset) when reviewing, so two pages aligning to "the same top spacing" agree on the sum, not just one addend.
|
||||
|
||||
## Spacing review
|
||||
|
||||
- Review every changed region for gaps: nothing sits flush against its neighbour without an intentional gap, and adjacent icons and elements keep the region's established spacing.
|
||||
- Prefer symmetry for parallel elements; an unexplained one-off gap or offset usually signals a missed shared value.
|
||||
|
||||
## Design review escalation
|
||||
|
||||
- A PR with extensive UI changes, or any clearly user-perceivable change to UI or interaction, must get a designer or product review. When no approver holds that role, suggest requesting one from the design/product reviewers (roster as of 2026-09: yx zhang, yifffan, zhangziya, gaokaige; update this list here when membership changes).
|
||||
@@ -14,6 +14,7 @@ description: Use when reviewing a pull request in the deepseek-harness repo —
|
||||
- [docs/AGENTS.md](../../../docs/AGENTS.md): documentation placement and prose discipline.
|
||||
- [dsh-prose-standard](../dsh-prose-standard/SKILL.md): required coverage and editorial judgment for comments, docs, prompts, and visible strings.
|
||||
- [dsh-ci-test-reliability](../dsh-ci-test-reliability/SKILL.md): isolation and regression-proof rules for resource-owning, asynchronous, or flaky tests and fixtures.
|
||||
- [dsh-client-ui-ux](../dsh-client-ui-ux/SKILL.md): feedback-surface choice, overlay safety, platform window adaptation, and visual token discipline for product-user-visible GUI changes.
|
||||
- [docs/testing.md](../../../docs/testing.md) and the [quality-gates Agent Note](../../notes/implemented/process/2026-06-11-quality-gates.md): required test tiers and gates.
|
||||
- [Agent Notes](../../notes/README.md): design rationale. Treat disagreement with an Agent Note as a design discussion, not an automatic veto.
|
||||
- For bilingual changes, read [translation-rules.md](../../../docs/i18n/translation-rules.md) and [terminology.md](../../../docs/i18n/terminology.md); the extended translation skill is outside automatic review and runs only on explicit user invocation.
|
||||
|
||||
@@ -165,8 +165,8 @@ jobs:
|
||||
|
||||
# The persistent VM image owns Playwright's Linux system packages; this
|
||||
# step also proves that browser provisioning remains usable for failover.
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium
|
||||
- name: Install Playwright Chromium and WebKit
|
||||
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium webkit
|
||||
|
||||
- name: Prepare bubblewrap (unrestrict userns)
|
||||
run: bash scripts/prepare-ci-bubblewrap.sh
|
||||
|
||||
@@ -344,18 +344,18 @@ jobs:
|
||||
r'https://\1/', source.read_text()))
|
||||
PY
|
||||
|
||||
- name: Install Playwright Chromium and hosted dependencies
|
||||
- name: Install Playwright Chromium, WebKit, and hosted dependencies
|
||||
if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
|
||||
# Ephemeral runner images can carry Ubuntu mirrors whose HTTP endpoints are unreachable.
|
||||
run: |
|
||||
sudo find /etc/apt -maxdepth 2 -type f \( -name '*.list' -o -name '*.sources' \) -exec sed -i -E 's@http://([a-z]{2}\.)?archive\.ubuntu\.com/ubuntu@https://archive.ubuntu.com/ubuntu@g; s@http://security\.ubuntu\.com/ubuntu@https://security.ubuntu.com/ubuntu@g' {} +
|
||||
pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium
|
||||
pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium webkit
|
||||
|
||||
# The persistent VM image owns Playwright's Linux system packages; do
|
||||
# not mutate the shared host with apt on every failover run.
|
||||
- name: Install Playwright Chromium on the failover VM
|
||||
- name: Install Playwright Chromium and WebKit on the failover VM
|
||||
if: vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium
|
||||
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium webkit
|
||||
|
||||
- name: Build and execute the Linux Office runtime
|
||||
run: CI=true pnpm run prepare:primary-runtime --target linux-x64 --output "$RUNNER_TEMP/office-runtime" --cache "$RUNNER_TEMP/office-runtime-downloads"
|
||||
|
||||
@@ -126,7 +126,7 @@ Real-API tests/demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and
|
||||
|
||||
## Conventions
|
||||
|
||||
- Every npm package is `@deepseek-ai/dsh-<name>`; vendored packages are rescoped ([mapping](docs/rescope.md)) and `private: true`. `@deepseek-ai/cordis` is a peerDependency (+ dev) of every harness package.
|
||||
- Packages use `@deepseek-ai/dsh-<name>`; vendor is [rescoped](docs/rescope.md) and `private: true`. Harness packages declare `@deepseek-ai/cordis` in `peerDependencies`/`devDependencies`. Workspace dependency sections use DSH `workspace:*`, vendor/native `workspace:~` ([rules](.agents/notes/implemented/process/2026-09-22-workspace-release-ranges.md)).
|
||||
- ESM everywhere (`"type": "module"`). Use package names across packages and `.ts` in local relative imports. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). The `dsh` CLI source launch runs through tsx's ESM-only hook (`node --import tsx/esm`); modules it reaches must stay ESM (no CJS-only exports) — Node's native TypeScript modes are unavailable across the engines range ([source-launch contract](.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md)). Raw/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces it.
|
||||
- **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer.
|
||||
- **Runtime invariants assert owned relationships.** Publish `./invariant` only when independent observations can diverge. Otherwise omit its source and wiring and record why in its README; empty installers and checks of service presence, plugin metadata, effects, or fixed examples are invalid ([package invariant rules](packages/AGENTS.md)).
|
||||
|
||||
@@ -75,6 +75,7 @@ External packages installed for runtime use or distributed inside the prebuilt b
|
||||
| [`acorn`](https://github.com/acornjs/acorn) | MIT |
|
||||
| [`ajv`](https://github.com/ajv-validator/ajv) | MIT |
|
||||
| [`anser`](https://github.com/IonicaBizau/anser) | MIT |
|
||||
| [`big.js`](https://github.com/MikeMcl/big.js) | MIT |
|
||||
| [`buffer`](https://github.com/feross/buffer) | MIT |
|
||||
| [`chokidar`](https://github.com/paulmillr/chokidar) | MIT |
|
||||
| [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) | Apache-2.0 |
|
||||
@@ -142,6 +143,7 @@ pnpm applies local patches to the following packages at install time, so shipped
|
||||
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
- `@fortune-sheet/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
- `@yao-pkg/[email protected]` — [`patches/@[email protected]`](patches/@[email protected])
|
||||
- `[email protected]` — [`patches/[email protected]`](patches/[email protected])
|
||||
- `[email protected]` — [`patches/[email protected]`](patches/[email protected])
|
||||
|
||||
## Official Claude Code platform payloads
|
||||
@@ -188,6 +190,7 @@ External packages **directly declared** for development, tests, types, or toolin
|
||||
| [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT |
|
||||
| [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT |
|
||||
| [`@types/babel__code-frame`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/big.js`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/compression`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
| [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
|
||||
|
||||
@@ -50,6 +50,10 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_config_editor
|
||||
plugin_dsh_base_settings["settings<br/>@deepseek-ai/dsh-settings"]
|
||||
cfg --> plugin_dsh_base_settings
|
||||
plugin_dsh_base_authorization["authorization<br/>@deepseek-ai/dsh-authorization"]
|
||||
cfg --> plugin_dsh_base_authorization
|
||||
plugin_dsh_base_deepseek_account["deepseek-account<br/>@deepseek-ai/dsh-deepseek-account-platform"]
|
||||
cfg --> plugin_dsh_base_deepseek_account
|
||||
plugin_dsh_base_credentials["credentials<br/>@deepseek-ai/dsh-credentials-local"]
|
||||
cfg --> plugin_dsh_base_credentials
|
||||
plugin_dsh_base_llm_pi_ai["llm-pi-ai<br/>@deepseek-ai/dsh-llm-pi-ai"]
|
||||
@@ -213,6 +217,8 @@ flowchart LR
|
||||
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
|
||||
| `config-editor` | `@deepseek-ai/dsh-config-editor` |
|
||||
| `settings` | `@deepseek-ai/dsh-settings` |
|
||||
| `authorization` | `@deepseek-ai/dsh-authorization` |
|
||||
| `deepseek-account` | `@deepseek-ai/dsh-deepseek-account-platform` |
|
||||
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
|
||||
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
|
||||
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
|
||||
|
||||
+116
-116
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
|
||||
"version": "0.1.6-alpha.2",
|
||||
"version": "0.1.7-alpha.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -20,127 +20,127 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-hmr": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-include": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-timer": "workspace:^",
|
||||
"@deepseek-ai/dsh-acp-app": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-instructions": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-tool-presentation": "workspace:^",
|
||||
"@deepseek-ai/dsh-app-boot": "workspace:^",
|
||||
"@deepseek-ai/dsh-base": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-cmdline": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-compact": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-compaction-basic": "workspace:^",
|
||||
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^",
|
||||
"@deepseek-ai/dsh-cordis-client-runner": "workspace:^",
|
||||
"@deepseek-ai/dsh-experimental-agent-team-profile": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal-round-driver": "workspace:^",
|
||||
"@deepseek-ai/dsh-headless": "workspace:^",
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-hooks-claude-code": "workspace:^",
|
||||
"@deepseek-ai/dsh-hooks-codex": "workspace:^",
|
||||
"@deepseek-ai/dsh-jobs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-launch-environment": "workspace:^",
|
||||
"@deepseek-ai/dsh-mcp-client": "workspace:^",
|
||||
"@deepseek-ai/dsh-persona": "workspace:^",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-schedule": "workspace:^",
|
||||
"@deepseek-ai/dsh-sdk-app": "workspace:^",
|
||||
"@deepseek-ai/dsh-sdk-minimal": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill-office": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:^",
|
||||
"@deepseek-ai/dsh-terminal": "workspace:^",
|
||||
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-time-context": "workspace:^",
|
||||
"@deepseek-ai/dsh-tmux-context": "workspace:^",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-bash-persistent": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-present": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs-search": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-jobs": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-pwsh": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-todo": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-web": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
|
||||
"@deepseek-ai/dsh-web-app": "workspace:^",
|
||||
"@deepseek-ai/dsh-webhook": "workspace:^",
|
||||
"@deepseek-ai/dsh-webhook-github": "workspace:^",
|
||||
"@deepseek-ai/dsh-workflow-ptc": "workspace:^",
|
||||
"@deepseek-ai/schemastery": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-hmr": "workspace:*",
|
||||
"@deepseek-ai/cordis-plugin-include": "workspace:~",
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
|
||||
"@deepseek-ai/cordis-plugin-timer": "workspace:~",
|
||||
"@deepseek-ai/dsh-acp-app": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-instructions": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-tool-presentation": "workspace:*",
|
||||
"@deepseek-ai/dsh-app-boot": "workspace:*",
|
||||
"@deepseek-ai/dsh-base": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-agent-preset": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-cordis": "workspace:*",
|
||||
"@deepseek-ai/dsh-cmdline": "workspace:*",
|
||||
"@deepseek-ai/dsh-command-compact": "workspace:*",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:*",
|
||||
"@deepseek-ai/dsh-compaction-basic": "workspace:*",
|
||||
"@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:*",
|
||||
"@deepseek-ai/dsh-cordis-client-runner": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-agent-team-profile": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-goal": "workspace:*",
|
||||
"@deepseek-ai/dsh-goal-round-driver": "workspace:*",
|
||||
"@deepseek-ai/dsh-headless": "workspace:*",
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:*",
|
||||
"@deepseek-ai/dsh-hooks-claude-code": "workspace:*",
|
||||
"@deepseek-ai/dsh-hooks-codex": "workspace:*",
|
||||
"@deepseek-ai/dsh-jobs-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-launch-environment": "workspace:*",
|
||||
"@deepseek-ai/dsh-mcp-client": "workspace:*",
|
||||
"@deepseek-ai/dsh-persona": "workspace:*",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:*",
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-pwsh-sandbox": "workspace:*",
|
||||
"@deepseek-ai/dsh-schedule": "workspace:*",
|
||||
"@deepseek-ai/dsh-sdk-app": "workspace:*",
|
||||
"@deepseek-ai/dsh-sdk-minimal": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:*",
|
||||
"@deepseek-ai/dsh-skill": "workspace:*",
|
||||
"@deepseek-ai/dsh-skill-filesystem": "workspace:*",
|
||||
"@deepseek-ai/dsh-skill-office": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:*",
|
||||
"@deepseek-ai/dsh-terminal": "workspace:*",
|
||||
"@deepseek-ai/dsh-terminal-bash": "workspace:*",
|
||||
"@deepseek-ai/dsh-time-context": "workspace:*",
|
||||
"@deepseek-ai/dsh-tmux-context": "workspace:*",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-bash": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-bash-persistent": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-cordis": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-present": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-fs": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-fs-search": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-goal": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-jobs": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-pwsh": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-ralph": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-skill": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-subagent-control": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-todo": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-web": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-workflow": "workspace:*",
|
||||
"@deepseek-ai/dsh-web-app": "workspace:*",
|
||||
"@deepseek-ai/dsh-webhook": "workspace:*",
|
||||
"@deepseek-ai/dsh-webhook-github": "workspace:*",
|
||||
"@deepseek-ai/dsh-workflow-ptc": "workspace:*",
|
||||
"@deepseek-ai/schemastery": "workspace:~",
|
||||
"commander": "^15.0.0",
|
||||
"js-yaml": "^4.2.0",
|
||||
"node-addon-require-builtin": "^0.1.6",
|
||||
"@deepseek-ai/dsh-http-proxy": "workspace:^",
|
||||
"@deepseek-ai/dsh-mcp-resources": "workspace:^",
|
||||
"@deepseek-ai/dsh-plugin-manager": "workspace:^",
|
||||
"@deepseek-ai/dsh-atomic-write": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-preset": "workspace:^",
|
||||
"@deepseek-ai/dsh-experimental-voice-input-bundle": "workspace:^"
|
||||
"@deepseek-ai/dsh-http-proxy": "workspace:*",
|
||||
"@deepseek-ai/dsh-mcp-resources": "workspace:*",
|
||||
"@deepseek-ai/dsh-plugin-manager": "workspace:*",
|
||||
"@deepseek-ai/dsh-atomic-write": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-preset": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-voice-input-bundle": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@agentclientprotocol/sdk": "1.4.0",
|
||||
"@deepseek-ai/dsh-acp": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
|
||||
"@deepseek-ai/dsh-attachment-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^",
|
||||
"@deepseek-ai/dsh-experimental-agent-team": "workspace:^",
|
||||
"@deepseek-ai/dsh-experimental-ptc-runtime-python": "workspace:^",
|
||||
"@deepseek-ai/dsh-experimental-tool-agent-team": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-observation-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-frontend-static": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-mock-server": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-replay": "workspace:^",
|
||||
"@deepseek-ai/dsh-loader-smoke": "workspace:^",
|
||||
"@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-log-deepseek": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:^",
|
||||
"@deepseek-ai/dsh-shell-env": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent-fork-in-process": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/dsh-acp": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:*",
|
||||
"@deepseek-ai/dsh-attachment-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-credentials-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-agent-team": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-ptc-runtime-python": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-tool-agent-team": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-observation-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-sandbox": "workspace:*",
|
||||
"@deepseek-ai/dsh-host-frontend-static": "workspace:*",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-mock-server": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-replay": "workspace:*",
|
||||
"@deepseek-ai/dsh-loader-smoke": "workspace:*",
|
||||
"@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:*",
|
||||
"@deepseek-ai/dsh-sandbox-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-log-deepseek": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:*",
|
||||
"@deepseek-ai/dsh-shell-env": "workspace:*",
|
||||
"@deepseek-ai/dsh-settings": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-fork-in-process": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:*",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:*",
|
||||
"@deepseek-ai/dsh-tools": "workspace:*",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:*",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/ws": "8.18.1",
|
||||
"ajv": "^8.20.0",
|
||||
|
||||
@@ -36,7 +36,7 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
|
||||
process.send({ type: 'booting', packageManager: options.packageManager });
|
||||
return new Promise((resolve, reject) => process.once('disconnect', () => {
|
||||
if (${String(fail)}) { reject(new Error('fixture boot failure')); return; }
|
||||
resolve({ ctx: { plugin: async () => {}, effect: () => {}, on: () => {},
|
||||
resolve({ ctx: { plugin: async () => {}, effect: () => {}, on: () => {}, inject: () => {},
|
||||
connection: { authenticatedUrl: value => value }, webServer: { port: 19387 } },
|
||||
shutdown: { shutdown: async () => writeFileSync(${JSON.stringify(join(root, 'stopped'))}, 'stopped') } });
|
||||
}));
|
||||
@@ -69,8 +69,10 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
|
||||
expect(boot.packageManager.env.ELECTRON_RUN_AS_NODE).toBe('1')
|
||||
expect(boot.packageManager.env.PATH).toBe(`${nodeBin}${delimiter}${process.env.PATH ?? ''}`)
|
||||
child.disconnect()
|
||||
expect(await exited).toBe(fail ? 1 : 0)
|
||||
const exitCode = await exited
|
||||
await drained
|
||||
expect(child.signalCode).toBeNull()
|
||||
expect(exitCode, stderr).toBe(fail ? 1 : 0)
|
||||
expect(stderr).not.toContain('ERR_IPC_CHANNEL_CLOSED')
|
||||
expect(stderr).not.toContain('Unhandled')
|
||||
if (fail) expect(stderr).toContain('fixture boot failure')
|
||||
|
||||
Loaded 100 of 1386 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user