diff --git a/.agents/notes/README.i18n.yaml b/.agents/notes/README.i18n.yaml index c07b56468c..f6289cf96f 100644 --- a/.agents/notes/README.i18n.yaml +++ b/.agents/notes/README.i18n.yaml @@ -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/README.md -README.md: 9fdb4039b525bbc4051ab3c8058b117e4de5f2f4 -README.zh.md: 2d7e07b963d3c4d77da9148b011e1478fa2ae5da +README.md: 6872de0a1390fa5a1707b9f464c9e0edeb2da4be +README.zh.md: ced81f77fa904ad0b5cb5219383ef651d87392b8 diff --git a/.agents/notes/README.md b/.agents/notes/README.md index 9fdb4039b5..6872de0a13 100644 --- a/.agents/notes/README.md +++ b/.agents/notes/README.md @@ -16,7 +16,7 @@ Every Agent Note has two axes, both encoded in its **path** — `{lifecycle}/{cl The date in the filename is when the topic was **first proposed** (per git history). Cross-references between Agent Notes use relative markdown links (`[topic](../../implemented/architecture/2026-…-….md)`) — never bare prose or numbers — so they are mechanically checkable and survive moves between folders. -The active lifecycle tree is the working inventory: browse its lifecycle/class folders or search the repository. Do not add a centralized `INDEX.md`; the [no-index Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md) owns the rationale. Low-future-value implemented records move to the separate frozen [`archived/`](archived/AGENTS.md) tree described below. +The active lifecycle tree is the working inventory: browse its lifecycle/class folders or search the repository. Do not add a centralized `INDEX.md`; the [no-index Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md) owns the rationale. Implemented records with historical decision value but little future guidance move to the separate frozen [`archived/`](archived/AGENTS.md) tree described below. ## Classification @@ -35,9 +35,9 @@ The `architecture` / `process` line: **architecture** is about the source we shi ## Archiving and deletion -Archive an implemented Agent Note when the shipped decision is complete and its rationale is unlikely to guide future work. Keep it active when its alternatives, ownership boundary, negative guarantee, durable or wire semantics, security rule, or reintroduction condition remains useful. Never archive a proposed note: reject an obsolete proposal. Keep a rejected note only while it prevents a plausible mistake; otherwise delete its English, Chinese, and sidecar files together. Use the calibrated [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) workflow rather than word count, age, or a target quota. +Delete an implemented Agent Note that only describes small UI adjustments or purely mechanical changes; delete its English, Chinese, and sidecar files together and repair or remove inbound links. A local bug fix, performance change, new capability, or substantive behavior decision does not qualify merely because its implementation is small. Archive other implemented notes only when the shipped decision is complete, its rationale is unlikely to guide future work, and the record still has historical decision value. Keep it active when its alternatives, ownership boundary, negative guarantee, durable or wire semantics, security rule, or reintroduction condition remains useful. Never archive a proposed note: reject an obsolete proposal. Keep a rejected note only while it prevents a plausible mistake; otherwise delete its English, Chinese, and sidecar files together. Use the calibrated [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) workflow rather than word count, age, or a target quota. -The archive is path-encoded as `archived/{class}/yyyy-mm-dd-topic-title.md`; `implemented` is deliberately absent because only implemented notes can enter it. An archival change moves the complete English/Chinese/sidecar triplet, retains `Status: implemented`, inserts the same `Archived: YYYY-MM-DD` line immediately below that status in both language files, re-records the sidecar, and repairs or deletes inbound links. These are the only permitted content changes during archival. +The archive is path-encoded as `archived/{class}/yyyy-mm-dd-topic-title.md`; `implemented` is deliberately absent because only implemented notes can enter it. An archival change moves the complete English/Chinese/sidecar triplet, retains `Status: implemented`, inserts the same `Archived: YYYY-MM-DD` line immediately below that status in both language files, re-records the sidecar, and repairs or deletes inbound links. These are the only permitted content changes during archival. Existing title punctuation, blank-line layout, and language-switcher wording do not block archival; preserve them with the body. Once sealed, every archived triplet is permanently frozen. Do not edit, translate, reformat, update, move, or delete it, and do not treat it as authority for current behavior. Documentation gates skip archived sources, including their outbound links; active prose may still link into an archived note when it intentionally cites history. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) enforces the closed class tree, complete triplets, archive metadata, sidecar hashes, and the append-only frozen-content manifest. The [archive-policy Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) owns the rationale. diff --git a/.agents/notes/README.zh.md b/.agents/notes/README.zh.md index 2d7e07b963..ced81f77fa 100644 --- a/.agents/notes/README.zh.md +++ b/.agents/notes/README.zh.md @@ -16,7 +16,7 @@ 文件名中的日期是该主题**首次提出**的时间(以 git 历史为准)。Agent Note 之间的交叉引用使用相对 Markdown 链接(`[topic](../../implemented/architecture/2026-…-….md)`),从不使用纯文字或编号,这样既可机械检查,也能在文件夹间移动时保持有效。 -活跃生命周期目录树就是工作清单:浏览其生命周期/类别文件夹,或搜索仓库即可。请勿添加集中式 `INDEX.md`;设计理由见[不设索引的 Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md)。未来指导价值较低的已实施记录会移至下文所述、单独冻结的 [`archived/`](archived/AGENTS.md) 目录树。 +活跃生命周期目录树就是工作清单:浏览其生命周期/类别文件夹,或搜索仓库即可。请勿添加集中式 `INDEX.md`;设计理由见[不设索引的 Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md)。具有历史决策价值但未来指导价值较低的已实施记录会移至下文所述、单独冻结的 [`archived/`](archived/AGENTS.md) 目录树。 @@ -37,9 +37,9 @@ ## 归档与删除 -当一份 implemented Agent Note 记录的交付决策已经完整落地,且其决策依据不太可能再指导未来工作时,将其归档。如果其中的备选方案、归属边界、否定性保证、持久化语义或协议语义、安全规则,或者重新引入条件仍有价值,则继续作为活跃记录保留。绝不归档 proposed Agent Note:过时的提案应转为 rejected。仅当 rejected Agent Note 仍能避免一种可能发生的错误时保留;否则一并删除其英文、中文和伴随记录文件。请使用经过校准的 [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) 工作流,不要根据字数、存续时间或目标配额来判断。 +仅描述小型 UI 调整或纯机械变更的 implemented Agent Note 直接删除;一并删除英文、中文和伴随记录文件,并修复或移除入站链接。局部 bug 修复、性能变更、新能力或实质行为决策,不会仅因实现规模小就符合删除条件。其他已实施记录只有在交付决策完整落地、其依据不太可能再指导未来工作,但仍具有历史决策价值时才归档。如果其中的备选方案、归属边界、否定性保证、持久化语义或协议语义、安全规则,或者重新引入条件仍有价值,则继续作为活跃记录保留。绝不归档 proposed Agent Note:过时的提案应转为 rejected。仅当 rejected Agent Note 仍能避免一种可能发生的错误时保留;否则一并删除其英文、中文和伴随记录文件。请使用经过校准的 [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) 工作流,不要根据字数、存续时间或目标配额来判断。 -归档路径编码为 `archived/{class}/yyyy-mm-dd-topic-title.md`;其中有意省略 `implemented`,因为只有 implemented Agent Note 可以进入归档。归档变更会移动完整的英文、中文和伴随记录三个文件,保留 `Status: implemented`,在两种语言的文件中紧接该状态行插入相同的 `Archived: YYYY-MM-DD` 行,重新记录伴随记录,并修复或删除入站链接。归档时只允许对内容做这些更改。 +归档路径编码为 `archived/{class}/yyyy-mm-dd-topic-title.md`;其中有意省略 `implemented`,因为只有 implemented Agent Note 可以进入归档。归档变更会移动完整的英文、中文和伴随记录三个文件,保留 `Status: implemented`,在两种语言的文件中紧接该状态行插入相同的 `Archived: YYYY-MM-DD` 行,重新记录伴随记录,并修复或删除入站链接。归档时只允许对内容做这些更改。 既有标题标点、空行布局和语言切换栏措辞不阻止归档;与正文一起保留。 封存后,每组归档文件都永久冻结。禁止编辑、翻译、重新格式化、更新、移动或删除,也不得将其视为当前行为的权威依据。文档门禁会跳过归档源文件,包括其中的出站链接;当活跃文档有意引用历史时,仍可链接到归档 Agent Note。[`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) 强制执行封闭的类别目录树、完整的三文件配对、归档元数据、伴随记录 hash,以及仅追加的冻结内容 manifest(元数据清单)。[归档政策 Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.zh.md) 记录了设计依据。 diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml index aad1a96be8..bb7be001de 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml @@ -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-09-client-conversation-node-assembly.md -2026-08-09-client-conversation-node-assembly.md: 43f9f0359822f7d7c5b52c09646fd3ffbf80f783 -2026-08-09-client-conversation-node-assembly.zh.md: d7475f5ff8218419743448d455d5d5ffe3cc4091 +2026-08-09-client-conversation-node-assembly.md: bb1d68cc045710d46a44ffb295c2307628338521 +2026-08-09-client-conversation-node-assembly.zh.md: 6291211d589b3389e7dc9a42d15fe6ac0d9e7ab9 diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md index 43f9f03598..bb1d68cc04 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md @@ -291,7 +291,7 @@ Page size, record packing, the number of history loads, and RAF coalescing affec Inbox demonstrates that every Event can be a start-only instantaneous-state Context; not every business requires a start/update pair. Reader links each next-step state to the prior same-kind Context instead of inventing a lifecycle ID for the entire Inbox. The state itself shares immutable pending splice nodes and one current claimed-batch Set, while unconsumed next-turn input remains outside Conversation because no Chat or Trajectory classification reads it. -Request Prompt demonstrates shared pure interpretation without shared target State: Chat and Trajectory call `inspectRequestPrompt()` from their own Definitions. The function canonicalizes the full header and classifies model-visible system/tool differences; each target then chooses its own output. Chat materializes a non-empty initial system field, a real system change, and each `series` snapshot that explicitly begins a message series or follows a surface replacement. An unchanged `resume` remains in Trajectory and reconstruction state but does not repeat the visible Chat row once its predecessor is loaded. Ordinary append-only later Turns do not write another unchanged header. The first header in a Step follows the provider envelope rather than the header Event position: step one uses the owning Turn start and later steps use their Step start, placing the system field before the request's user-role messages; a later header in the same Step stays at its own Event after the surface rewrite that began the new series. When the preceding header is outside a partial window, a non-`initial` header stays at its own Event and renders conservatively. Prepending an identical predecessor hides an unchanged resume without withdrawing its stable Node key; a real change remains visible. Every header is a full snapshot, so a first loaded `resume`, `change`, or `series` header can render its system field without fabricating a comparison to unloaded history ([resume presentation decision](../bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.md)). +Request Prompt demonstrates shared pure interpretation without shared target State: Chat and Trajectory call `inspectRequestPrompt()` from their own Definitions. The function canonicalizes the full header and classifies model-visible system/tool differences; each target then chooses its own output. Chat materializes a non-empty initial system field, a real system change, and each `series` snapshot that explicitly begins a message series or follows a surface replacement. Resume headers retain a Chat prompt card even when the system text is unchanged. Ordinary append-only later Turns do not write another unchanged header. The first header in a Step follows the provider envelope rather than the header Event position: step one uses the owning Turn start and later steps use their Step start, placing the system field before the request's user-role messages; a later header in the same Step stays at its own Event after the surface rewrite that began the new series. When the preceding header is outside a partial window, a non-`initial` header stays at its own Event and renders conservatively. Prepending an identical predecessor can hide an unchanged `change` header without withdrawing its stable Node key; resume and explicit series-start cards remain visible. Every header is a full snapshot, so a first loaded `resume`, `change`, or `series` header can render its system field without fabricating a comparison to unloaded history ([request-prompt Definition](../../../../packages/client/ui-chat/src/client/conversation-nodes/request-prompt.ts)). Retry, Assistant, and Turn Tail demonstrate independent claims on one Event. Each Definition updates only its own State and produces its own atomic Chat Node. diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md index d7475f5ff8..6291211d58 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md @@ -291,7 +291,7 @@ Chat `order` 的结构性变化仍可能重排当前可见 key;纯 data 更新 Inbox 展示了“每条 Event 都是一个 start-only 瞬间态 Context”,不是所有业务都需要 start/update 配对。每个 next-step state 通过 Reader 与前一个同 kind Context 形成连续 fold,而非给整个 Inbox 人工制造生命周期 ID。state 自身共享不可变 pending splice 节点和一个当前 claimed-batch Set;未消费的 next-turn input 不进入 Conversation,因为 Chat 与 Trajectory 都不读取它来分类。 -Request Prompt 展示了如何在不共享 target State 的前提下共用纯解释逻辑:Chat 与 Trajectory 各自在自己的 Definition 中调用 `inspectRequestPrompt()`。该函数规范化完整 header,并判定面向模型的 system/tool 差异;随后每个 target 自行选择产物。Chat 会物化非空的初始 system 字段、真实 system 变化,以及每个显式开启消息序列或紧随表层替换的 `series` 快照。未变化的 `resume` 会留在 Trajectory 和重建状态中,但前序 header 已加载时不会重复可见的 Chat 行。普通的仅追加后续 Turn 不会再次写入未变 header。一个 Step 中的首条 header 遵循提供方信封,而不是 header Event 位置:step one 使用所属 Turn start,后续 step 使用各自的 Step start,把 system 字段放到该请求的 user-role 消息之前;同一 Step 的后续 header 保留在开启新序列的表层改写之后。部分窗口未包含前序 header 时,非 `initial` header 会保留在自身 Event 并保守渲染。prepend 补入相同的前序 header 后,内容未变的 resume 会隐藏但不撤回其稳定 Node key;真实变化仍然可见。每条 header 都是完整快照,因此已加载窗口中的首条 `resume`、`change` 或 `series` header 无需凭空构造与未加载历史的比较,也能渲染其 system 字段([resume 展示决策](../bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.zh.md))。 +Request Prompt 展示了如何在不共享 target State 的前提下共用纯解释逻辑:Chat 与 Trajectory 各自在自己的 Definition 中调用 `inspectRequestPrompt()`。该函数规范化完整 header,并判定面向模型的 system/tool 差异;随后每个 target 自行选择产物。Chat 会物化非空的初始 system 字段、真实 system 变化,以及每个显式开启消息序列或紧随表层替换的 `series` 快照。即使 system 文本未变化,resume header 仍保留 Chat prompt 卡片。普通的仅追加后续 Turn 不会再次写入未变 header。一个 Step 中的首条 header 遵循提供方信封,而不是 header Event 位置:step one 使用所属 Turn start,后续 step 使用各自的 Step start,把 system 字段放到该请求的 user-role 消息之前;同一 Step 的后续 header 保留在开启新序列的表层改写之后。部分窗口未包含前序 header 时,非 `initial` header 会保留在自身 Event 并保守渲染。prepend 补入相同的前序 header 后,内容未变的 `change` header 可以隐藏,但不撤回其稳定 Node key;resume 和显式序列起点的卡片仍然可见。每条 header 都是完整快照,因此已加载窗口中的首条 `resume`、`change` 或 `series` header 无需凭空构造与未加载历史的比较,也能渲染其 system 字段([request-prompt Definition](../../../../packages/client/ui-chat/src/client/conversation-nodes/request-prompt.ts))。 Retry、Assistant 和 Turn Tail 展示了同一 Event 被多个 Definition 独立认领。每个 Definition 只更新自己的 State,最终分别生成原子 Chat Node。 diff --git a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml index 0ac7d7109d..b3d68505df 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.i18n.yaml @@ -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-18-experimental-agent-teams-packages.md -2026-08-18-experimental-agent-teams-packages.md: 4a78a60c2ad7463c06b670e6ec664579ffd767b9 -2026-08-18-experimental-agent-teams-packages.zh.md: 8056cd8194695e8ce41acb359e51c040d4ed3aec +2026-08-18-experimental-agent-teams-packages.md: a1d29b5e96b185a5539d664e19f2f5cc342a6aba +2026-08-18-experimental-agent-teams-packages.zh.md: f4c4eb481ae6ad6319ddac50eda82fa9b9feebf7 diff --git a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md index 4a78a60c2a..a1d29b5e96 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md +++ b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md @@ -12,17 +12,17 @@ Moving the packages into product-role groups would remove their experimental nam ## Decision -`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, `packages/experimental/client-ui-agent-team`, and `packages/experimental/agent-team-web-profile` are public workspace packages. They retain their existing `@deepseek-ai/dsh-experimental-*` names and join the dsh release family. The [publication denylist decision](../process/2026-09-12-experimental-publication-denylist.md) owns the public default and private exceptions; the [experimental package rules](../../../../packages/experimental/AGENTS.md) own dependency isolation and later promotion. +`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, `packages/experimental/agent-team-profile`, and `packages/experimental/client-ui-agent-team` are public workspace packages. They retain their existing `@deepseek-ai/dsh-experimental-*` names and join the dsh release family. The [publication denylist decision](../process/2026-09-12-experimental-publication-denylist.md) owns the public default and private exceptions; the [experimental package rules](../../../../packages/experimental/AGENTS.md) own dependency isolation and later promotion. -The dsh pack and publish set and the local baseline publisher include these five Agent Teams directories and the [Cua Driver providers](2026-09-12-computer-use-provider-registration.md). Workspace constraints require them to omit `private`, set `publishConfig.access` to `public`, and keep the experimental npm prefix. Release packages and apps outside the experimental group, plus the Python runtime, cannot name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`; experimental packages may depend on release packages and each other. +The dsh pack and publish set and the local baseline publisher include these four Agent Teams directories and the [Cua Driver providers](2026-09-12-computer-use-provider-registration.md). Workspace constraints require them to omit `private`, set `publishConfig.access` to `public`, and keep the experimental npm prefix. Release packages and apps outside the experimental group, plus the Python runtime, cannot name experimental packages in `dependencies`, `optionalDependencies`, or `peerDependencies`; experimental packages may depend on release packages and each other. The generic caller-reserved continuable child identity and selective direct-child drain remain in the stable Subagent service. They own Subagent identity and Activation lifecycle without importing or naming Agent Teams; the experimental Team service consumes them in the permitted direction. -The published Host-side Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows and disables the global continuable-child controls whose model-visible names overlap the Team tools. The separate published Web profile applies after `dsh-web-app` and the Host profile; it inserts the Team UI, which mounts the Remote contribution generated by the Team package. Both layers remain opt-in and leave the shipped base, CLI, Web, and Python runtime dependency graphs unchanged. +The [single-bundle decision](2026-09-18-agent-teams-single-bundle.md) supersedes the separate Host and Web bundle composition. This note retains publication, dependency isolation, and promotion rationale. The current Team profile remains opt-in and disables the global continuable-child controls whose model-visible names overlap the Team tools; the [optional-bundle decision](../process/2026-09-15-shipped-optional-bundles.md) owns its inclusion in the installation. Profile startup resolves selected bundles before computing the [immutable profile resolution generation](2026-09-09-profile-resolution-generations.md). The generation retains installation-first precedence, traverses each explicit bundle root completely in profile order, and keeps pnpm-managed profile packages authoritative. Runtime mode enforces the result in memory; retained link and dual modes materialize the same result as shared and profile-owned projections. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution. -Experimental status changes compatibility and support expectations, not publication for these five packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations. +Experimental status changes compatibility and support expectations, not publication for these four packages. They retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion still requires review of the public contracts, limitations, test evidence, runtime dependents, and a named owner accepting stable-package obligations. ## Alternatives considered @@ -36,6 +36,6 @@ Experimental status changes compatibility and support expectations, not publicat ## Consequences -Agent Teams publishes as five installable tarballs in the dsh release family without changing package names or enabling Team in a shipped profile. Public availability does not make the packages stable or supported by default, and stable release packages cannot take runtime dependencies on them. +Agent Teams publishes as four installable tarballs in the dsh release family without changing package names or enabling Team in a shipped profile. Public availability does not make the packages stable or supported by default, and stable release packages cannot take runtime dependencies on them. The release family carries the experimental npm names. Promotion still creates path and npm-name churn as specified by the experimental package rules. diff --git a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md index 8056cd8194..f4c4eb481a 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.zh.md @@ -12,17 +12,17 @@ Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Sess ## 决策 -`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile`、`packages/experimental/client-ui-agent-team` 与 `packages/experimental/agent-team-web-profile` 是公开 workspace 包。它们保留现有 `@deepseek-ai/dsh-experimental-*` 名称并加入 dsh 发布系列。[发布拒绝列表决策](../process/2026-09-12-experimental-publication-denylist.zh.md)负责默认公开与私有例外;[实验性包规则](../../../../packages/experimental/AGENTS.md)负责依赖隔离与后续 promotion。 +`packages/experimental/agent-team`、`packages/experimental/tool-agent-team`、`packages/experimental/agent-team-profile` 与 `packages/experimental/client-ui-agent-team` 是公开 workspace 包。它们保留现有 `@deepseek-ai/dsh-experimental-*` 名称并加入 dsh 发布系列。[发布拒绝列表决策](../process/2026-09-12-experimental-publication-denylist.zh.md)负责默认公开与私有例外;[实验性包规则](../../../../packages/experimental/AGENTS.md)负责依赖隔离与后续 promotion。 -dsh 打包与发布集合以及本地基线发布器包含这五个 Agent Teams 目录和 [Cua Driver 提供方](2026-09-12-computer-use-provider-registration.zh.md)。workspace 约束要求它们省略 `private`、将 `publishConfig.access` 设为 `public`,并保留实验性 npm 前缀。实验组之外的发布包、应用和 Python 运行时不能在 `dependencies`、`optionalDependencies` 或 `peerDependencies` 中引用实验包;实验包可以依赖发布包和彼此。 +dsh 打包与发布集合以及本地基线发布器包含这四个 Agent Teams 目录和 [Cua Driver 提供方](2026-09-12-computer-use-provider-registration.zh.md)。workspace 约束要求它们省略 `private`、将 `publishConfig.access` 设为 `public`,并保留实验性 npm 前缀。实验组之外的发布包、应用和 Python 运行时不能在 `dependencies`、`optionalDependencies` 或 `peerDependencies` 中引用实验包;实验包可以依赖发布包和彼此。 通用的调用方预留 continuable child 身份和精确 direct-child drain 仍属于稳定 Subagent 服务。它们负责 Subagent 身份与 Activation 生命周期,不 import 或命名 Agent Teams;实验性 Team 服务沿允许的方向消费这些能力。 -公开发布的 Host 侧 Agent Teams profile bundle 依赖 Team 包,并在 `dsh-base` 之后应用。它会插入 Team 配置行,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control。独立公开发布的 Web profile 在 `dsh-web-app` 与 Host profile 之后应用;它会插入 Team UI,后者挂载 Team package 生成的 Remote contribution。两个层都保持显式启用,不改变随附 base、CLI、Web 与 Python runtime 的依赖图。 +[单组合包决策](2026-09-18-agent-teams-single-bundle.zh.md)取代了独立 Host 与 Web 组合包的组合方式。本记录保留发布、依赖隔离与 promotion 的理由。当前 Team profile 仍须显式启用,并禁用模型可见名称与 Team 工具重叠的全局 continuable-child control;[可选组合包决策](../process/2026-09-15-shipped-optional-bundles.zh.md)负责其随安装交付的规则。 profile 启动会先解析所选 bundle,再计算[不可变 profile resolution generation](2026-09-09-profile-resolution-generations.zh.md)。generation 保留安装优先顺序,按 profile 顺序完整遍历每个显式 bundle 根,并让 pnpm 管理的 profile 包保持优先。runtime 模式在内存中强制该结果;保留的 link 与 dual 模式把同一结果物化为共享和 profile 自有投影。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。 -对这五个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。 +对这四个包而言,实验性状态改变兼容性与支持预期,而不阻止发布。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前仍须评审公开约定、限制、测试证据、运行时依赖方,并由一名具名 owner 接受稳定包义务。 ## 曾考虑的替代方案 @@ -36,6 +36,6 @@ profile 启动会先解析所选 bundle,再计算[不可变 profile resolution ## 后果 -Agent Teams 会作为 dsh 发布系列中的五个可安装 tarball 发布,同时保持包名不变,也不会在随附 profile 中启用 Team。公开可用不代表这些包稳定或默认受支持,稳定发布包也不能对其建立运行时依赖。 +Agent Teams 会作为 dsh 发布系列中的四个可安装 tarball 发布,同时保持包名不变,也不会在随附 profile 中启用 Team。公开可用不代表这些包稳定或默认受支持,稳定发布包也不能对其建立运行时依赖。 发布系列保留实验性 npm 名称。promotion 仍会按照实验性包规则产生路径和 npm 名改动。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml index fff8e98bc5..cdf124f72a 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml @@ -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-23-client-derived-tool-presentation.md -2026-08-23-client-derived-tool-presentation.md: 703960cb24f90eb678fda381f929efb3f94bbd28 -2026-08-23-client-derived-tool-presentation.zh.md: 7f161379553f3aca33c72c2edf93e114d27eb7f9 +2026-08-23-client-derived-tool-presentation.md: 8befe840b08006a7d2ee0fa1b7875c2c989d4779 +2026-08-23-client-derived-tool-presentation.zh.md: 249366f10e6eb06941b344977169609358786413 diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md index 703960cb24..8befe840b0 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md @@ -64,7 +64,7 @@ The Host `ToolDefinition.presentCall`, `ToolDefinition.presentResult`, `ToolCall **Client card model** means the pure props data under `ui-tool/src/client/tool/models/` consumed directly by `TerminalBlock`, `DiffBlock`, `ReadBlock`, `SearchBlock`, `WebBlock`, or `ToolRow`. -**Specialized card** means the structured terminal, diff, read, search, or web body. Titles, summaries, status dots, and ordinary IN/OUT text remain part of the generic tool row. +**Specialized card** means the structured terminal, diff, read, search, or web body. Titles, summaries, lifecycle styling, business glyphs, and ordinary IN/OUT text remain part of the generic tool row. **Equivalent** means that the same supported input produces the user-visible result and interaction pinned by the existing component, assembly, and browser evidence. It does not require the same intermediate TypeScript types or internal calls. @@ -301,7 +301,7 @@ The Client terminal model derives existing `TerminalBlock` props from the tool n | Input | Preserved result | |---|---| | running standard `bash`/`pwsh` foreground call | terminal prompt, description, cwd, and running state | -| successful standard foreground call | terminal output, exit code/signal, and success or failure status dot | +| successful standard foreground call | terminal output, exit code/signal, and settled state presentation | | `run_in_background:true` | Generic row and raw result | | tool execution error | Generic IN/OUT and error summary | | running persistent `bash`/`pwsh` | terminal prompt | diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md index 7f16137955..249366f10e 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md @@ -64,7 +64,7 @@ Host 的 `ToolDefinition.presentCall`、`ToolDefinition.presentResult`、`ToolCa **Client card model**指 `ui-tool/src/client/tool/models/` 下直接供 `TerminalBlock`、`DiffBlock`、`ReadBlock`、`SearchBlock`、`WebBlock` 或 `ToolRow` 使用的纯 props 数据。 -**专用卡片**指 terminal、diff、read、search 与 web 的结构化正文;标题、摘要、状态点和普通 IN/OUT 文本仍属于通用工具行。 +**专用卡片**指 terminal、diff、read、search 与 web 的结构化正文;标题、摘要、生命周期样式、业务图形和普通 IN/OUT 文本仍属于通用工具行。 **对等**指同一受支持输入产生由现有组件、组装与浏览器证据固定的用户可见结果和交互,不要求相同的中间 TypeScript 类型或内部函数调用。 @@ -301,7 +301,7 @@ Client terminal model 从工具名称、调用参数、结果 content、error | 输入 | 保持的结果 | |---|---| | 标准 `bash`/`pwsh` 前台 running | terminal prompt、description、cwd、running 状态 | -| 标准前台 success | terminal output、exit code/signal、成功或失败状态点 | +| 标准前台 success | terminal output、exit code/signal 与结算状态呈现 | | `run_in_background:true` | Generic 行与原始结果 | | 工具执行 error | Generic IN/OUT 与错误摘要 | | persistent `bash`/`pwsh` running | terminal prompt | diff --git a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.i18n.yaml index 89a705592d..55f30d278a 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.i18n.yaml @@ -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-05-shared-client-control-primitives.md -2026-09-05-shared-client-control-primitives.md: 14128497cf4152659a62136cbdeb74b92ee3fbf0 -2026-09-05-shared-client-control-primitives.zh.md: 66268b48f81887181c3981bf89ead30eb74250af +2026-09-05-shared-client-control-primitives.md: db8cb9a8bc515918867b27b216fe8edb50c392db +2026-09-05-shared-client-control-primitives.zh.md: 6d4b04a21bf224eaa32b5fbdfb2bf1c3a2cd804d diff --git a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md index 14128497cf..db8cb9a8bc 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md +++ b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.md @@ -18,15 +18,15 @@ Neither half of the fix was available to an author. No checklist told them to lo `Switch` is the two-state toggle, at the 36×20 track and 16px thumb `ui-settings-plugins` established. `label` is required and has no default, so a render site cannot omit the accessible name; `title` carries a lock reason where a deployment disables the control. -`StateDotState` gains `idle`, a static grey dot in the same halo-and-core construction as `done`, `warning`, and `error`. The plugin inventory's `pending` and `unloading` phases mean no activity is in progress, and the four existing states had no member for that; without `idle` those two phases lose their marker entirely. The addition is safe for the eight packages already consuming `StateDot` because each of them produces a `StateDotState` from its own closed status union and none switches over `StateDotState` itself. +`StateDotState` includes `idle` for a tracked subject with no activity in progress. The plugin inventory's `pending` and `unloading` phases need that semantic member; without it those two phases lose their marker entirely. The five-state union remains safe for consumers because each package produces a `StateDotState` from its own closed status union and none switches over `StateDotState` itself. The current solid-dot palette and rotating `ongoing` loader are owned by the later [StateDot visual language decision](../feature/2026-09-17-state-dot-visual-language.md). -**The component catalog in the `ui-primitives` README is what makes the rule usable.** It lists every exported component with its purpose and the case it is wrong for, and it names the three pairs that are easy to confuse: `Tag` against `Pill`, `DisclosureRow` against card-shaped disclosure, and the package-internal `FoldToggle` against the exported surface. `Pill` is the selectable capsule button — it takes `active` and `onClick` and drives view switchers and filters; `Tag` is the read-only badge and takes neither. `Pill`'s own header comment previously advertised itself for badges, which contradicted the catalog, and no longer does. +**The component catalog in the `ui-primitives` README is what makes the rule usable.** It lists every exported component with its purpose and the case it is wrong for, and it names the three pairs that are easy to confuse: `Tag` against `Pill`, `DisclosureRow` against card-shaped disclosure, and the package-internal `FoldToggle` against the exported surface. `Pill` is the selectable capsule button — it takes `active` and `onClick` and drives view switchers and filters; `Tag` is the read-only badge and takes neither. The component comments use the same distinction. The rule is stated in [packages/client/AGENTS.md](../../../../packages/client/AGENTS.md) as the first step of the new-component checklist, and [docs/web-styling.md](../../../../docs/web-styling.md) points at the catalog so an author arriving from the styling side reaches it too. ## Finding the duplicates -A name-based search undercounts. `.badge`, `.tag`, `.chip`, and `.configTag` miss a capsule named for its role rather than its appearance — `PluginCard`'s unsaved marker is `.pending`, and its rule was byte-identical to the badge two files away. What finds them is the geometry: a CSS Modules rule carrying both `border-radius: 999px` and `padding: 1px 8px`. After this change that signature matches exactly two rules: `Tag` itself and the broken badge below. The unsaved marker keeps only a `flex: none` placement class, which the signature no longer matches. +A name-based search undercounts. `.badge`, `.tag`, `.chip`, and `.configTag` miss a capsule named for its role rather than its appearance — `PluginCard`'s unsaved marker is `.pending`. Geometry is the reliable probe: search CSS Modules rules carrying both `border-radius: 999px` and `padding: 1px 8px`. That signature belongs to `Tag` and the deliberately local broken badge below; the unsaved marker keeps only its `flex: none` placement class. ## What stays local @@ -34,7 +34,6 @@ A name-based search undercounts. `.badge`, `.tag`, `.chip`, and `.configTag` mis A mechanical search groups these controls with the promoted three. They stay in their own packages, because the grouping is superficial: - **`ui-trajectory`'s toolbar toggle** carries `role="switch"` but is an 88px labelled control with an inline track, and it currently renders `hidden`. It is not the same widget as `Switch`. -- **`ui-schedule`'s status dot** is a static blue dot for the next run that turns amber when overdue. `StateDot` has no static blue — its only blue is `ongoing`, an animated pixel matrix — and one animation per row in a schedule list would misstate the meaning as well as the appearance. - **`ui-plan`'s mode chip** and **`ui-conversation`'s `ReferenceChip`** are interactive: the first is a warning-toned button with hover, focus, disabled, and a close affordance; the second is a Lexical atom node with its own truncation. Neither is a read-only badge. - **`ui-trajectory`'s cell tag** and **`ui-user-questions`'s recommendation badge** use their own geometry — a 6px radius at table density, and a 6px radius at 600 weight on the sidebar accent. Forcing either into the capsule baseline would change a deliberate design, not an accidental one. - **`TerminalBlock`'s exit-status pill** stays a static `Pill`. It sits on a 24px command line at the pill's own geometry, and `Tag`'s 11px capsule would not fit that row. The read-only/selectable split is the usual guide, but size decides this one, and the catalog says so. @@ -52,7 +51,7 @@ A mechanical search groups these controls with the promoted three. They stay in **A two-axis `variant × tone` API for `Tag`.** Rejected. Three variants against six tones describes eighteen combinations of which eight ship, and it lets a caller request combinations with no defined appearance. The flat eight-member union maps each value to exactly one shipped appearance. -**Rendering no dot for `pending` and `unloading` instead of adding `idle`.** Rejected. Those rows show a grey dot today, and dropping it would remove information from the inventory in a change whose purpose is to consolidate presentation. +**Rendering no dot for `pending` and `unloading` instead of adding `idle`.** Rejected. Those phases need a compact inactive or transitional marker; dropping it would remove information from the inventory in a change whose purpose is to consolidate presentation. **Putting the reuse rule in the root `AGENTS.md`.** Rejected. The rule governs `packages/client` alone, and the root file sits exactly at its 1950-word ceiling in `scripts/doc-budgets.manifest.json`, so stating it there would have to displace an unrelated repository-wide rule. @@ -60,15 +59,15 @@ A mechanical search groups these controls with the promoted three. They stay in `Tag`, `Switch`, and the extended `StateDot` carry component specs in `packages/client/ui-primitives/tests`, inside the per-file 100% coverage gate. `StateDot`'s palette is pinned by reading its stylesheet: CSS Modules resolve to class-name maps in the component suites, so a state whose color rule is missing renders on the inherited color and no render assertion notices. -The migrated render sites keep their existing package specs unchanged. The web e2e goldens are ARIA snapshots, and the full replayed web suite passes without re-recording, because the migration preserves every role, accessible name, and state — `Switch` keeps `role="switch"` with `aria-checked`, and the inventory's phase dot keeps its `role="img"` name on a wrapper, since `StateDot` is `aria-hidden`. +The migrated render sites keep package specs for their role, accessible name, state mapping, and interaction. Web ARIA snapshots cover the assembled presentations — `Switch` keeps `role="switch"` with `aria-checked`, and the inventory's phase dot keeps its `role="img"` name on a wrapper because `StateDot` is `aria-hidden`. -That is also the limit of the automated evidence. No gate in this repository compares pixels, so the capsule geometry, the dot halo, and the font-weight change are verified by review against the light and dark screenshots in the pull request. +The component and stylesheet specs pin the capsule geometry, StateDot artwork, animation, tokens, and font weight; Web snapshots pin accessible structure. Manual visual inspection remains responsible for the rendered result across themes. ## Consequences - A new client control now has one place to check and one place to add, and the catalog makes the check a single file read rather than a `grep` over forty exports. - `TagTone` is eight members wide because eight appearances shipped. Adding a ninth requires a render site that needs it, not a symmetry argument. -- The plugin inventory's tags change from a 5px rectangle to a capsule, and the disabled tag's fill moves from `--dsw-alias-bg-layer-1` to the `neutral` tone's `--dsw-alias-bg-module-platform`, which is a visible grey where it used to be near-transparent. Its phase dots gain a halo and animate through `loading` and `unloading`. The unconfigured-secret badge in the plugin settings fields moves from 400 to the baseline 500 weight. These are deliberate visual changes, recorded here so a later reader does not treat them as regressions. -- The migration removes a literal `#b45309`. The plugin inventory's `conditional` tag read `var(--dsw-alias-state-warning-primary, #b45309)`, and no such alias exists — the real token is `--dsw-alias-state-warn-primary` — so both themes had been painting the hardcoded fallback that [docs/web-styling.md](../../../../docs/web-styling.md) forbids. +- The plugin inventory uses capsule tags; the disabled tag uses the `neutral` tone's visible `--dsw-alias-bg-module-platform` fill. Its phase markers use the shared `StateDot`, including the rotating loader for active transitions. The unconfigured-secret badge in the plugin settings fields uses the baseline 500 weight. +- The plugin inventory palette contains no literal `#b45309`. The conditional tag uses the real `--dsw-alias-state-warn-primary` token rather than the nonexistent `--dsw-alias-state-warning-primary` alias and its hardcoded fallback. - The rule cannot be checked mechanically. A future author can still copy a control, and only review will catch it. That is the accepted cost of not gating: the alternative rejects legitimate work, and the packages that stay local above are proof that legitimate work exists. -- `ui-primitives` grows two components that one package each consumes today. The switch in particular is a single-consumer primitive, promoted because it is a general control and because the plugin-management work already in flight will adopt it rather than adding two further copies. +- `Tag` and `Switch` are shared across settings and preset packages; future consumers extend these primitives only when an existing prop cannot express a deliberate difference. diff --git a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.zh.md b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.zh.md index 66268b48f8..6d4b04a21b 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-05-shared-client-control-primitives.zh.md @@ -18,15 +18,15 @@ Status: implemented `Switch` 是双态开关,采用 `ui-settings-plugins` 确立的 36×20 轨道与 16px 滑块。`label` 必填且没有默认值,渲染点无法省略无障碍名称;`title` 在部署禁用该控件时承载锁定原因。 -`StateDotState` 增加 `idle`,一个静态灰点,与 `done`、`warning`、`error` 采用同样的光晕加实心核结构。插件清单的 `pending` 与 `unloading` 相位表示没有进行中的活动,原有四态没有对应成员;没有 `idle`,这两个相位会彻底失去标记。这次增补对已经消费 `StateDot` 的八个包是安全的:它们各自从自己的闭合状态联合**产生** `StateDotState`,没有任何一个对 `StateDotState` 本身做 switch。 +`StateDotState` 包含 `idle`,表示一个被跟踪的对象当前没有进行中的活动。插件清单的 `pending` 与 `unloading` 相位需要这个语义成员;没有 `idle`,这两个相位会彻底失去标记。五态联合对消费方保持安全,因为每个包都从自己的闭合状态联合**产生** `StateDotState`,没有任何一个对 `StateDotState` 本身做 switch。当前的纯色点配色与旋转 `ongoing` loading 由后续的 [StateDot 视觉语言决策](../feature/2026-09-17-state-dot-visual-language.zh.md)负责。 -**`ui-primitives` README 里的组件目录,是这条规则得以可用的前提。** 它列出每个导出组件的用途,以及它不适用的场合,并点名三组容易混淆的配对:`Tag` 与 `Pill`、`DisclosureRow` 与卡片式折叠、包内的 `FoldToggle` 与对外导出面。`Pill` 是可选中的胶囊按钮——它接受 `active` 与 `onClick`,用于视图切换与筛选器;`Tag` 是只读徽章,两者都不接受。`Pill` 自己的头注释此前把自身宣传为可用作徽章,与目录互相矛盾,现已改掉。 +**`ui-primitives` README 里的组件目录,是这条规则得以可用的前提。** 它列出每个导出组件的用途,以及它不适用的场合,并点名三组容易混淆的配对:`Tag` 与 `Pill`、`DisclosureRow` 与卡片式折叠、包内的 `FoldToggle` 与对外导出面。`Pill` 是可选中的胶囊按钮——它接受 `active` 与 `onClick`,用于视图切换与筛选器;`Tag` 是只读徽章,两者都不接受。组件注释使用同一套区分。 规则写在 [packages/client/AGENTS.md](../../../../packages/client/AGENTS.md) 新组件清单的第一步,[docs/web-styling.md](../../../../docs/web-styling.zh.md) 指向该目录,让从样式一侧进来的作者也能到达。 ## 如何找出重复 -按名字搜会漏。`.badge`、`.tag`、`.chip`、`.configTag` 找不到以角色而非外观命名的胶囊——`PluginCard` 的未保存标记叫 `.pending`,而它的规则与两个文件之外的徽章逐字节相同。能找出它们的是几何特征:同时带有 `border-radius: 999px` 与 `padding: 1px 8px` 的 CSS Modules 规则。本次改动后,这个特征恰好匹配两条规则:`Tag` 本身与下面那个损坏徽章。未保存标记只剩一个 `flex: none` 布局类,已不再匹配该特征。 +按名字搜会漏。`.badge`、`.tag`、`.chip`、`.configTag` 找不到以角色而非外观命名的胶囊——`PluginCard` 的未保存标记叫 `.pending`。几何特征才是可靠探针:搜索同时带有 `border-radius: 999px` 与 `padding: 1px 8px` 的 CSS Modules 规则。这个特征属于 `Tag` 与下面那个有意保留在原包的损坏徽章;未保存标记只保留自己的 `flex: none` 布局类。 ## 保留在原包的控件 @@ -34,7 +34,6 @@ Status: implemented 机械搜索会把下面这些控件归到被提升的那三类里。它们留在各自的包中,因为那种归并只是表面相似: - **`ui-trajectory` 的工具栏开关**带 `role="switch"`,但它是 88px 宽的带标签控件、轨道内联,而且当前渲染为 `hidden`。它和 `Switch` 不是同一个部件。 -- **`ui-schedule` 的状态点**是表示下次运行的静态蓝点,逾期转为琥珀色。`StateDot` 没有静态蓝——它唯一的蓝是 `ongoing`,一个动画像素方阵——日程列表里每行一个动画,既说错了含义也说错了观感。 - **`ui-plan` 的模式 chip** 与 **`ui-conversation` 的 `ReferenceChip`** 都是可交互的:前者是警告色调的按钮,带 hover、focus、disabled 与关闭操作;后者是带自有截断逻辑的 Lexical 原子节点。两者都不是只读徽章。 - **`ui-trajectory` 的单元格 tag** 与 **`ui-user-questions` 的推荐徽章**使用各自的几何——一个是表格密度下的 6px 圆角,一个是侧栏强调色上 600 字重的 6px 圆角。把它们塞进胶囊基准,改掉的是有意的设计,不是意外的分歧。 - **`TerminalBlock` 的退出状态胶囊**保持为静态 `Pill`。它落在 24px 的命令行上、用的是 pill 自己的几何,`Tag` 的 11px 胶囊放不进那一行。只读与可选中的区分是通常的判据,但这一处由尺寸决定,目录里也是这么写的。 @@ -52,7 +51,7 @@ Status: implemented **给 `Tag` 设计 `variant × tone` 双轴 API。** 否决。三个 variant 乘六个 tone 描述十八种组合,其中八种会发布,而且它允许调用方请求没有定义外观的组合。扁平的八成员联合把每个值映射到恰好一种已发布外观。 -**让 `pending` 与 `unloading` 不渲染点,而不是新增 `idle`。** 否决。这两行今天显示灰点,在一个目的是统一呈现的改动里把它删掉,等于从清单中拿走信息。 +**让 `pending` 与 `unloading` 不渲染点,而不是新增 `idle`。** 否决。这些阶段需要紧凑的未活动或过渡标记;在一个目的是统一呈现的改动里把它删掉,等于从清单中拿走信息。 **把复用规则写进根 `AGENTS.md`。** 否决。这条规则只管 `packages/client`,而根文件正好处在 `scripts/doc-budgets.manifest.json` 给它的 1950 词上限上,写在那里就必须挤掉一条不相干的仓库级规则。 @@ -60,15 +59,15 @@ Status: implemented `Tag`、`Switch` 与扩展后的 `StateDot` 在 `packages/client/ui-primitives/tests` 中各有组件测试,处于每文件 100% 覆盖率门禁之内。`StateDot` 的配色通过读取其样式表来钉住:CSS Modules 在组件测试里解析为类名映射,因此某个状态缺了配色规则时会落到继承色上,而任何渲染断言都不会察觉。 -迁移后的渲染点保留各自包内原有的测试,未作改动。web e2e 的 golden 是 ARIA 快照,完整的 replay web 套件无需重录即可通过,因为这次迁移保住了每一个 role、无障碍名称与状态——`Switch` 保留带 `aria-checked` 的 `role="switch"`,而由于 `StateDot` 是 `aria-hidden`,插件清单的相位点把 `role="img"` 名称保留在外层包裹元素上。 +迁移后的渲染点保留包级测试,覆盖 role、无障碍名称、状态映射与交互。Web ARIA 快照覆盖装配后的呈现——`Switch` 保留带 `aria-checked` 的 `role="switch"`,而由于 `StateDot` 是 `aria-hidden`,插件清单的相位点把 `role="img"` 名称保留在外层包裹元素上。 -这同时也是自动化证据的边界。本仓库没有任何门禁比较像素,因此胶囊几何、状态点光晕与字重变化,只能由评审对照 PR 中的明暗两套截图来确认。 +组件与样式表测试固定胶囊几何、StateDot 图稿、动画、token 与字重;Web 快照固定无障碍结构。明暗主题下的最终渲染仍由人工视觉检查负责。 ## Consequences - 新的客户端控件现在有一处可查、一处可加,而目录把这次查询变成读一个文件,而不是在四十多个导出上 `grep`。 - `TagTone` 有八个成员,因为发布了八种外观。加第九个需要一个真的需要它的渲染点,而不是一个对称性论证。 -- 插件清单的标签从 5px 矩形变为胶囊,其中"已停用"标签的底色从 `--dsw-alias-bg-layer-1` 变为 `neutral` 色调的 `--dsw-alias-bg-module-platform`,由近乎透明变成可见的灰底。相位点获得光晕,并在 `loading` 与 `unloading` 两个相位动画。插件设置字段中"未配置"徽章的字重从 400 改为基准的 500。这些都是有意的视觉变更,记录在此,以免后来的读者把它们当成回归。 -- 本次迁移移除了一个 `#b45309` 字面量。插件清单的 `conditional` 标签写的是 `var(--dsw-alias-state-warning-primary, #b45309)`,而这个别名并不存在——真正的 token 是 `--dsw-alias-state-warn-primary`——所以明暗两套主题一直在绘制那个被 [docs/web-styling.md](../../../../docs/web-styling.zh.md) 禁止的硬编码兜底色。 +- 插件清单使用胶囊标签;"已停用"标签使用 `neutral` 色调下可见的 `--dsw-alias-bg-module-platform` 填充。相位标记使用共享 `StateDot`,活动转换采用旋转 loading。插件设置字段中的"未配置"徽章使用基准 500 字重。 +- 插件清单调色板不含 `#b45309` 字面量。`conditional` 标签使用真实的 `--dsw-alias-state-warn-primary` token,而不是不存在的 `--dsw-alias-state-warning-primary` 别名及其硬编码兜底色。 - 这条规则无法被机械检查。将来的作者仍然可以复制一个控件,只有评审能拦住。这是不设门禁所接受的代价:另一条路会拒绝正当的工作,而上面那份保留在原包的清单就是正当工作确实存在的证据。 -- `ui-primitives` 多出两个各自只有一个消费方的组件。开关尤其如此,提升它是因为它是一个通用控件,也因为正在进行中的插件管理工作会采用它,而不是再加两份拷贝。 +- `Tag` 与 `Switch` 由多个设置和预设包共享;只有现有 prop 无法表达明确差异时,后续消费方才扩展这些原语。 diff --git a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml index 2ccfc07ac8..577fbe18d0 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.i18n.yaml @@ -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-05-sidebar-tab-types-and-navigation.md -2026-09-05-sidebar-tab-types-and-navigation.md: 834a26e55f87e5db54d57f06fec592e4fdb9f61c -2026-09-05-sidebar-tab-types-and-navigation.zh.md: 9884db9eef49162e8883c390dd71bdbf9d45489f +2026-09-05-sidebar-tab-types-and-navigation.md: 599ce36ed4bb1dfc2cce063a461903e1a4aeb40e +2026-09-05-sidebar-tab-types-and-navigation.zh.md: e4e8de5a389fea77aae705d63cc4fb16f88af647 diff --git a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md index 834a26e55f..599ce36ed4 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md +++ b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.md @@ -36,7 +36,7 @@ interface SidebarRightTabDefinition { `priority` is one of three literal bands, spelled as strings so that a type from another package needs no runtime import: `extension` is the band of a type from outside the product and the highest, so a type that declares nothing outranks every viewer shipped here; `builtin` is the ordinary band for shipped types; `fallback` is the plain-content position that anything more specific should beat, which VS Code's text editor holds implicitly and our text preview holds explicitly. `candidates(address)` returns every type whose globs match and whose `canOpen` does not veto, ranked by band, then by the length of the longest pattern that matched, then by registration order. `claim(address, kind?)` takes the best candidate, or the named kind's type in force when the caller overrides (its globs are not consulted; naming the type is the decision), and throws for an address nothing will open — a wiring mistake, not a user error. `get(kind)` returns the type in force; `entries()` and `guide()` list the types and their guide boxes in force; `subscribe` observes changes. -`title(address)`, `guide[].title()`, and optional `guide[].description()` are thunks read on every use, so a language change needs no re-registration. [Guide start page and stat pill refinements](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the current description visibility and fallback-glyph rules. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error. +`title(address)`, `guide[].title()`, and optional `guide[].description()` are thunks read on every use, so a language change needs no re-registration. The [Sidebar README](../../../../packages/client/ui-sidebar-right/README.md#the-guide) defines description visibility and fallback-glyph rules. The registry itself is a plain object provided at `apply`'s top level **without** `Service.tracker`: a tracker would rebind `this.ctx` to the caller's context, and a cross-package `register()` would then add its effect to the caller's fiber while that fiber is the active scope, stalling the browser boot with no error. ### Bodies and titles: keyed Slot seats under the definition's `id` diff --git a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md index 9884db9eef..e4e8de5a38 100644 --- a/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md @@ -36,7 +36,7 @@ interface SidebarRightTabDefinition { `priority` 是三个字面量档位之一,写成字符串,好让别的包的类型不需要任何运行时引入:`extension` 是来自产品之外的类型的档位也是最高档,所以什么都不声明的类型压过这里随包交付的每个查看器;`builtin` 是随包类型的常规档;`fallback` 是任何更具体的东西都应压过的纯内容位置,VS Code 的文本编辑器隐含地占据它,我们的文本预览明确地占据它。`candidates(address)` 返回 glob 命中且 `canOpen` 未否决的每个类型,按档位、再按命中的最长 pattern 长度、再按注册顺序排序。`claim(address, kind?)` 取最佳候选,或在调用方指定时取该 kind 生效的类型(不查它的 glob;点名即决定),对无人愿开的地址抛错——这是接线错误,不是用户错误。`get(kind)` 返回生效类型;`entries()` 与 `guide()` 列出生效类型及其引导入口;`subscribe` 观察变化。 -`title(address)`、`guide[].title()` 与可选的 `guide[].description()` 都是每次使用时重读的 thunk,语言切换无需重新注册。[引导起始页与统计 pill 的细化](../feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责当前的描述显示与兜底图标规则。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。 +`title(address)`、`guide[].title()` 与可选的 `guide[].description()` 都是每次使用时重读的 thunk,语言切换无需重新注册。[Sidebar README](../../../../packages/client/ui-sidebar-right/README.zh.md#the-guide)定义描述显示与兜底图标规则。注册表本身是 `apply` 顶层提供的普通对象,**不带** `Service.tracker`:tracker 会把 `this.ctx` 重绑到调用方上下文,跨包 `register()` 就会在调用方 fiber 仍是活动作用域时往它上加 effect,浏览器启动会无声卡死。 ### 正文与标题:按定义 `id` keyed 的 Slot 坑位 diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml index 60e03dbff7..414fda0806 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml @@ -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-08-document-preview-operations.md -2026-09-08-document-preview-operations.md: 980e2e443db13d6a956d554172bc765a5c11b435 -2026-09-08-document-preview-operations.zh.md: 364ef9ced6f8d98313f9bc287588c98daa408837 +2026-09-08-document-preview-operations.md: be3a835a6296cb28d9a90c5c5e8b9bf7367598c2 +2026-09-08-document-preview-operations.zh.md: efb87e16cc126001c1a6eb2bf938124d7e00044a diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md index 980e2e443d..be3a835a62 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md @@ -18,7 +18,7 @@ Readable files use `dsh-resource://file/session//`. The path ma [Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists supported alternatives and remembers a manual choice per tab. Plain text remains available for unknown extensions and text-compatible renderers. Registered binary suffixes determine text compatibility independently of loading mode; HTML and SVG remain outside those suffixes and retain source viewing. A single candidate renders no viewer control. Registered binary suffixes omit plain text; known binary suffixes without a renderer show an unsupported state without reading the file ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls existing `read`, `readAll`, and `readRelated` through ordinary injection and decodes bytes in its own `rpc.ts`. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session. -Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `` static-image context. An image wider than the pane scales down to its width at its aspect ratio; a smaller image keeps its intrinsic CSS-pixel dimensions centred by auto margins, and a taller image extends the shared scroller's vertical range ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL. +Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete `Uint8Array` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. With [Developer tools](../feature/2026-09-17-developer-tools-settings.md) enabled, HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an `` static-image context. An image wider than the pane scales down to its width at its aspect ratio; a smaller image keeps its intrinsic CSS-pixel dimensions centred by auto margins, and a taller image extends the shared scroller's vertical range ([sidebar preview polish](../feature/2026-09-11-sidebar-document-preview-polish.md)). The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL. PDF.js's official TextLayerBuilder owns selection boundaries and copy normalization over the width-fitted canvas, with shared page cleanup and a component-owned resize observer. Responsive sizing uses the CSS `scale` property independently of PDF.js's page rotation and translation transforms. Its end-of-content marker and stacking rules constrain selection in blank regions; line-break highlighting is suppressed. Per-page cancellation uses the builder's cleanup rather than aborting the first page's signal, because the official selection listeners are shared across pages. diff --git a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md index 364ef9ced6..efb87e16cc 100644 --- a/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md @@ -18,7 +18,7 @@ Document Preview 将资源观察与内容读取分开。[资源模型](2026-09-0 [Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出受支持的候选,按 tab 记住手动选择。未知扩展名及文本兼容的渲染器仍可使用纯文本。文本兼容性由注册的二进制后缀决定,与加载方式无关;HTML 和 SVG 不属于二进制后缀,保留源码查看。只有一个候选时不显示查看器控件。注册声明为二进制的后缀不提供纯文本;已知二进制后缀没有注册渲染器时不发起读取,并显示不支持预览的空态([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 经普通注入调用既有 `read`、`readAll` 与 `readRelated`,在自己的 `rpc.ts` 解码字节。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。 -Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `` 静态图片上下文中渲染。比面板宽的图片按纵横比缩小到面板宽度;较小的图片保留固有 CSS 像素尺寸并由 auto margin 居中,较高的图片扩展共享滚动区的纵向范围([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。 +Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 `Uint8Array` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。开启[开发者工具](../feature/2026-09-17-developer-tools-settings.zh.md)时,HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 `` 静态图片上下文中渲染。比面板宽的图片按纵横比缩小到面板宽度;较小的图片保留固有 CSS 像素尺寸并由 auto margin 居中,较高的图片扩展共享滚动区的纵向范围([侧边栏预览打磨](../feature/2026-09-11-sidebar-document-preview-polish.zh.md))。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。 PDF.js 官方 TextLayerBuilder 在适配宽度的 canvas 上负责选择边界和复制规范化,共享页面清理,并使用由组件拥有的 resize observer。响应式尺寸适配使用独立的 CSS `scale` 属性,与 PDF.js 的页面旋转和平移变换组合。其内容结束标记和堆叠规则限制空白区域中的选择;换行高亮被抑制。逐页取消使用 builder 的清理操作,而不 abort 第一页的信号,因为官方选择监听器跨页面共享。 diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml index e4d4d80da4..97cdb07d3f 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.i18n.yaml @@ -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-09-workspace-file-read-authority.md -2026-09-09-workspace-file-read-authority.md: 35ac15f5e463910be3b3a0cfe7b80dc69c129ad3 -2026-09-09-workspace-file-read-authority.zh.md: 559a59af0bfa988ff880fad22470397e4e74f296 +2026-09-09-workspace-file-read-authority.md: d1a5b9d9634381cb74b17651c67f2b701402ff1c +2026-09-09-workspace-file-read-authority.zh.md: f8b0fae4325a90548c6b9dc33ac46b17248b04e7 diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md index 35ac15f5e4..d1a5b9d963 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md +++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.md @@ -14,7 +14,7 @@ Workspace Files serves both file content and workspace navigation. Applying work `list` and `changes` remain workspace-scoped because they expose workspace navigation and observation rather than a named file read. `list` rejects a directory outside the root, and `changes` filters observations through the backend's workspace-containment predicate. -`readRelated` resolves a relative path from the base file's directory. A `..` path may therefore read JavaScript or CSS outside the workspace when the Session backend permits it. Document Preview packages bounded, statically declared local scripts and stylesheets into an HTML Blob iframe with `sandbox="allow-scripts"`; the opaque origin blocks parent access, but the browser retains normal network access. This exposure is an intentional security trade-off for rendering static generated HTML. +`readRelated` resolves a relative path from the base file's directory. A `..` path may therefore read JavaScript or CSS outside the workspace when the Session backend permits it. With [Developer tools](../feature/2026-09-17-developer-tools-settings.md) enabled, Document Preview packages bounded, statically declared local scripts and stylesheets into an HTML Blob iframe with `sandbox="allow-scripts"`; the opaque origin blocks parent access, but the browser retains normal network access. This exposure is an intentional security trade-off for rendering static generated HTML. The [Workspace Files service](2026-09-05-workspace-files-service.md) owns paging, file checks, listing, and observation. [Document Preview](2026-09-08-document-preview-operations.md) owns which related files are packaged and the iframe sandbox. @@ -26,4 +26,4 @@ The [Workspace Files service](2026-09-05-workspace-files-service.md) owns paging ## Consequences -Any caller holding a valid Session file address can receive bytes from every regular file that the Session filesystem backend permits it to read, including files outside the workspace. A previewed HTML document can execute packaged local JavaScript and make network requests. Outside files do not produce `changes` frames, so their previews require explicit refresh to observe updates. +Any caller holding a valid Session file address can receive bytes from every regular file that the Session filesystem backend permits it to read, including files outside the workspace. With Developer tools enabled, a previewed HTML document can execute packaged local JavaScript and make network requests. Outside files do not produce `changes` frames, so their previews require explicit refresh to observe updates. diff --git a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md index 559a59af0b..f8b0fae432 100644 --- a/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-09-workspace-file-read-authority.zh.md @@ -14,7 +14,7 @@ Workspace Files 同时提供文件内容与工作区导航。对所有操作应 `list` 与 `changes` 仍限于工作区,因为它们暴露工作区导航和观察,而不是读取一个具名文件。`list` 拒绝根外目录,`changes` 通过后端的工作区包含判定过滤观察。 -`readRelated` 从基准文件所在目录解析相对路径。因此,只要 Session 后端允许,`..` 路径就可以读取工作区外的 JavaScript 或 CSS。Document Preview 把有界、静态声明的本地脚本与样式表打包进带 `sandbox="allow-scripts"` 的 HTML Blob iframe;不透明源阻止访问父应用,但浏览器保留正常网络访问。这种暴露是为渲染静态生成 HTML 而有意接受的安全取舍。 +`readRelated` 从基准文件所在目录解析相对路径。因此,只要 Session 后端允许,`..` 路径就可以读取工作区外的 JavaScript 或 CSS。开启[开发者工具](../feature/2026-09-17-developer-tools-settings.zh.md)时,Document Preview 把有界、静态声明的本地脚本与样式表打包进带 `sandbox="allow-scripts"` 的 HTML Blob iframe;不透明源阻止访问父应用,但浏览器保留正常网络访问。这种暴露是为渲染静态生成 HTML 而有意接受的安全取舍。 [Workspace Files 服务](2026-09-05-workspace-files-service.zh.md)负责分页、文件检查、列举和观察。[Document Preview](2026-09-08-document-preview-operations.zh.md)负责选择要打包的关联文件及 iframe sandbox。 @@ -26,4 +26,4 @@ Workspace Files 同时提供文件内容与工作区导航。对所有操作应 ## Consequences -持有有效 Session 文件地址的调用方可以接收 Session 文件系统后端允许读取的每个普通文件的字节,包括工作区外文件。预览的 HTML 文档可以执行已打包的本地 JavaScript,并发起网络请求。工作区外文件不会产生 `changes` 帧,因此其预览需要显式刷新才能观察更新。 +持有有效 Session 文件地址的调用方可以接收 Session 文件系统后端允许读取的每个普通文件的字节,包括工作区外文件。开启开发者工具时,预览的 HTML 文档可以执行已打包的本地 JavaScript,并发起网络请求。工作区外文件不会产生 `changes` 帧,因此其预览需要显式刷新才能观察更新。 diff --git a/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.i18n.yaml index 5ab066c127..c27ef04676 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.i18n.yaml @@ -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-11-node-office-kit.md -2026-09-11-node-office-kit.md: d77d6c5392532f39a9993af60faad6474bc8f36c -2026-09-11-node-office-kit.zh.md: a0cbd22e4f06bd4f2878adf27852f9d2378b8b33 +2026-09-11-node-office-kit.md: 6cab947d7cb8d4c2c416a83f0b765813c7f98ea9 +2026-09-11-node-office-kit.zh.md: 8ed1a81922249cf0b9c1a74b06e5ce9d44696e0a diff --git a/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.md b/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.md index d77d6c5392..6cab947d7c 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.md +++ b/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.md @@ -22,7 +22,7 @@ The kit owns default serif, sans-serif, and monospace preference groups, includi DSH exports raster images at configurable resolution, defaulting to 192 DPI for the shared PDF canvas's 96 CSS DPI at device-pixel ratio 2. Text and vectors remain scalable; explicit bookmark export preserves the engine default when JSON filter options replace it. Node WASM downscales images with LibreOffice's CPU filter. Native conversion uses its separate platform engine. -The [Office viewer](../../../../packages/client/ui-sidebar-documentpreview/README.md#office-preview) lives under Document Preview’s `client/office/` directory, alongside the loading lifecycle, PDF body, and reader types it uses. Keeping these components in one package removes an independent UI boot entry without creating cross-plugin runtime imports. Its bounded cache validates authorized source metadata, shares pending conversions between readers, cancels only when the final reader leaves, excludes failures, and clears on connection reset. Conversion starts when a user opens a preview. Missing declared font families accompany the PDF and appear in a dismissible notice above the Office scrollport; font-table inventories and unrelated engine defaults are not warnings. The shared preview entry’s `office` cache settings use the existing page-global injection channel because the module boot graph carries package identities, not Loader configuration. Reloading the page adopts updated YAML values. +The [Office viewer](../../../../packages/client/ui-sidebar-documentpreview/README.md#office-preview) lives under Document Preview’s `client/office/` directory, alongside the loading lifecycle, PDF body, and reader types it uses. Keeping these components in one package removes an independent UI boot entry without creating cross-plugin runtime imports. Its bounded cache validates authorized source metadata, shares pending conversions between readers, cancels only when the final reader leaves, excludes failures, and clears on connection reset. Conversion starts when a user opens a preview. Missing declared font families accompany the PDF and are available from the document toolbar’s warning icon; font-table inventories and unrelated engine defaults are not warnings. The shared preview entry’s `office` cache settings use the existing page-global injection channel because the module boot graph carries package identities, not Loader configuration. Reloading the page adopts updated YAML values. Ordinary file reads and Office responses share `documentFileBytes()`, which decodes into one typed byte buffer rather than materializing a JavaScript element array from the binary string. Element-array expansion can exhaust the browser heap for a PDF that the configured Host limits permit. A child-process regression checks byte equality within a fixed heap, while the built browser scenario verifies the transport and PDF Worker together. Cache byte limits do not bound transient transport or viewer memory. diff --git a/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.zh.md b/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.zh.md index a0cbd22e4f..8ed1a81922 100644 --- a/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-11-node-office-kit.zh.md @@ -22,7 +22,7 @@ kit 维护 serif、sans-serif 和 monospace 的默认优先组,其中包含中 DSH 按可配置分辨率导出栅格图片,默认 192 DPI,对应共享 PDF 画布在设备像素比 2 时的 96 CSS DPI。文本与矢量仍可缩放;JSON 过滤选项替代隐式选项时,显式书签导出保留引擎默认行为。Node WASM 使用 LibreOffice 的 CPU 过滤器降采样图片。原生转换使用独立的平台引擎。 -[Office 查看器](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md#office-preview)位于文档预览的 `client/office/` 目录,与其使用的加载生命周期、PDF 正文和读取器类型同属一个包。这些组件放在同一包中,既减少一个独立 UI 启动入口,也无需跨插件运行时导入。其有界缓存校验已授权的源元数据,在读取方之间共享待完成转换,仅在最后一个读取方离开时取消,不缓存失败,并在连接重置时清空。用户打开预览时才开始转换。缺失的已声明字体族随 PDF 返回,在 Office 滚动区上方显示可关闭的提示;字体表清单与无关的引擎默认字体不构成警告。共享预览入口的 `office` 缓存设置复用页面全局注入通道,因为模块启动图携带包标识而不传递 Loader 配置。重新加载页面后采用更新的 YAML 值。 +[Office 查看器](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md#office-preview)位于文档预览的 `client/office/` 目录,与其使用的加载生命周期、PDF 正文和读取器类型同属一个包。这些组件放在同一包中,既减少一个独立 UI 启动入口,也无需跨插件运行时导入。其有界缓存校验已授权的源元数据,在读取方之间共享待完成转换,仅在最后一个读取方离开时取消,不缓存失败,并在连接重置时清空。用户打开预览时才开始转换。缺失的已声明字体族随 PDF 返回,可通过文档工具栏的警告图标查看;字体表清单与无关的引擎默认字体不构成警告。共享预览入口的 `office` 缓存设置复用页面全局注入通道,因为模块启动图携带包标识而不传递 Loader 配置。重新加载页面后采用更新的 YAML 值。 普通文件读取与 Office 响应共享 `documentFileBytes()`,解码使用一个类型化字节缓冲区,不将二进制字符串物化为 JavaScript 元素数组。即使 PDF 符合 Host 配置的大小限制,元素数组展开也可能耗尽浏览器堆内存。子进程回归测试在固定堆容量内检查字节一致性,构建后的浏览器场景则一起验证传输和 PDF Worker。缓存字节限制不约束临时传输或查看器内存。 diff --git a/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.i18n.yaml index 8ddfd2d49e..2f64b37f55 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.i18n.yaml @@ -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-bounded-office-conversion.md -2026-09-15-bounded-office-conversion.md: 78a2fc46d97500575262d2263bf5549037872264 -2026-09-15-bounded-office-conversion.zh.md: 88cb15c8cbf2200a9365b9a42cb5802c1bfb65d8 +2026-09-15-bounded-office-conversion.md: b15aa5421c575451ddb4a7bf91169bcb2fa38d3c +2026-09-15-bounded-office-conversion.zh.md: f23c38096764cf32207125cc0c3ebbdf1c04d009 diff --git a/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.md b/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.md index 78a2fc46d9..b15aa5421c 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.md +++ b/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.md @@ -37,3 +37,7 @@ The cache is transient and cannot bypass source authorization. Oversized PDFs ca Office preview checks source authorization and versions through Workspace Files, then reads raw input with `fs.readBytes` within its conversion reservation. Office input limits govern that read. The Host conversion path avoids base64 source allocation; PDF responses encode only the converted output. Controlled source and engine completions verify pre-read admission, content joining, priority, cancellation isolation, delayed resource release, LRU/alias limits, stale versions, and converter replacement. Loader composition and native conversion checks exercise the shared provider independently of presentation consumers. + +## Related + +The [background scheduling proposal](../../proposed/simplification/2026-09-17-defer-office-background-scheduling.md) questions the need for speculative priority without a production caller; this note remains the authority for the implemented behavior. diff --git a/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.zh.md b/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.zh.md index 88cb15c8cb..f23c380967 100644 --- a/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-15-bounded-office-conversion.zh.md @@ -37,3 +37,7 @@ Office 预览和显式文档检查可能请求相同转换。仅缓存已完成 Office 预览通过 Workspace Files 检查源文件授权与版本,再使用 `fs.readBytes` 在转换预留容量内读取原始输入。该读取受 Office 输入上限约束。Host 转换路径不分配 base64 源数据;PDF 响应只编码转换后的输出。 受控的源读取和引擎完成验证读取前准入、内容合并、优先级、取消隔离、延迟资源释放、LRU 与别名限额、过期版本及转换器替换。Loader 组合与原生转换检查独立于展示消费者验证共享提供方。 + +## 相关记录 + +[后台调度提案](../../proposed/simplification/2026-09-17-defer-office-background-scheduling.zh.md) 质疑没有生产调用方时保留推测性优先级的必要性;本记录仍定义已实现的行为。 diff --git a/.agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.md b/.agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.md deleted file mode 100644 index 26fd635c6c..0000000000 --- a/.agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.md +++ /dev/null @@ -1,37 +0,0 @@ -# Agent Note: Show a local document while the policy login page loads - -Status: implemented - -English | [中文](2026-09-16-desktop-policy-login-loading.zh.md) - -## Problem - -The test-deployment login window ([mandatory-update client](../feature/2026-09-11-desktop-mandatory-update-client.md)) navigated straight to the policy origin. Until that document committed and painted, the window was blank, and the remote page can take seconds to appear. A user pressed "Sign in with Feishu" and saw an empty window with no sign that anything was happening — while the window had to stay closable and must never cover a page that had become interactive. - -## Decision - -[policy-test-auth.ts](../../../../apps/desktop/src/policy-test-auth.ts) loads [renderer/policy-login-loading.html](../../../../apps/desktop/renderer/policy-login-loading.html) as the window's first document and requests the policy origin only after that document has committed. The placeholder is a packaged, self-contained file whose only text is the label the main process passes through `loadFile`'s query, so the copy stays in the shell locale dictionary (`policyLoginLoading`). The first committed remote document replaces it. - -The placeholder never coexists with the remote page, so there is no removal step and no timer: Chromium keeps the placeholder frame until the remote document's first frame is ready, and from then on the third-party page owns the window. A redirect chain keeps the placeholder until the last document commits. The window is created visible and closable as before. - -Two places recognize the placeholder's own load by file name and nothing else: the Session's `onBeforeRequest` filter, which cancels document requests outside the policy and Feishu origins, and the `did-fail-load` handler, which fails the login on a main-frame error. The `will-navigate` and `will-redirect` guards still require an allowed HTTPS origin, so the remote page cannot steer the window back to a local file. A placeholder that cannot load is not a login failure: the login request starts anyway and the window is blank exactly as it was before this change. - -## Alternatives considered - -**Overlay the window with a `WebContentsView` (or `BrowserView`) and remove it when the page is ready.** It would show feedback even earlier and can be removed at any chosen signal, but it needs a second renderer, a position that follows window resizes, and an explicit removal point. Every available removal signal is either late (`did-finish-load` still waits for subresources) or a guess (`dom-ready` can precede the first paint), and a view left in place covers an interactive page. - -**Inject an overlay element into the login page with a preload script.** The one approach the decision has to keep out: it puts a shell-owned DOM layer inside a third-party page, and it would give that page a preload bridge the isolated Session deliberately withholds (`webPreferences.preload` is unset). - -**Keep the window hidden until the remote page finishes loading.** It removes the blank flash by removing the feedback, and it delays the user's first view of the page until every subresource completes. - -**Show a native splash window, or the loading text in the window title.** A second window adds a focus and lifetime problem for a two-second wait, and a title is not visible feedback inside the window. - -**Reveal the page with a fixed delay.** A timer cannot distinguish a slow origin from a fast one, and it either covers a ready page or uncovers a blank one. - -## Consequences - -The login window gives immediate local feedback that cannot depend on the network: the placeholder's CSP sets `default-src 'none'`, so the document has no origin, font, image, or connection to reach. Because the placeholder is the window's document rather than a layer, nothing about it can survive into the third-party page, and a slow subresource on the login page cannot extend it. - -The placeholder shares the login Session, so its request passes through the same document filter; the filter's exemption is name-scoped and does not widen the allowed-origin check for navigation. The window title remains the shell-owned login title (the renderer prevents `page-title-updated`), so the placeholder contributes no copy of its own. - -[policy-test-auth.spec.ts](../../../../apps/desktop/tests/policy-test-auth.spec.ts) covers the order (placeholder first, remote page only after it settles), the filter's acceptance of the placeholder, a failed placeholder load that must not fail the login, and a window closed while the placeholder is loading that never starts the remote page. [policy-login-loading.spec.ts](../../../../apps/desktop/tests/policy-login-loading.spec.ts) loads the packaged document under jsdom and checks that it renders the label it is given, leaves the label empty without one, and forbids every network source. The placeholder's rendered frames and the first-paint timing of the substituted login page remain unverified against a real login window. diff --git a/.agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.zh.md b/.agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.zh.md deleted file mode 100644 index 16de25323a..0000000000 --- a/.agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.zh.md +++ /dev/null @@ -1,37 +0,0 @@ -# Agent Note: 策略登录页加载期间显示本地文档 - -Status: implemented - -[English](2026-09-16-desktop-policy-login-loading.md) | 中文 - -## 问题 - -测试部署的登录窗口([强制更新客户端](../feature/2026-09-11-desktop-mandatory-update-client.zh.md))直接导航到策略 origin。在该文档提交并绘制之前,窗口是空白的,而远端页面可能需要数秒才出现。用户点击“通过飞书登录”后只看到一个空窗口,没有任何正在进行的迹象——同时窗口必须保持可关闭,也绝不能遮挡已经可交互的页面。 - -## 决策 - -[policy-test-auth.ts](../../../../apps/desktop/src/policy-test-auth.ts)把 [renderer/policy-login-loading.html](../../../../apps/desktop/renderer/policy-login-loading.html) 作为窗口的首个文档载入,并在该文档提交之后才请求策略 origin。占位页是随包发布的、自包含的文件,其中唯一的文本是主进程通过 `loadFile` 的 query 传入的文案,因此文案仍由 shell 本地化字典(`policyLoginLoading`)拥有。首个提交的远端文档会替换它。 - -占位页从不与远端页面共存,因此没有移除步骤,也没有计时器:Chromium 会一直保留占位帧,直到远端文档的首帧就绪;此后窗口归第三方页面所有。重定向链会把占位页保持到最后一个文档提交。窗口仍与之前一样在创建时即显示且可关闭。 - -只有两处按文件名识别占位页自身的加载,不识别其他任何东西:Session 的 `onBeforeRequest` 过滤器(它会取消策略与飞书 origin 之外的文档请求)以及 `did-fail-load` 处理器(主框架出错时让登录失败)。`will-navigate` 与 `will-redirect` 守卫仍然要求允许的 HTTPS origin,因此远端页面无法把窗口引回本地文件。占位页无法载入不算登录失败:登录请求照常开始,窗口与本次改动之前一样保持空白。 - -## 考虑过的替代方案 - -**用 `WebContentsView`(或 `BrowserView`)覆盖窗口,并在页面就绪时移除。** 它能更早显示反馈,也可以在任选信号处移除,但需要第二个渲染器、一个随窗口尺寸变化的位置,以及一个明确的移除时机。可用的移除信号要么偏晚(`did-finish-load` 仍会等待子资源),要么只是猜测(`dom-ready` 可能早于首帧绘制);而一旦忘记移除,视图就会遮挡可交互的页面。 - -**通过 preload 脚本向登录页注入覆盖元素。** 这是决策必须排除的做法:它把 shell 拥有的 DOM 层放进第三方页面,并且会给该页面一个隔离 Session 刻意不提供的 preload 桥(`webPreferences.preload` 未设置)。 - -**在远端页面加载完成前保持窗口隐藏。** 它通过取消反馈来消除空白闪烁,并把用户第一次看到页面的时间推迟到所有子资源完成之后。 - -**使用原生启动窗口,或把加载文本放进窗口标题。** 为一个两秒的等待增加第二个窗口会带来焦点与生命周期问题,而标题不是窗口内的可见反馈。 - -**用固定延时揭示页面。** 计时器无法区分慢 origin 与快 origin,结果要么遮挡已就绪的页面,要么在空白页面上取消遮挡。 - -## 结果 - -登录窗口给出不依赖网络的即时本地反馈:占位页的 CSP 设置了 `default-src 'none'`,因此该文档没有任何 origin、字体、图片或连接可用。由于占位页是窗口的文档而不是覆盖层,它的任何部分都不会残留到第三方页面中,登录页上缓慢的子资源也无法延长它的存在。 - -占位页共享登录 Session,因此它的请求会经过同一个文档过滤器;该过滤器的豁免按文件名限定,不会放宽导航所用的允许 origin 检查。窗口标题仍是 shell 拥有的登录标题(渲染器阻止了 `page-title-updated`),因此占位页不贡献自己的文案。 - -[policy-test-auth.spec.ts](../../../../apps/desktop/tests/policy-test-auth.spec.ts)覆盖顺序(先占位页,只有它稳定后才请求远端页面)、过滤器对占位页的接受、占位页载入失败不得导致登录失败,以及占位页加载期间关闭窗口后绝不启动远端页面。[policy-login-loading.spec.ts](../../../../apps/desktop/tests/policy-login-loading.spec.ts)在 jsdom 下载入随包发布的文档,检查它渲染传入的文案、在没有文案时保持为空,并禁止一切网络来源。占位页的实际渲染帧与替代登录页的首帧时机仍未在真实登录窗口上验证。 diff --git a/.agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.md b/.agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.md deleted file mode 100644 index 1b06a3a1a4..0000000000 --- a/.agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: Suppress an enclosing tooltip while a nested one is shown - -Status: implemented - -English | [中文](2026-09-16-nested-tooltip-suppression.zh.md) - -## Problem - -The collapsed sidebar renders its update badge through the `sidebar.toggle.badge` slot inside the expand button, and both the button and the badge attach a `Tooltip`. Hovering the badge showed two bubbles at once — the version notice and "Open sidebar" — because the badge is a DOM descendant of the button: the button's hover state stays true for as long as the pointer rests on the badge, and each tooltip correctly follows its own anchor. - -## Decision - -[Tooltip](../../../../packages/client/ui-primitives/src/Tooltip.tsx) provides a suppression setter through an internal `TooltipSuppression` context wrapped around its cloned anchor and its bubble. A nested tooltip consumes the nearest setter and announces whenever its own bubble becomes visible. An enclosing tooltip that receives a suppression claim keeps its hover/focus triggers and its measured position but does not render its bubble, so the enclosing bubble returns in the same commit that the nested one disappears. - -The nested tooltip announces synchronously inside `show()` and through a `visible` effect. The synchronous call keeps a nested pair that becomes visible in one React commit from painting both bubbles for a frame; the effect releases the claim on every other path that hides a bubble, including unmount and a `disabled` flip. - -No component in `ui-sidebar` or `ui-settings-general` changed: the badge already renders inside the toggle's anchor, so the shared primitive resolves the overlap for any future nested pair as well. - -## Alternatives considered - -**Disable the toggle tooltip whenever an update exists.** The simplest guard, and wrong: it removes "Open sidebar" from ordinary rail hovers for the whole time an update is available, including hovers that never touch the badge. - -**Disable the toggle tooltip while the pointer is over the badge.** Hover-scoped, but `Tooltip` clears its hover and focus triggers when it is disabled, and no new `mouseenter` fires on the button while the pointer moves from the badge onto the button's own area. The enclosing bubble would stay gone until the pointer left and re-entered the button. - -**Stop the badge's `mouseenter` from reaching the button.** It only helps a pointer that enters the badge directly; a pointer that enters the button first and then moves onto the badge already has the enclosing bubble visible, because the enclosing hover never ended. - -**Render the badge outside the toggle's anchor.** The slot is declared inside the toggle button, and the badge must sit on the button's corner; moving the anchor out of the button would either detach the bubble from the control or change the slot's declared placement. - -## Consequences - -The rule is a property of tooltip nesting, not of the update badge: any tooltip whose anchor contains another tooltip now yields the bubble to the innermost visible one. A tooltip with no nested tooltip is unaffected — it consumes a null context and never announces. - -The enclosing tooltip's bubble is withdrawn only while a descendant is visible; its trigger state survives, so no re-entry is needed to restore it. Suppression does not change what the anchors do: the button still toggles the sidebar and the badge remains a non-interactive marker. - -[Tooltip tests](../../../../packages/client/ui-primitives/tests/tooltip.client.spec.tsx) cover withdrawal, restoration when the nested anchor is left for the enclosing one with a real `relatedTarget`, and release when a shown nested tooltip unmounts. The [sidebar shell test](../../../../packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx) covers the product wiring: the rail badge replaces the toggle bubble after the toggle's own hover delay has already elapsed. diff --git a/.agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.zh.md b/.agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.zh.md deleted file mode 100644 index 96063f78b2..0000000000 --- a/.agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.zh.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: 嵌套提示显示时抑制外层提示 - -Status: implemented - -[English](2026-09-16-nested-tooltip-suppression.md) | 中文 - -## 问题 - -侧边栏收起时,更新蓝点通过 `sidebar.toggle.badge` 插槽渲染在展开按钮内部,而按钮和蓝点各自挂了一个 `Tooltip`。hover 蓝点会同时出现两个气泡——版本提示和“打开侧边栏”——因为蓝点是按钮的 DOM 后代:只要指针停在蓝点上,按钮的 hover 状态就保持为真,而两个 tooltip 都在正确地跟随各自的 anchor。 - -## 决策 - -[Tooltip](../../../../packages/client/ui-primitives/src/Tooltip.tsx)通过内部 `TooltipSuppression` context 提供一个抑制 setter,并把该 context 包在克隆后的 anchor 与气泡外层。嵌套的 tooltip 消费最近的 setter,并在自己的气泡可见时上报。收到抑制声明后,外层 tooltip 保留 hover/focus 触发状态与已测量的位置,但不渲染自己的气泡,因此嵌套气泡消失的同一提交里外层气泡就会回来。 - -嵌套 tooltip 既在 `show()` 中同步上报,也通过 `visible` effect 上报。同步调用避免在同一 React 提交中同时可见的一对工具提示多绘制一帧的两个气泡;effect 则在其他所有隐藏气泡的路径上释放抑制,包括卸载和 `disabled` 翻转。 - -`ui-sidebar` 与 `ui-settings-general` 中的组件无需改动:蓝点本就渲染在 toggle 的 anchor 内,因此共享 primitive 也顺带解决了未来任何嵌套组合。 - -## 考虑过的替代方案 - -**只要存在更新就禁用 toggle 的 tooltip。** 这是最简单的防护,但也是错的:它会在更新可用的整个期间移除普通 rail hover 的“打开侧边栏”,包括根本不会碰到蓝点的 hover。 - -**指针位于蓝点上时禁用 toggle 的 tooltip。** 作用域正确,但 `Tooltip` 被禁用时会清空自己的 hover 与 focus 触发状态,而指针从蓝点移到按钮自身区域时按钮不会再触发新的 `mouseenter`。外层气泡会一直消失,直到指针离开按钮并重新进入。 - -**阻止蓝点的 `mouseenter` 传播到按钮。** 这只能帮助直接从蓝点进入的指针;先进入按钮再移到蓝点上的指针,其外层气泡早已可见,因为外层的 hover 从未结束过。 - -**把蓝点渲染到 toggle 的 anchor 之外。** 该插槽声明在 toggle 按钮内部,蓝点必须落在按钮角上;把 anchor 移出按钮要么让气泡脱离控件,要么改变插槽声明的放置位置。 - -## 结果 - -这条规则是 tooltip 嵌套的性质,而不是更新蓝点的性质:任何 anchor 内含另一个 tooltip 的 tooltip 现在都会把气泡让给最内层可见的那个。没有嵌套 tooltip 的 tooltip 不受影响——它消费到的是 null context,永远不会上报。 - -外层 tooltip 仅在子级可见期间收回气泡;它的触发状态得以保留,因此不需要重新进入即可恢复。抑制不改变 anchor 的行为:按钮仍然切换侧边栏,蓝点仍然是非交互标记。 - -[Tooltip 测试](../../../../packages/client/ui-primitives/tests/tooltip.client.spec.tsx)覆盖收回、用真实 `relatedTarget` 从嵌套 anchor 移到外层 anchor 时的恢复,以及已显示的嵌套 tooltip 卸载时的释放。[侧边栏 shell 测试](../../../../packages/client/ui-sidebar/tests/sidebar-root.client.spec.tsx)覆盖产品接线:在 toggle 自身的 hover 延迟已经过去之后,rail 蓝点气泡仍替换掉 toggle 气泡。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.i18n.yaml similarity index 54% rename from .agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.i18n.yaml rename to .agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.i18n.yaml index c971fb1bfd..758bca04da 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.i18n.yaml @@ -1,6 +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-09-composer-placeholder-whitespace.md -2026-09-09-composer-placeholder-whitespace.md: c5d778f8fb1fdfc742636819a9ff27ddeeb0234e -2026-09-09-composer-placeholder-whitespace.zh.md: b5f82bcd86f8567976bba96095d4de9d42bae9b1 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md +2026-09-16-size-neutral-product-icon-weights.md: 3850fcbddfef6d096f648d5b3efa73790f40f8c1 +2026-09-16-size-neutral-product-icon-weights.zh.md: 84031677e9b13b8dbf3b8e5d959359824e0c94fc diff --git a/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md new file mode 100644 index 0000000000..3850fcbddf --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md @@ -0,0 +1,29 @@ +# Agent Note: Size-neutral product icon weights + +Status: implemented + +English | [中文](2026-09-16-size-neutral-product-icon-weights.zh.md) + +## Problem + +Shared product-icon exports encoded one historical render size in each symbol name even though every component accepted a `size` prop. Consumers also thickened selected icons through local CSS or separate drawings, so emphasis had no named library choice and could drift between features. + +## Decision + +Every shared product glyph exports a size-neutral `Regular` and `Medium` component. `Regular` renders the supplied one-pixel artwork, while `Medium` renders the same paths with a 1.3px inherited stroke; filled regions remain unchanged. The `size` prop controls rendered dimensions, and the former numeric suffix survives only as the default size when a glyph has no 16px source. + +`LinkIconRegular` and `LinkIconMedium`, `ReferenceIconRegular` and `ReferenceIconMedium`, and the three `PermissionIcon*Regular`/`PermissionIcon*Medium` pairs follow the same rule. Repository consumers use a named weight instead of a numeric export. + +Medium weight is reserved for deliberate emphasis: new-Session controls, Settings trigger and navigation icons, Appearance choices, the composer add button, and clickable artifact-link glyphs. Other existing consumers use Regular. The composer command menu uses `PermissionIconFullAccessRegular` for the permission command because that row names permission elevation rather than the current mode. + +## Alternatives considered + +**Keep pixel sizes in export names.** Rejected because the suffix duplicated the runtime `size` prop and created separate names for identical geometry. + +**Expose one component with a `weight` prop.** Rejected because call sites would hide the chosen weight inside props; explicit component names keep visual emphasis searchable and reviewable. + +**Override stroke width in consumer CSS.** Rejected because source SVG elements can carry their own stroke widths, and local overrides would recreate inconsistent weighting outside the library. + +## Consequences + +The icon API is intentionally breaking while pre-stable: every consumer names `Regular` or `Medium`, and former 14px call sites pass an explicit size when they share geometry with a 16px export. New product glyphs add both weights from one geometry definition. Fill-only artwork exposes both names for API consistency even when stroke weight does not change its appearance. The hierarchy choices above remain explicit at their render sites rather than becoming global size or opacity overrides. diff --git a/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md new file mode 100644 index 0000000000..84031677e9 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md @@ -0,0 +1,29 @@ +# Agent Note: 尺寸无关的产品图标线重 + +Status: implemented + +[English](2026-09-16-size-neutral-product-icon-weights.md) | 中文 + +## 问题 + +共享产品图标的导出名称包含一个历史渲染尺寸,但每个组件已经接受 `size` prop。消费方还会通过本地 CSS 或独立图稿加粗部分图标,视觉强调没有具名的图标库选项,因而可能在不同功能间漂移。 + +## 决策 + +每个共享产品图形都导出尺寸无关的 `Regular` 和 `Medium` 组件。`Regular` 渲染提供的 1px 图稿,`Medium` 使用相同路径并继承 1.3px 描边;填充区域保持不变。`size` prop 控制渲染尺寸;仅当图稿没有 16px 来源时,原数字后缀才作为默认尺寸保留。 + +`LinkIconRegular` 与 `LinkIconMedium`、`ReferenceIconRegular` 与 `ReferenceIconMedium`,以及三组 `PermissionIcon*Regular`/`PermissionIcon*Medium` 遵循同一规则。仓库内消费方使用具名线重,不再使用带数字的导出名。 + +Medium 线重只用于明确强调的场景:新建会话控件、设置入口与导航图标、外观选项、输入框加号按钮,以及可点击产物链接的图标。其他既有消费方使用 Regular。编辑器加号菜单中的权限命令使用 `PermissionIconFullAccessRegular`,因为该行表示权限提升,而不是当前权限模式。 + +## 考虑过的替代方案 + +**在导出名中保留像素尺寸。** 否决,因为后缀重复了运行时 `size` prop,并为相同几何创建了多个名称。 + +**只导出一个带 `weight` prop 的组件。** 否决,因为调用点会把线重选择隐藏在 prop 中;显式组件名便于搜索和审查视觉强调。 + +**在消费方 CSS 中覆盖描边宽度。** 否决,因为源 SVG 元素可能自带描边宽度,本地覆盖会在图标库外重新产生不一致线重。 + +## 后果 + +图标 API 在预稳定阶段有意进行破坏性调整:每个消费方明确选择 `Regular` 或 `Medium`;原 14px 调用点与 16px 导出共享几何时会显式传入尺寸。新增产品图形从同一份几何定义同时提供两种线重。仅填充的图稿也为 API 一致性导出两个名称,即使线重不会改变其外观。上述层级选择继续由对应渲染点显式声明,不会变成全局尺寸或透明度覆盖。 diff --git a/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.i18n.yaml similarity index 57% rename from .agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.i18n.yaml rename to .agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.i18n.yaml index 209fc7e492..a005e6d029 100644 --- a/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.i18n.yaml @@ -1,6 +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-09-turn-duration-hour-unit.md -2026-09-09-turn-duration-hour-unit.md: f0f5cb478dafa7938614bdeeec3a6de9f58aed8d -2026-09-09-turn-duration-hour-unit.zh.md: c9c94eef2a88a0f58d5f5a05332b4b5034ef977a +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.md +2026-09-18-agent-teams-single-bundle.md: 21878a8e481871f5b0eeebd8f1bea707411ff3b7 +2026-09-18-agent-teams-single-bundle.zh.md: 39c6bfb7b365a45ff98d29f91a4d7905b26db008 diff --git a/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.md b/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.md new file mode 100644 index 0000000000..21878a8e48 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.md @@ -0,0 +1,33 @@ +# Agent Note: Enable Agent Teams with one bundle + +Status: implemented + +English | [中文](2026-09-18-agent-teams-single-bundle.zh.md) + +## Problem + +Separate Agent Teams and Agent Teams Web switches require users to discover that tools and their browser controls need both selections. The package split exposes composition details in the plugin page without helping users choose a different Team capability. + +## Decision + +`@deepseek-ai/dsh-experimental-agent-team-profile` carries the Team service, tools, and browser UI in one optional bundle. Its patch retains the `agent-team`, `tool-agent-team`, and `ui-agent-team` row ids, so profile patches can still configure individual rows. The UI package has an inert Host entry; its browser entry mounts only in a Web Client, and headless does not start a Web server. + +The separate `@deepseek-ai/dsh-experimental-agent-team-web-profile` package is absent from the workspace and release family. The plugin page exposes one Team selection, disabled by default. + +This decision supersedes the separate Host/Web composition in the [package publication note](2026-08-18-experimental-agent-teams-packages.md) and the two Team selections in the [optional-bundle note](../process/2026-09-15-shipped-optional-bundles.md). Both remain active for publication, dependency isolation, promotion, and installation ownership. The [archived Web-controls note](../../archived/feature/2026-08-06-agent-teams-web.md) records the original split; its historical text remains frozen. + +## Alternatives considered + +**Keep two switches and explain their dependency.** Users still need two selections for one feature, and either selection alone leaves an incomplete browser experience. + +**Combine the entries in the generic profile loader or plugin manager.** Recognizing Team package names there would make shared loading or presentation code own feature-specific composition. The bundle patch already declares the required rows and dependencies. + +## Consequences + +Users enable tools and browser controls together. Separate installation-level selection of a UI-only Team bundle is unavailable; custom profiles still control individual patch rows. Headless installations include the UI dependency graph even though they do not mount its browser entry. + +Existing profiles that select the removed Web bundle have an upgrade compatibility gap: startup fails when that package cannot be resolved. Bundle composition provides no automatic rewrite of those saved selections. Compatibility handling remains separate from this composition decision. + +## Verification + +The plugin-manager browser test observes the Team controls appear after one switch, reads the roster and task board through Remote, and observes their removal after disabling the bundle. The Team-panel browser test covers task creation; the built headless CLI test covers delegation without a Web server. These scenarios do not cover upgrading a profile that still selects the removed Web bundle. diff --git a/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.zh.md b/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.zh.md new file mode 100644 index 0000000000..39c6bfb7b3 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-18-agent-teams-single-bundle.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 通过一个组合包启用 Agent Teams + +Status: implemented + +[English](2026-09-18-agent-teams-single-bundle.md) | 中文 + +## 问题 + +独立的 Agent Teams 与 Agent Teams Web 开关要求用户自行发现:工具与浏览器控件需要同时选中两项。包的拆分把组合细节暴露在插件页上,却没有帮助用户选择不同的 Team 能力。 + +## 决策 + +`@deepseek-ai/dsh-experimental-agent-team-profile` 在一个可选组合包中携带 Team 服务、工具与浏览器 UI。其 patch 保留 `agent-team`、`tool-agent-team` 与 `ui-agent-team` 配置行 id,因此 profile patch 仍可单独配置各行。UI 包的 Host 入口不执行逻辑;其浏览器入口只在 Web 客户端中挂载,headless 不会启动 Web 服务。 + +独立的 `@deepseek-ai/dsh-experimental-agent-team-web-profile` 包不在 workspace 与发布系列中。插件页提供一个 Team 选项,默认关闭。 + +本决策取代[包发布记录](2026-08-18-experimental-agent-teams-packages.zh.md)中的独立 Host/Web 组合方式,以及[可选组合包记录](../process/2026-09-15-shipped-optional-bundles.zh.md)中的两个 Team 选项。两条记录仍负责发布、依赖隔离、promotion 与安装方所有权。[已归档的 Web 控件记录](../../archived/feature/2026-08-06-agent-teams-web.md)记载原始拆分方式,其历史文本保持冻结。 + +## 曾考虑的替代方案 + +**保留两个开关并解释依赖关系。** 用户仍须为一个功能选中两项,单独选中任意一项都不能提供完整的浏览器体验。 + +**在通用 profile loader 或插件管理器中合并入口。** 在这些位置识别 Team 包名会让共用的加载或展示代码负责功能特定的组合。组合包 patch 已能声明所需配置行与依赖。 + +## 后果 + +用户同时启用工具与浏览器控件。安装层面不再提供仅选择 Team UI 组合包的入口;自定义 profile 仍能单独控制 patch 配置行。headless 安装包含 UI 依赖图,但不会挂载其浏览器入口。 + +选中了已移除 Web 组合包的已有 profile 存在升级兼容性缺口:无法解析该包时,启动会失败。组合包本身不会自动改写这些已保存的选择。兼容性处理与本组合决策分开。 + +## 验证 + +插件管理器浏览器测试观察一次开关后 Team 控件出现,通过 Remote 读取成员列表与任务看板,并观察关闭组合包后控件移除。Team 面板浏览器测试覆盖任务创建;构建后的 headless CLI 测试覆盖无需 Web 服务的委派。这些场景不覆盖仍选中已移除 Web 组合包的 profile 升级。 diff --git a/.agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.i18n.yaml similarity index 56% rename from .agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.i18n.yaml rename to .agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.i18n.yaml index 7079d4ec79..143d9ccffe 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-nested-tooltip-suppression.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.i18n.yaml @@ -1,6 +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-16-nested-tooltip-suppression.md -2026-09-16-nested-tooltip-suppression.md: 1b06a3a1a4f754aa78e821ee8a1479dab19873ff -2026-09-16-nested-tooltip-suppression.zh.md: 96063f78b281deb3561045bb73fcffa64a2c1ef8 +# 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 diff --git a/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.md b/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.md new file mode 100644 index 0000000000..d990c0cb9a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.md @@ -0,0 +1,59 @@ +# Agent Note: Bound turn navigation work and batch historical jumps + +Status: implemented + +English | [中文](2026-09-18-chat-navigation-performance.zh.md) + +## Problem + +A long Session makes three independent costs visible: a turn rail with one DOM mark per known turn, transcript navigation that rediscovers an already known landing, and historical jumps that publish every fetched page. Each intermediate prepend rebuilds growing conversation indexes and exposes another transcript update before the requested turn is available. Combining these concerns into one controller obscures which operations require DOM geometry and which only apply reading or paging policy. + +## Decision + +### Fixed-pitch virtual rail + +[TurnNavigator](../../../../packages/client/ui-chat/src/client/chat/TurnNavigator.tsx) uses TanStack React Virtual with a fixed 10px pitch. Only the visible range, overscan, and the focused mark with its neighbors remain mounted. The library owns the size container and absolute mark transforms. The full turn metadata remains resident; this is rail virtualization, not transcript virtualization. + +The rail uses ResizeObserver-delivered viewport dimensions and fixed item sizes. Its offset calculation clamps against cached geometry and invokes the supported `scrollBy` operation, avoiding the library's DOM-based maximum-offset calculation used by `scrollToIndex`. The first usable size places the latest committed active turn directly after the body has restored its reading state. A ref exposes independent activation and rail-only scrolling. Native pointer targets replace coordinate hit testing, and mark length changes use transforms. Layout containment belongs to the floating rail frame; it does not promise that browser geometry reads flush only that subtree. + +### Two-layer transcript navigation + +[ChatViewport](../../../../packages/client/ui-chat/src/client/chat/use-chat-viewport.ts) owns DOM reads, clamped writes, native events, and turn anchors. It returns the actual landing with available turn and semantic-position facts. Reading policy consumes those facts without another hit test; the viewport ignores its own unchanged scroll echoes and events from the nested rail. + +[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. + +### 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. + +### Theoretical work model + +Let `T` be known turns, `V` mounted virtual marks, `P` fetched pages, `B` entries per page, and `W0` entries already loaded. These are work-count estimates, not timing measurements. + +| Operation | Previous work | Current work | +|---|---|---| +| Rail DOM and React mark traversal | `O(T)` | `O(V)`; turn metadata and initial index/cache construction remain `O(T)` | +| Active-turn index lookup with unchanged items | Linear search, `O(T)` | Map lookup, expected `O(1)` | +| Rail target-offset calculation | Synchronous DOM extent/viewport reads | Fixed arithmetic and cached sizes; no DOM extent read on this path | +| Known transcript jump | Landing followed by position rediscovery | Reuse the landing; no hit test, but existing anchor-selector and turn lookup costs remain | +| History-driven linear index passes | `O(P * W0 + B * P²)` | `O(W0 + B * P)` for one final pass, plus `O(B * P)` buffer flattening | +| History-driven sorting | Repeated sorting of each growing window | One final-window sort, `O((W0 + B * P) log(W0 + B * P))` | +| Historical prepend publications | `P` | One; live-event and loading-state updates remain independent | + +For illustration, 10,000 turns and about 50 mounted marks imply roughly 99.5% fewer rail marks, not a 200× end-to-end speedup. With 20 equal pages and a negligible initial window, a full linear pass after each page visits `210B` entries in aggregate; one final pass visits `20B`, about 10.5× less work for that pass. Page requests remain 20, and the final transcript still needs assembly, rendering, and layout. + +## Alternatives considered + +**Containment or React batching alone.** Containment does not remove repeated JavaScript work. Suppressing React renders after each published page still leaves conversation assembly and index rebuilds upstream. + +**Patch the virtualizer.** The supported callbacks and relative-scroll operation cover fixed-height placement without maintaining a dependency fork. The native scroll container still needs a total extent; removing that extent would remove native scrolling, not virtualize it. + +**One large scrolling controller.** It centralizes state but couples DOM mechanics to follow and paging policy. Separate owners allow business changes without copying geometry operations. + +**Virtualize the whole transcript.** That introduces renderer lifetime and semantic-anchor constraints outside this change. Batching the requested historical jump removes intermediate publications without changing transcript residency. + +## Consequences + +Loading a distant turn retains the current transcript until the batch lands, instead of revealing intermediate pages. Buffered pages add references proportional to newly fetched entries. Main-transcript geometry reads, semantic-anchor queries, ordinary native-scroll metrics, final layout, and center-width reflow during Sidebar animation remain; none is claimed eliminated by this change. Transform and color changes may still repaint, and assigning `scrollTop` may still deliver a native scroll event. + +[Rail tests](../../../../packages/client/ui-chat/tests/turn-navigator.client.spec.tsx) cover bounded mounting, first placement, fixed-size controls, and focus retention. [Chat tests](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx) cover semantic restoration, follow attribution, own-scroll echoes, and navigation without hit testing. [Session tests](../../../../packages/api/session-controller/tests/session.client.spec.ts) cover atomic prepend publication with live delivery, failure, and replacement. [Browser navigation](../../../../apps/web/tests/chat-scroll-contract.e2e.ts) covers real scrolling and unloaded-turn landing. Functional evidence is separate from the theoretical estimates above. diff --git a/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.zh.md b/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.zh.md new file mode 100644 index 0000000000..1a330d3b6b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-18-chat-navigation-performance.zh.md @@ -0,0 +1,59 @@ +# Agent Note: 限制轮次导航工作量并聚合历史跳转 + +Status: implemented + +[English](2026-09-18-chat-navigation-performance.md) | 中文 + +## Problem + +长会话会放大三类独立成本:轮次轨道为每个已知轮次挂载一个 DOM 刻度,正文导航重新查找已知落点,以及历史跳转逐页发布数据。每次中间前插都会重建不断增大的对话索引,并在目标轮次可用前再次更新正文。把这些职责放进一个控制器,会使需要 DOM 几何的操作与只负责阅读、分页策略的操作难以区分。 + +## Decision + +### 固定间距虚拟轨道 + +[TurnNavigator](../../../../packages/client/ui-chat/src/client/chat/TurnNavigator.tsx) 使用 TanStack React Virtual,间距固定为 10px。只保留可见范围、预加载项,以及键盘焦点刻度及其相邻项。库负责尺寸容器和绝对定位刻度的变换。全部轮次元数据仍驻留内存;这里虚拟化的是轨道,不是正文。 + +轨道采用 ResizeObserver 提供的视口尺寸和固定条目高度。偏移计算根据缓存几何限制范围,再调用库支持的 `scrollBy`,避开 `scrollToIndex` 使用的基于 DOM 的最大偏移计算。正文恢复阅读状态后,轨道在首次获得可用尺寸时直接定位到最新已提交的活跃轮次。ref 分别提供激活轮次和仅滚动轨道的操作。原生指针目标替代坐标命中测试,刻度长度通过变换改变。布局包含作用于悬浮轨道外框,但不承诺浏览器几何读取只刷新该子树。 + +### 两层正文导航 + +[ChatViewport](../../../../packages/client/ui-chat/src/client/chat/use-chat-viewport.ts) 负责 DOM 读取、限制范围后的写入、原生事件和轮次锚点。它返回实际落点及可用的轮次、语义位置信息。阅读策略直接消费这些信息,无需再次命中测试;视口会忽略自身位置未变的滚动回声,以及内嵌轨道发出的事件。 + +[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) 仍有独立必要性:期间发生的布局增长不能抵消读者的小幅操作。 + +### 每次历史跳转统一发布 + +[Session.loadThrough](../../../../packages/api/session-controller/src/client/sessions/session.ts) 在共享目标被覆盖或加载结束前,保留已接受的页面和私有分页游标。它只反转一次页面列表并展开一次,然后发布一次有序前插。实时事件继续走正常发布路径。后续页面失败时,已成功取得的部分统一发布一次;流或窗口被替换时丢弃失效的暂存页面。单页 `loadOlder` 仍立即发布,重复跳转请求保留既有的最低目标策略。 + +### 理论工作量模型 + +设 `T` 为已知轮次数,`V` 为已挂载虚拟刻度数,`P` 为获取页数,`B` 为每页条目数,`W0` 为原先已加载条目数。以下估算工作量,不代表耗时测量。 + +| 操作 | 原有工作量 | 当前工作量 | +|---|---|---| +| 轨道 DOM 与 React 刻度遍历 | `O(T)` | `O(V)`;轮次元数据与初始索引、缓存构建仍为 `O(T)` | +| 条目未变时查找活跃轮次索引 | 线性搜索,`O(T)` | Map 查找,期望 `O(1)` | +| 轨道目标偏移计算 | 同步读取 DOM 总高度、视口尺寸 | 固定算术与缓存尺寸;此路径不读取 DOM 滚动总高度 | +| 已知正文轮次跳转 | 定位后重新查找位置 | 复用落点;不做命中测试,但既有锚点选择器与轮次查找成本仍在 | +| 历史驱动的线性索引遍历 | `O(P * W0 + B * P²)` | 最终一次遍历为 `O(W0 + B * P)`,另有 `O(B * P)` 暂存展开 | +| 历史驱动的排序 | 对每个不断增大的窗口重复排序 | 对最终窗口排序一次,`O((W0 + B * P) log(W0 + B * P))` | +| 历史前插发布次数 | `P` | 一次;实时事件与加载状态更新仍独立发生 | + +例如,10,000 个轮次只挂载约 50 个刻度,意味着轨道刻度约减少 99.5%,不代表端到端加速 200 倍。若获取 20 个等大页面且初始窗口可忽略,每页到达后做一次全量线性遍历,累计访问 `210B` 个条目;最终只遍历一次访问 `20B`,该遍历工作量约减少到原来的 1/10.5。分页请求仍为 20 次,最终正文仍需装配、渲染和布局。 + +## Alternatives considered + +**只加布局包含或 React 批处理。** 布局包含不会删除重复的 JavaScript 工作。仅抑制每页发布后的 React 渲染,也会保留上游对话装配与索引重建。 + +**修改虚拟化库。** 现有回调和相对滚动操作已能完成固定高度定位,无需维护依赖分支。原生滚动容器仍需总高度;删除总高度会使原生滚动失效,而不是实现虚拟化。 + +**单一大滚动控制器。** 它能集中状态,却把 DOM 操作与跟随、分页策略耦合。拆分负责对象后,业务变更无需复制几何操作。 + +**虚拟化整个正文。** 这会引入本次范围之外的 renderer 生命周期与语义锚点约束。聚合目标历史跳转即可删除中间发布,无需改变正文驻留方式。 + +## Consequences + +跳转远处轮次时,当前正文会保持到整批数据落地,不再逐页展示中间结果。暂存页面增加与新获取条目数成正比的引用。正文几何读取、语义锚点查询、普通原生滚动指标、最终布局,以及侧栏动画改变中间区域宽度时的重排仍然存在;本次不声称消除了这些成本。变换与颜色变化仍可能重绘,赋值 `scrollTop` 仍可能产生原生滚动事件。 + +[轨道测试](../../../../packages/client/ui-chat/tests/turn-navigator.client.spec.tsx) 覆盖有界挂载、首次定位、固定尺寸控制与焦点保留。[Chat 测试](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx) 覆盖语义恢复、跟随归因、自身滚动回声与不做命中测试的导航。[Session 测试](../../../../packages/api/session-controller/tests/session.client.spec.ts) 覆盖实时事件并发下的原子前插发布、失败与替换。[浏览器导航](../../../../apps/web/tests/chat-scroll-contract.e2e.ts) 覆盖真实滚动与未加载轮次落点。功能证据与上述理论估算相互独立。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.i18n.yaml deleted file mode 100644 index d7871098ba..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.i18n.yaml +++ /dev/null @@ -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/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.md -2026-09-03-resume-headers-do-not-repeat-system-prompts.md: de5e509c9e57af35bc36c24004457f02b63fd99d -2026-09-03-resume-headers-do-not-repeat-system-prompts.zh.md: 008e25d8d507aac8fbaec7230bad433732f0efd0 diff --git a/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.md b/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.md deleted file mode 100644 index de5e509c9e..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.md +++ /dev/null @@ -1,31 +0,0 @@ -# Agent Note: Resume headers do not repeat system prompts - -Status: implemented - -English | [中文](2026-09-03-resume-headers-do-not-repeat-system-prompts.zh.md) - -## Problem - -Forking a Session copies the source history into the child. The child's first model request then records a `request/header` with reason `resume`, even when its system field is identical to the preceding copied header. Chat treated every resume header as a new display point, so continuing the fork showed a second `System prompt` row and suggested that the system prompt had been injected twice. The provider request still carried the system field once; the duplicate existed only in Chat presentation. - -## Decision - -The durable resume header records the request boundary needed for exact Session reconstruction. Chat compares that full header with the preceding loaded Request Prompt and displays a non-empty system prompt only for the initial request, an explicit message-series start, or a real system-field change. An unchanged resume does not create a visible repetition. - -A partial history window may begin with a non-initial header and lack the predecessor needed for comparison. Chat renders that system prompt conservatively. If prepend later supplies an identical predecessor, the existing request-prompt Node becomes hidden instead of being withdrawn; its key and page-lifetime anchor stay stable. A different system field remains visible. - -Trajectory exposes every request header and its classified changes. Chat presentation does not alter provider requests, Session events, or reconstruction. - -## Alternatives considered - -**Omit unchanged resume headers from the Session log.** Rejected: resume is a real request boundary, and removing it would make exact reconstruction depend on process history that the durable log does not contain. - -**Special-case only forked Sessions.** Rejected: an ordinary process resume has the same presentation semantics, and the request headers already contain the system fields needed for a direct comparison. - -**Keep the duplicate row as a lifecycle marker.** Rejected: `System prompt` describes model-visible request content, so using it to mark a loop restart incorrectly implies another prompt injection. Request lifecycle evidence remains available in Trajectory. - -## Consequences - -Continuing a fork or resuming a process with an unchanged system field leaves one visible `System prompt` row for the current message series. Explicit series starts and real system changes still repeat the row. A partial window can initially show a conservative row and hide it after older history loads, while retaining the same materialized Node. - -The unit regression covers initial, series, unchanged resume, system-change, and prepend cases. The Web recorded-session scenario contains an unchanged resume header and asserts that the settled Chat renders exactly one `System prompt` control. diff --git a/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.zh.md b/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.zh.md deleted file mode 100644 index 008e25d8d5..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-03-resume-headers-do-not-repeat-system-prompts.zh.md +++ /dev/null @@ -1,31 +0,0 @@ -# Agent Note: Resume header 不重复系统提示词 - -Status: implemented - -[English](2026-09-03-resume-headers-do-not-repeat-system-prompts.md) | 中文 - -## 问题 - -fork Session 会把源会话历史复制到子会话。即使 system 字段与前一条被复制的 header 完全相同,子会话的第一个模型请求仍会记录一条 reason 为 `resume` 的 `request/header`。Chat 把每条 resume header 都视作新的展示点,因此继续 fork 会显示第二行`系统提示词`,让人误以为系统提示词被注入了两次。提供方请求实际仍只携带一次 system 字段;重复仅存在于 Chat 展示中。 - -## 决策 - -持久化 resume header 记录精确重建 Session 所需的请求边界。Chat 会把完整 header 与前一条已加载 Request Prompt 比较,只在初始请求、显式消息序列起点或真实 system 字段变化时显示非空系统提示词。内容未变的 resume 不创建可见的重复行。 - -部分历史窗口可能以非初始 header 开头,因缺少前序 header 而无法比较。Chat 会保守渲染该系统提示词。如果 prepend 随后补入相同的前序 header,既有 request-prompt Node 会转为隐藏而不是被撤回;其 key 和页面生命周期内的 anchor 保持稳定。不同的 system 字段仍然可见。 - -Trajectory 会展示每一条请求 header 及其变化分类。Chat 展示不会改变提供方请求、Session event 或重建行为。 - -## 考虑过的替代方案 - -**从 Session log 省略未变化的 resume header。** 否决:resume 是真实的请求边界,移除后精确重建将依赖持久日志未记录的进程历史。 - -**只对 fork Session 做特殊处理。** 否决:普通进程恢复具有相同的展示语义,请求 header 已经包含可直接比较的 system 字段。 - -**把重复行保留为生命周期标记。** 否决:`系统提示词`描述模型可见的请求内容,用它标记 loop 重启会错误暗示再次注入提示词。请求生命周期证据仍可在 Trajectory 中查看。 - -## 后果 - -继续 fork 或在 system 字段未变时恢复进程,当前消息序列只保留一行可见的`系统提示词`。显式序列起点与真实 system 变化仍会重复该行。部分窗口起初可以显示保守行,并在更早历史加载后将其隐藏,同时保留同一个已物化 Node。 - -单元回归覆盖初始请求、显式序列、未变化 resume、system 变化与 prepend 场景。Web 录制会话场景包含一条未变化的 resume header,并断言稳定后的 Chat 只渲染一个`系统提示词`控件。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.md b/.agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.md deleted file mode 100644 index f3b428dbe0..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.md +++ /dev/null @@ -1,23 +0,0 @@ -# Agent Note: Settle pinned scroll deliveries before layout changes - -Status: implemented - -English | [中文](2026-09-07-pinned-scroll-delivery-before-layout.zh.md) - -## Problem - -A delayed scroll sample compares positions from different layouts. While Chat is pinned, a composer or transcript shrink can move the browser floor; subsequent growth can move the browser position again before `scrollend` or the sampling timer. Deferring follow during that interval leaves the observed-top ledger stale and can classify browser layout movement as reader input, disabling follow without a reader gesture. - -## Decision - -[ChatView](../../../../packages/client/ui-chat/src/client/chat/ChatView.tsx) uses the existing observed-top comparison to sample non-reader pinned scroll deliveries synchronously through the same sample operation that clears pending work. This releases layout follow before further growth. Genuine reader movement remains pending until the existing interval or `scrollend`, even within the follow threshold: growth must not erase small gestures before they accumulate into a scroll-away. Immediate pinned samples use scroll metrics, not semantic-row geometry. - -## Alternatives considered - -**Defer every delivery.** Coalescing reduces geometry work while reading history, but a pinned browser position and its floor must be attributed in the same layout. A longer timeout or retry cannot recover ownership once the stale comparison disarms it. - -**Sample every delivery synchronously.** This restores attribution but also repeats semantic-anchor and reading-line measurements throughout an away-reader scroll burst. Only pinned ownership needs the immediate path. - -## Consequences - -Pinned deliveries incur immediate scroll-metric reads. History reading retains its bounded sampling cadence, and explicit return-to-bottom deliveries clear any pending away sample. [Focused tests](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx) cover shrink/regrowth before scrollend, observer growth without row measurements, repinning with a pending sample, timer and scrollend sampling, and unmount cancellation. The [keyless browser scenario](../../../../apps/web/tests/chat-scroll-contract.e2e.ts) covers pinned Send, real scroll-away input, streaming, and tool disclosure across the long transcript. diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.zh.md b/.agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.zh.md deleted file mode 100644 index 9b200f4553..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.zh.md +++ /dev/null @@ -1,23 +0,0 @@ -# Agent Note: 在布局变化前处理贴底滚动事件 - -Status: implemented - -[English](2026-09-07-pinned-scroll-delivery-before-layout.md) | 中文 - -## Problem - -延迟的滚动采样会比较来自不同布局的位置。Chat 贴底时,输入框或 transcript(文本记录)收缩可能改变浏览器底部位置;随后的增长又可能在 `scrollend` 或采样定时器触发前改变浏览器位置。在此期间推迟跟随会使已观察顶部位置记录过期,把浏览器布局移动误判为读者输入,在没有读者操作时关闭跟随。 - -## Decision - -[ChatView](../../../../packages/client/ui-chat/src/client/chat/ChatView.tsx) 使用现有的已观察顶部位置比较,通过同一个清除待处理工作的采样操作,同步采样非读者引起的贴底滚动事件。这会在后续增长前恢复布局跟随。真实读者移动即使位于跟随阈值内,也保持待处理直到现有周期或 `scrollend`:增长不能在小幅操作累积为离底滚动前将其抵消。立即执行的贴底采样只读取滚动指标,不读取语义行几何。 - -## Alternatives considered - -**延迟所有事件。** 合并采样减少阅读历史时的几何计算,但贴底浏览器位置及其底部必须在同一布局中完成归因。过期比较关闭跟随后,延长超时或重试都无法恢复归属。 - -**同步采样所有事件。** 这能恢复归因,却也会在离底读者连续滚动时重复测量语义锚点和阅读线。只有贴底归属需要立即处理。 - -## Consequences - -贴底事件会立即读取滚动指标。历史阅读保留有界采样节奏,显式回到底部的滚动事件会清除任何待处理的离底采样。[聚焦测试](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx) 覆盖 scrollend 前的收缩与增长、无需行测量的观察器增长、存在待处理采样时重新贴底、定时器与 scrollend 采样,以及卸载取消。[无密钥浏览器场景](../../../../apps/web/tests/chat-scroll-contract.e2e.ts) 覆盖长 transcript 中贴底发送、真实离底输入、流式输出与工具详情展开。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.i18n.yaml deleted file mode 100644 index f6a8425e96..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.i18n.yaml +++ /dev/null @@ -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/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.md -2026-09-08-stable-room-reading-under-hidden-split-controls.md: c89c5328ee896e23ed24454c50223193c2d587ba -2026-09-08-stable-room-reading-under-hidden-split-controls.zh.md: edf9948e547b79b33165b9c7168238e6cf1d2685 diff --git a/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.md b/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.md deleted file mode 100644 index c89c5328ee..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.md +++ /dev/null @@ -1,25 +0,0 @@ -# Agent Note: Keep the room reading independent of hidden split controls - -Status: implemented - -English | [中文](2026-09-08-stable-room-reading-under-hidden-split-controls.zh.md) - -## Problem - -The dockkit room rule measures each pane's tab strip after every commit to decide whether an equal split leaves two working halves. With `hideSplitWhenBlocked`, a width-blocked pane unmounts its split control — and the unmount changes the very strip the rule measured: the strip sheds the control's 28px box plus its 4px gap, the fixed part shrinks, and the same pane reads as fitting again. Remounting the control reverses the reading. Across a roughly 32px band of pane widths the two states alternate inside nested layout effects until React stops the update loop (error #185); the slot runtime catches the crash and unmounts the Sidebar's entry while the column still records itself expanded, so neither the panel nor the header's collapsed-only expand button renders. A grip drag on a squeezed viewport sweeps the panel through that band, which presented as the whole sidebar vanishing with no way back in. - -## Decision - -When the embedder hides blocked split controls, the room rule leaves the split control's footprint out of the strip's fixed part unconditionally, so the reading is the same whether the control is currently mounted or not. [`measurePaneFits`](../../../../packages/client/ui-dockkit/src/components/measure.ts) takes the embedder's `hideSplitWhenBlocked` choice, measures the rendered control's box plus the strip's column gap (`splitControlFootprint`), and passes it as [`PaneMeasure.splitControlWidth`](../../../../packages/client/ui-dockkit/src/engine/geometry.ts), which `halvesFit` subtracts from the fixed part. Excluding the footprint is also correct on its own terms: a half too narrow to split would hide its own control, so the footprint is not part of what a half must carry. Embedders that render blocked controls disabled pass nothing and keep the control in the fixed part, as before. - -## Alternatives considered - -**Hide only budget-blocked controls, render width-blocked ones disabled.** This is what the code did before `hideSplitAtCapacity` widened into `hideSplitWhenBlocked`: the budget is state-driven and cannot feed back through the measurement. It avoids the loop but forfeits the Sidebar's requested presentation — no disabled split control on panes that cannot split. - -**Debounce or freeze re-measurement during oscillation.** Damping hides the instability instead of removing it: the reading would still depend on the control's visibility, settle on an arbitrary one of the two states, and flip on the next resize. - -**Measure the control's footprint from a constant.** A hardcoded 32px drifts from the stylesheet; measuring the rendered control and the strip's real `column-gap` keeps the subtraction equal to what the strip actually sheds, which is the exact condition for a stable reading. - -## Consequences - -The room reading is a fixed point under control visibility, so `hideSplitWhenBlocked` embedders get hidden controls without feedback. Panes near the boundary now read as splittable slightly earlier than a disabled-control embedder would report, because the half being asked about would not carry the control. A [dockkit regression test](../../../../packages/client/ui-dockkit/tests/components.client.spec.tsx) emulates the strip shedding the control's footprint and fails with React's update-depth error on the unfixed code; a [Sidebar browser case](../../../../apps/web/tests/sidebar-right.e2e.ts) drags the panel grip past both clamps on a squeezed viewport and asserts the panel, its grip, and a clean console survive, because the crash surfaces only as a console error the scaffold tripwire does not watch. diff --git a/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.zh.md b/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.zh.md deleted file mode 100644 index edf9948e54..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-08-stable-room-reading-under-hidden-split-controls.zh.md +++ /dev/null @@ -1,25 +0,0 @@ -# Agent Note: Keep the room reading independent of hidden split controls - -Status: implemented - -[English](2026-09-08-stable-room-reading-under-hidden-split-controls.md) | 中文 - -## Problem - -dockkit 的空间规则在每次 commit 后测量各 pane 的标签条,判断等分后的两半是否仍可用。开启 `hideSplitWhenBlocked` 时,宽度不足的 pane 会卸载自己的分屏控件——而这次卸载恰恰改变了规则所测量的标签条:标签条少了控件的 28px 盒子加 4px 间距,固定部分随之变小,同一个 pane 又被读成"够宽"。控件重新挂载后读数再次反转。在约 32px 的 pane 宽度区间内,两种状态在嵌套 layout effect 中来回切换,直到 React 中止更新循环(错误 #185);slot 运行时捕获崩溃后卸载 Sidebar 的条目,而列状态仍记录为展开,于是面板和 header 上仅折叠时显示的展开按钮都不再渲染。在收窄的视口上拖动把手会让面板扫过该区间,表现为整个侧栏消失且无法再打开。 - -## Decision - -当嵌入方选择隐藏被阻止的分屏控件时,空间规则无条件将分屏控件的占位排除在标签条固定部分之外,使读数与控件当前是否挂载无关。[`measurePaneFits`](../../../../packages/client/ui-dockkit/src/components/measure.ts) 接收嵌入方的 `hideSplitWhenBlocked` 选择,测量已渲染控件的盒子加标签条的列间距(`splitControlFootprint`),并作为 [`PaneMeasure.splitControlWidth`](../../../../packages/client/ui-dockkit/src/engine/geometry.ts) 传入,由 `halvesFit` 从固定部分中减去。排除该占位本身也是正确的:窄到无法分屏的一半会隐藏自己的控件,所以这份占位并不属于一半必须承载的内容。将被阻止控件渲染为禁用态的嵌入方不传该值,控件照旧计入固定部分。 - -## Alternatives considered - -**只隐藏预算受限的控件,宽度受限的渲染为禁用态。** 这是 `hideSplitAtCapacity` 扩展为 `hideSplitWhenBlocked` 之前的做法:预算由状态驱动,不会经测量反馈回来。它避免了循环,但放弃了 Sidebar 想要的呈现——无法分屏的 pane 上不出现禁用的分屏控件。 - -**在振荡期间对重新测量做防抖或冻结。** 阻尼只是掩盖不稳定而非消除它:读数仍依赖控件的可见性,会任意停在两种状态之一,并在下次 resize 时再次翻转。 - -**用常量表示控件占位。** 硬编码的 32px 会与样式表漂移;测量实际渲染的控件和标签条真实的 `column-gap`,才能保证减去的量恰好等于标签条实际卸下的量,这正是读数稳定的确切条件。 - -## Consequences - -空间读数在控件可见性变化下是不动点,`hideSplitWhenBlocked` 的嵌入方获得隐藏控件的呈现且无反馈循环。临界宽度附近的 pane 会比禁用态嵌入方的报告稍早读成可分屏,因为被询问的那一半不会承载控件。[dockkit 回归测试](../../../../packages/client/ui-dockkit/tests/components.client.spec.tsx) 模拟标签条卸下控件占位的反馈,在未修复的代码上以 React 更新深度错误失败;[Sidebar 浏览器用例](../../../../apps/web/tests/sidebar-right.e2e.ts) 在收窄视口上把面板把手拖过两侧钳位,断言面板、把手与干净的 console 均存活——崩溃只以 console error 形式出现,而脚手架的 tripwire 不监听它。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.md b/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.md deleted file mode 100644 index c5d778f8fb..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.md +++ /dev/null @@ -1,21 +0,0 @@ -# Agent Note: Composer placeholder emptiness - -Status: implemented - -English | [中文](2026-09-09-composer-placeholder-whitespace.zh.md) - -## Problem - -Sharing the whitespace-trimmed submission check with placeholder rendering leaves guidance drawn over a draft containing spaces. - -## Decision - -The Composer hides its placeholder whenever the raw draft is nonempty. Submission keeps its trimmed-content check. Attachments and claimed commands retain their existing placeholder suppression. - -## Alternatives considered - -**Reuse the submission check.** Whitespace has no sendable message content, but it occupies the editor and moves its caret. A shared check conflates these two states. - -## Consequences - -All placeholder variants, including queued-message steering guidance, disappear after whitespace input and return after deletion. A whitespace-only draft without attachments remains unsendable. [Component tests](../../../../packages/client/ui-conversation/tests/input-bar.client.spec.tsx) cover visibility, composition, rerendering and submission; the [browser regression](../../../../apps/web/tests/composer-placeholder.e2e.ts) checks keyboard and clipboard gestures against built UI. diff --git a/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.zh.md b/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.zh.md deleted file mode 100644 index b5f82bcd86..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-09-composer-placeholder-whitespace.zh.md +++ /dev/null @@ -1,21 +0,0 @@ -# Agent Note: Composer 占位提示的判空规则 - -Status: implemented - -[English](2026-09-09-composer-placeholder-whitespace.md) | 中文 - -## Problem - -占位提示复用去除首尾空白后的提交判断,会让提示文字覆盖已经包含空格的草稿。 - -## Decision - -原始草稿非空时,Composer 隐藏占位提示。提交仍检查去除首尾空白后的内容。附件和已认领指令沿用现有的占位提示隐藏规则。 - -## Alternatives considered - -**复用提交判断。** 空白字符没有可发送的消息内容,但会占据编辑器并移动光标。共用判断会混淆这两种状态。 - -## Consequences - -所有占位提示,包括排队消息的插话提示,都会在输入空白字符后隐藏,删除后恢复。没有附件的纯空白草稿仍无法发送。[组件测试](../../../../packages/client/ui-conversation/tests/input-bar.client.spec.tsx) 覆盖显示、输入法组合、重新渲染和提交;[浏览器回归](../../../../apps/web/tests/composer-placeholder.e2e.ts) 使用构建后的界面检查键盘和剪贴板操作。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.i18n.yaml deleted file mode 100644 index 003ab9ba65..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.i18n.yaml +++ /dev/null @@ -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/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.md -2026-09-14-transcript-width-handle-layering.md: 01d70e646e874d8266633597f1b510ad73fc2287 -2026-09-14-transcript-width-handle-layering.zh.md: c6e03ec5775256f5aac87d2f7c1ed66a12cf0c2a diff --git a/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.md b/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.md deleted file mode 100644 index 01d70e646e..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.md +++ /dev/null @@ -1,33 +0,0 @@ -# Agent Note: Transcript width handles stay behind chat content - -Status: implemented - -English | [中文](2026-09-14-transcript-width-handle-layering.zh.md) - -## Problem - -The Conversation shell renders each content-width handle as a full-height absolute strip beside the reading column. A high stacking level placed that strip above wide Markdown tables that legitimately overflow the column. The handle glow covered their content and its pointer target could replace their interaction target. - -## Decision - -The Conversation shell keeps width handles at level zero without creating a body-wide stacking context. Chat raises concrete table elements to level one, while the reading column, column-bounded tool cards, and table breakout wrapper create no new stacking level. A table therefore owns the gutter only where its painted box actually reaches it; a fitting four-column table leaves the adjacent gutter draggable. Fixed message tooltips remain outside the table stacking context and can still paint over the sticky composer. - -This rule relies on the supported Chromium engine not turning ChatView's inline-size query container into an intermediate stacking context. The same engine behavior already lets the back-to-bottom control outrank the sticky composer. The Chromium hit-test scenario pins that assumption; raising the whole Chat root would instead make its transparent full-width box reclaim the gutter. - -A width handle starts resizing only for the primary pointer button. Ordinary wheel input over the handle is normalized from pixel, computed line-height, or page units and forwarded to the direct sibling transcript scrollport, so hovering the gutter does not suspend reading. Ctrl+wheel is not forwarded because it represents browser zoom or a trackpad pinch gesture. - -Each side's hit strip is at most 10px wide. Its hover indicator uses the lower-contrast resting scrollbar tint and is a 2px line with a 16px solid center and 28px fades, for a 72px total visible length. - -Composer chrome retains its higher layer, so its full sticky footer band intentionally does not start a width resize. A handle whose pointer capture began above that band temporarily rises to level eight, keeping the indicator visible until release. A full-view composer takeover continues to hide the handles entirely. - -## Alternatives considered - -**Move the handles farther from the reading column.** Wide tables can use the available transcript width, so a fixed larger inset would only reduce the overlap for some window sizes and would make the handles harder to reach. - -**Disable the handles whenever a transcript contains wide content.** One wide row would remove resizing from the entire Session, including empty gutter beside unrelated rows. - -**Inspect the elements under the pointer in JavaScript.** Dynamic hit testing would duplicate browser stacking and pointer dispatch rules and could still disagree with new plugin renderers. - -## Consequences - -Visible Chat content owns pointer input only where its painted element extends into a width-handle strip, while bare gutter keeps a quieter resize affordance, primary-button drag, and transcript wheel scrolling. Unit tests pin the declared handle geometry, pointer-button behavior, direct scrollport lookup, zoom exclusion, and every DOM delta mode. Chromium `elementFromPoint` coverage proves an overflowing table wins the hit while a fitting `md-table-wide` wrapper does not, and a message-action test proves fixed tooltips still paint above the sticky composer. diff --git a/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.zh.md b/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.zh.md deleted file mode 100644 index c6e03ec577..0000000000 --- a/.agents/notes/implemented/bug-fix/2026-09-14-transcript-width-handle-layering.zh.md +++ /dev/null @@ -1,33 +0,0 @@ -# Agent Note: 正文宽度拖拽条位于 Chat 内容下方 - -Status: implemented - -[English](2026-09-14-transcript-width-handle-layering.md) | 中文 - -## 问题 - -Conversation shell 将每个正文宽度拖拽条渲染为阅读列旁的全高绝对定位区域。较高的堆叠层级使该区域盖在会合理溢出阅读列的宽 Markdown 表格之上。拖拽条的高亮会遮住表格内容,其指针目标也可能取代表格自身的交互目标。 - -## 决策 - -Conversation shell 将宽度拖拽条保持在第零层,不创建覆盖整个 body 的堆叠上下文。Chat 只将具体的表格元素提到第一层,阅读列、限定在列内的工具卡片与表格 breakout 包装层都不创建新的堆叠层级。因此只有表格的可见盒子实际延伸进沟槽时才会占有指针;能容纳在正文中的四列表格不会阻断旁边的沟槽拖拽。消息的固定定位 Tooltip 保持在表格堆叠上下文之外,仍可绘制在粘滞 composer 上方。 - -该规则依赖交付的 Chromium 引擎不会把 ChatView 的 inline-size query container 变成中间堆叠上下文。back-to-bottom 控件能高于粘滞 composer 也已依赖相同的引擎行为。Chromium 命中测试固定这一假设;如果改为提高整个 Chat root,其透明全宽盒子反而会重新占用沟槽。 - -宽度拖拽条仅使用主指针按键开始调整。拖拽条上的普通滚轮输入会从像素、计算后的行高或页面单位规整为像素,再转发给直接相邻的 transcript scrollport,因此指针悬停在沟槽时不会中断阅读滚动。Ctrl+滚轮表示浏览器缩放或触控板捏合手势,不会被转发。 - -每侧的命中区域最宽为 10px。悬停指示线使用对比度更低的静止滚动条颜色,线宽 2px,中心实色段长 16px,两端各渐隐 28px,可见总长度为 72px。 - -Composer chrome 保留更高层级,因此完整的粘滞底部区带刻意不会开始宽度调整。如果拖拽条在进入该区带之前已获得指针捕获,它会临时提到第八层,使指示线在松开前保持可见。占据完整 View 的 composer takeover 仍会完全隐藏拖拽条。 - -## 考虑过的替代方案 - -**将拖拽条移到离阅读列更远的位置。** 宽表格可以使用 transcript 的可用宽度,因此固定增加间距只能减少部分窗口尺寸下的重叠,还会让拖拽条更难触达。 - -**只要 transcript 包含宽内容就禁用拖拽条。** 一行宽内容会让整个 Session 都无法调整宽度,包括与其他行相邻的空白沟槽。 - -**在 JavaScript 中检查指针下方的元素。** 动态命中测试会重复浏览器的堆叠与指针分发规则,并且仍可能与新的插件 renderer 不一致。 - -## 后果 - -可见 Chat 内容只在其可见元素实际延伸进宽度拖拽区域时拥有指针输入,裸露沟槽则保留更轻的宽度调整提示、主按键拖拽和 transcript 滚轮滚动。单元测试固定声明的拖拽条尺寸、指针按键行为、直接 scrollport 查找、缩放排除和每种 DOM delta mode。Chromium `elementFromPoint` 覆盖证明溢出表格获得指针命中,而可容纳的 `md-table-wide` 包装层不会;消息操作测试证明固定定位 Tooltip 仍绘制在粘滞 composer 上方。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.i18n.yaml index f0a89cb78d..6377a233cb 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.i18n.yaml @@ -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-09-17-thinking-markdown.md -2026-09-17-thinking-markdown.md: de49cb42ba537700f6584c9f4cef9a57fb2e3605 -2026-09-17-thinking-markdown.zh.md: e403630138f2f39aec0834e550d4d4a8860a67fe +2026-09-17-thinking-markdown.md: 6990fc6277d7d1c0a088a50cfb88694b2d9c55c7 +2026-09-17-thinking-markdown.zh.md: 963626088e50a44bcf76c7fc7ec3653e93fcd2c9 diff --git a/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.md b/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.md index de49cb42ba..6990fc6277 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.md +++ b/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.md @@ -10,9 +10,9 @@ Chat Thinking contains model-authored Markdown, but literal rendering exposes he ## Decision -[MarkdownText](../../../../packages/client/ui-primitives/src/markdown/MarkdownText.tsx) owns a compact presentation variant used by Chat Thinking and Trajectory thinking details. It keeps the secondary content font size, line height, and tertiary color throughout. Heading levels retain semantic elements but share the same 600 weight and size; paragraphs, lists, quotes, and code use compact spacing. Links keep a resting dotted underline with tertiary color as a secondary-content exception to the [shared link styling](../feature/2026-09-04-web-clickable-link-styles.md); code retains its monospace font and background. +[MarkdownText](../../../../packages/client/ui-primitives/src/markdown/MarkdownText.tsx) owns a compact presentation variant used by Chat Thinking and Trajectory thinking details. It keeps the secondary content font size, line height, and tertiary color throughout. Heading levels retain semantic elements but share the same 600 weight and size; paragraphs, lists, quotes, and code use compact spacing. Links keep a resting dotted underline with tertiary color as a secondary-content exception to the [shared link styling](../../../../docs/web-styling.md); code retains its monospace font and background. -Tables and formulas remain enabled through the existing parser. Their containers constrain horizontal overflow, and formula text inherits the secondary font size. Inline formulas retain native KaTeX baselines; enclosing text blocks own overflow so short formulas do not create scrollbars. Compact code banners stay in normal flow, keeping the [Thinking disclosure header](../feature/2026-08-03-web-sticky-collapsible-headers.md) above the scrolling content without a second sticky band. +Tables and formulas remain enabled through the existing parser. Their containers constrain horizontal overflow, and formula text inherits the secondary font size. Inline formulas retain native KaTeX baselines; enclosing text blocks own overflow so short formulas do not create scrollbars. Compact code banners stay in normal flow, keeping the [Thinking disclosure header](../../../../packages/client/ui-chat/src/client/chat/ReasoningRow.module.css) above the scrolling content without a second sticky band. Chat passes its running state to the existing incremental Markdown renderer. The collapsed summary remains a separate single-line text projection. The parser, frozen-block cache, stored reasoning, and Session format do not change. Trajectory pins Thinking to its inspector’s fixed 13px/20px tier, independent of Chat’s content-size setting. Its answer output retains its existing typography and its [Thinking disclosure behavior](../feature/2026-09-09-ptc-trajectory-code-inspection.md). diff --git a/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.zh.md b/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.zh.md index e403630138..963626088e 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-09-17-thinking-markdown.zh.md @@ -10,9 +10,9 @@ Chat Thinking 包含模型编写的 Markdown,但纯文本渲染直接显示标 ## 决策 -[MarkdownText](../../../../packages/client/ui-primitives/src/markdown/MarkdownText.tsx) 拥有紧凑展示变体,供 Chat Thinking 和 Trajectory 思考详情使用。内容沿用次级字号、行高和 tertiary 颜色。各级标题保留语义元素,但统一使用 600 字重和相同字号;段落、列表、引用和代码采用紧凑间距。链接保留默认点状下划线与 tertiary 颜色,作为[共享链接样式](../feature/2026-09-04-web-clickable-link-styles.zh.md)的次级内容例外;代码保留等宽字体和底色。 +[MarkdownText](../../../../packages/client/ui-primitives/src/markdown/MarkdownText.tsx) 拥有紧凑展示变体,供 Chat Thinking 和 Trajectory 思考详情使用。内容沿用次级字号、行高和 tertiary 颜色。各级标题保留语义元素,但统一使用 600 字重和相同字号;段落、列表、引用和代码采用紧凑间距。链接保留默认点状下划线与 tertiary 颜色,作为[共享链接样式](../../../../docs/web-styling.zh.md)的次级内容例外;代码保留等宽字体和底色。 -表格和公式继续通过既有解析器启用。其容器限制横向溢出,公式文本继承次级字号。行内公式保留 KaTeX 原生基线,由外层文本块承担溢出处理,短公式不会产生滚动条。紧凑代码栏保持正常文档流,让 [Thinking 折叠标题](../feature/2026-08-03-web-sticky-collapsible-headers.zh.md) 位于滚动内容上方,无需第二条 sticky 栏。 +表格和公式继续通过既有解析器启用。其容器限制横向溢出,公式文本继承次级字号。行内公式保留 KaTeX 原生基线,由外层文本块承担溢出处理,短公式不会产生滚动条。紧凑代码栏保持正常文档流,让 [Thinking 折叠标题](../../../../packages/client/ui-chat/src/client/chat/ReasoningRow.module.css) 位于滚动内容上方,无需第二条 sticky 栏。 Chat 将运行状态传给既有增量 Markdown 渲染器。折叠摘要仍是独立的单行文本投影。解析器、冻结块缓存、存储的推理内容和 Session 格式均保持不变。Trajectory 将 Thinking 固定在检查器的 13px/20px 层级,与 Chat 的内容字号设置无关。其回答输出保留既有排版及其 [Thinking 折叠行为](../feature/2026-09-09-ptc-trajectory-code-inspection.zh.md)。 diff --git a/.agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.i18n.yaml similarity index 54% rename from .agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.i18n.yaml rename to .agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.i18n.yaml index 5df3dbdf86..0898823b35 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-07-pinned-scroll-delivery-before-layout.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.i18n.yaml @@ -1,6 +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-07-pinned-scroll-delivery-before-layout.md -2026-09-07-pinned-scroll-delivery-before-layout.md: f3b428dbe0d2fdc7cbaedf0383d2ab80ea1bfd45 -2026-09-07-pinned-scroll-delivery-before-layout.zh.md: 9b200f45535c67cfbbb75feaf3025c5a3b5c1fa2 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.md +2026-09-18-messages-input-history-compatibility.md: 1775276deec6180f1fd186b11cb7a757a3328a54 +2026-09-18-messages-input-history-compatibility.zh.md: 50367e33c29df0679a44aca1fe680d2bedc8c263 diff --git a/.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.md b/.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.md new file mode 100644 index 0000000000..1775276dee --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.md @@ -0,0 +1,31 @@ +# Agent Note: Replay assistant blocks in Messages user input + +Status: implemented + +English | [中文](2026-09-18-messages-input-history-compatibility.zh.md) + +## Problem + +Saved subagent settlement notices can contain child reasoning and tool calls in parent user messages. Chat Completions and pi-ai omit these blocks from user input, while rejecting them in Messages prevents every later request containing the same history. The [text-only settlement producer](../feature/2026-08-06-manager-owned-subagent-settlement-delivery.md) prevents new notices from carrying these blocks but cannot repair already recorded messages. + +## Decision + +The [Messages serializer](../../../../packages/llm/llm-deepseek/src/protocols/messages/serialize.ts) omits `reasoning` and `tool-call` blocks from user and tool-result input. This matches Chat Completions and pi-ai for these two block types, without changing Session records or converting child reasoning into parent user text. The [provider README](../../../../packages/llm/llm-deepseek/README.md#model-experience) owns the input rules, including empty user messages, empty tool results, and rejection of other unsupported blocks. + +This partially supersedes the settlement decision's rejection of serializer tolerance and the [Messages adapter decision](../feature/2026-09-07-deepseek-messages-adapter.md)'s input rejection. Notice construction still projects nonempty child text for every parent provider. Canonical child output and ordinary assistant reasoning and tool calls remain available to their existing consumers. The omission is based on input role and block type, not the notice source or creation date, so it also applies to newly supplied user and tool-result content. + +## Alternatives considered + +**Fix only the producer.** New notices are representable, but saved notices still block protocol switching and continuation. + +**Rewrite Session history or flatten reasoning into user text.** Rewriting removes original evidence; flattening sends child reasoning as parent input. Request-only omission preserves the durable record and existing input semantics. + +**Ignore every unsupported block.** Unknown plugin content has no Messages representation defined here. Compatibility for two known assistant block types does not establish one. + +**Move a system update to an earlier retained user turn.** When the corresponding user input is omitted, moving the update before an earlier assistant changes its position in the conversation and rewrites a previously sent prefix. The [documented in-history limitation](../../../../packages/llm/llm-deepseek/README.md#known-limitations-and-deferred-work) remains explicit rather than inventing a user message or relocating the update. + +## Consequences + +Saved notices with retained text can continue through Messages. The provider no longer diagnoses these two block types as user-input producer errors; the settlement producer remains responsible for text-only notices. This does not promise lossless provider input or compatibility for every historical message sequence. + +Verification covers user and tool-result filtering, unchanged durable content, HTTP continuation, and a [headless Session replay](../../../../snapshots/session/deepseek-messages-input-history/snapshot.yml). Serialization tests pin rejection of an in-history update when its user input is entirely omitted, both before another assistant and at request end, and acceptance when text or a tool result retains the corresponding turn. diff --git a/.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.zh.md b/.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.zh.md new file mode 100644 index 0000000000..50367e33c2 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 回放 Messages 用户输入中的助手块 + +Status: implemented + +[English](2026-09-18-messages-input-history-compatibility.md) | 中文 + +## 问题 + +已保存的子 Agent 结算通知可能在父级用户消息中包含子级推理和工具调用。Chat Completions 与 pi-ai 会在用户输入中省略这些块,而 Messages 拒绝它们会阻断包含相同历史的所有后续请求。[仅生成文本的结算通知生产方](../feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)可以避免新通知携带这些块,但无法修复已经记录的消息。 + +## 决策 + +[Messages 序列化器](../../../../packages/llm/llm-deepseek/src/protocols/messages/serialize.ts)省略用户与工具结果输入中的 `reasoning` 和 `tool-call` 块。这两类块的处理与 Chat Completions、pi-ai 一致,不改变 Session 记录,也不把子级推理转换成父级用户文本。[提供方 README](../../../../packages/llm/llm-deepseek/README.zh.md#model-experience)统一说明输入规则,包括空用户消息、空工具结果,以及对其他不支持块的拒绝。 + +这部分取代了结算决策对序列化器容错的拒绝,以及 [Messages 适配器决策](../feature/2026-09-07-deepseek-messages-adapter.zh.md)中的输入拒绝规则。通知构建仍为每种父级提供方投影子级的非空文本。子级的规范输出以及普通助手推理和工具调用仍可供现有消费者使用。省略依据是输入角色和块类型,而不是通知来源或创建日期,因此也适用于新提供的用户和工具结果内容。 + +## 考虑过的替代方案 + +**只修复生产方。** 新通知可以表示,但已保存的通知仍会阻断协议切换和会话续接。 + +**改写 Session 历史或把推理压平为用户文本。** 改写会移除原始证据;压平会把子级推理作为父级输入发送。只在请求中省略可以保留持久化记录和现有输入语义。 + +**忽略所有不支持的块。** 此处没有定义未知插件内容的 Messages 表示方式。兼容两种已知助手块类型并不意味着其他类型也有对应表示。 + +**把 system 更新移到更早保留的用户轮次。** 对应用户输入被省略时,将更新移到更早的 assistant 之前,会改变它在对话中的位置,并改写已发送的前缀。保留明确记录的[历史内更新限制](../../../../packages/llm/llm-deepseek/README.zh.md#known-limitations-and-deferred-work),不编造用户消息或移动更新位置。 + +## 后果 + +保留文本的已保存通知可以通过 Messages 继续会话。提供方不再将这两种块类型诊断为用户输入生产方错误;结算通知生产方仍负责仅生成文本通知。这不保证提供方输入无损,也不保证兼容所有历史消息序列。 + +验证覆盖用户与工具结果过滤、持久化内容不变、HTTP 续接,以及 [headless Session 回放](../../../../snapshots/session/deepseek-messages-input-history/snapshot.yml)。序列化测试固定了历史内更新在对应用户输入被完全省略时的拒绝行为,覆盖下一个 assistant 之前和请求结束两种位置;文本或工具结果保留对应轮次时则可以通过。 diff --git a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml index 1ef5c7c433..e678639894 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml @@ -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-27-web-subagent-conversations.md -2026-07-27-web-subagent-conversations.md: fd2ff912a83f2535e33472e2b5755b9d059ba674 -2026-07-27-web-subagent-conversations.zh.md: 0124ce4143d65fe79dc0f5c66a42058984f5182e +2026-07-27-web-subagent-conversations.md: aacb52eca5680534b74abd42ca07c2b568c23e21 +2026-07-27-web-subagent-conversations.zh.md: 30c41f03703004e3175cee8fad9d11e0cf93f06a diff --git a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md index fd2ff912a8..aacb52eca5 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md +++ b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md @@ -39,7 +39,7 @@ The Figma [subagent list](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Ha ## Product contract -The descendant-count control is absent only when a complete empty direct-catalog response agrees with the session-summary projection that no subagent descendants are known. On an ordinary session, a slash separates the current title from that count control. Every subagent breadcrumb combines its compact 12px title with a fixed double chevron: the current breadcrumb uses primary color and weight 500, while ancestors use tertiary color and weight 400. Hovering a combined control for 150ms opens its direct-parent catalog, with a short crossing grace for the portaled menu; ArrowDown remains the keyboard entry path. Clicking an ancestor cancels pending hover, closes an open catalog, and only navigates upward. Each direct-parent catalog enables sibling switching and bolds its selected row; its label overrides the session-summary title, and every missing switcher catalog refreshes automatically. Only the current subagent appends its own descendant-count control. Long titles truncate before the fixed chevron. The count trigger counts every known session-summary descendant reached through an uninterrupted `origin: 'subagent'` lineage, stops at ordinary forks, and shows ongoing activity when any counted descendant is running. Because ordinary sidebar rows hide subagent-origin sessions, the Workspace browser indexes the same uninterrupted lineage onto each visible ordinary row: any running descendant supplies its blue activity indicator and exact count in hover and assistive text without describing an idle parent as running. An ordinary fork starts a separate aggregation subtree. Pending interaction outranks parent running; either remains primary while descendant activity becomes a second hover and assistive status. With neither present, descendant activity outranks an unviewed completion reminder, which returns after the last running descendant stops. Every healthy direct-catalog row carries a read-time `hasChildren` hint derived only from direct lineage headers with durable `origin: 'subagent'`; normal healthy and diagnostic subagent candidates carry that marker, while ordinary forks do not. This lookahead reads no descendant event log, and the descriptor-backed catalog loaded after disclosure remains authoritative. When summaries establish descendants before that catalog exists or after a stale empty response, the action stays visible and exposes only disabled loading rows until opening it refreshes the catalog; summary-only rows never grant navigation. The UI omits disclosure for a known leaf before interaction; the hint does not promise that the child will remain a leaf. While an expanded direct catalog is loading, known lineage reserves one disabled loading row per direct descendant without recursively fetching descendant catalogs. The tree then presents continuable and one-shot rows, falling back to the session id when an optional one-shot label is absent. Corrupt, unsupported, and unavailable candidates remain visible as disabled diagnostic rows. +The descendant-count control is absent only when a complete empty direct-catalog response agrees with the session-summary projection that no subagent descendants are known. On an ordinary session, a slash separates the current title from that count control. Every subagent breadcrumb combines its compact 12px title with a fixed double chevron: the current breadcrumb uses primary color and weight 500, while ancestors use tertiary color and weight 400. Hovering a combined control for 150ms opens its direct-parent catalog, with a short crossing grace for the portaled menu; ArrowDown remains the keyboard entry path. Clicking an ancestor cancels pending hover, closes an open catalog, and only navigates upward. Each direct-parent catalog enables sibling switching and bolds its selected row; its label overrides the session-summary title, and every missing switcher catalog refreshes automatically. Only the current subagent appends its own descendant-count control. Long titles truncate before the fixed chevron. The count trigger counts every known session-summary descendant reached through an uninterrupted `origin: 'subagent'` lineage, stops at ordinary forks, and shows ongoing activity when any counted descendant is running. Because ordinary sidebar rows hide subagent-origin sessions, the Workspace browser indexes the same uninterrupted lineage onto each visible ordinary row: any running descendant supplies the shared tertiary-grey ongoing loader and exact count in hover and assistive text without describing an idle parent as running. An ordinary fork starts a separate aggregation subtree. Pending interaction outranks parent running; either remains primary while descendant activity becomes a second hover and assistive status. An ordinary Workspace row with a pending approval, plan review, or question replaces its relative time with the compact label `Approval`, `Plan review`, or `Answer`; hover and assistive details retain the full status and relative time. With neither primary state present, descendant activity outranks an unviewed completion reminder, which returns after the last running descendant stops. Every healthy direct-catalog row carries a read-time `hasChildren` hint derived only from direct lineage headers with durable `origin: 'subagent'`; normal healthy and diagnostic subagent candidates carry that marker, while ordinary forks do not. This lookahead reads no descendant event log, and the descriptor-backed catalog loaded after disclosure remains authoritative. When summaries establish descendants before that catalog exists or after a stale empty response, the action stays visible and exposes only disabled loading rows until opening it refreshes the catalog; summary-only rows never grant navigation. The UI omits disclosure for a known leaf before interaction; the hint does not promise that the child will remain a leaf. While an expanded direct catalog is loading, known lineage reserves one disabled loading row per direct descendant without recursively fetching descendant catalogs. The tree then presents continuable and one-shot rows, falling back to the session id when an optional one-shot label is absent. Corrupt, unsupported, and unavailable candidates remain visible as disabled diagnostic rows. `running` means the exact child Agent driver is draining work at the Host sampling boundary; `inactive` means that driver is idle or absent. The UI does not translate either value into success, failure, cancellation, completeness, or resumability. `subagent.list` supplies the current driver-status baseline, `host/session-status` updates known activity in place, request-local replay prevents an older in-flight list response from overwriting a newer transition, and `host/session-removed` returns a known row to `inactive`; reconnect reads a fresh baseline. A `host/session-added` frame for a direct subagent immediately flips any loaded parent row to `hasChildren: true`, and that positive hint survives an older in-flight catalog response; membership, labels, mode, diagnostics, and the authoritative snapshot still require a debounced `subagent.list` refresh while the affected branch is open. A prompt response remains delivery-time authority. diff --git a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md index 0124ce4143..30c41f0370 100644 --- a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md +++ b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md @@ -39,7 +39,7 @@ Figma 中的 [subagent 列表](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5 ## 产品约定 -只有当完整的直接目录空响应与会话摘要投影相符,二者均表明没有已知的 subagent 后代时,才不显示后代数量控件。普通会话会用斜杠分隔当前 title 和该数量控件。每一级 subagent 面包屑都将紧凑的 12px title 与固定显示的双向箭头组合成一个控件:当前面包屑使用主标签颜色和 500 字重,祖先则使用三级标签颜色和 400 字重。悬停组合控件 150ms 后会打开其直接 parent 目录;浮层菜单具有短暂的跨越宽限时间,ArrowDown 保留为键盘入口。点击祖先会取消待触发的悬停、关闭已打开的目录,并且只向上导航。每个直接 parent 目录都可切换 sibling,并会加粗其选中行;目录 label 优先于会话摘要 title,缺失的每个切换器目录都会自动刷新。只有当前 subagent 会追加自己的后代数量控件。过长的 title 会在固定箭头之前截断。数量触发器会统计经不间断的 `origin: 'subagent'` 谱系可达的每个已知会话摘要后代,在普通 fork 处停止,并在任一计入统计的后代处于 `running` 时显示活动仍在进行。由于普通侧边栏行会隐藏 origin 为 subagent 的会话,Workspace 浏览器会在每个可见的普通行上索引同一条不间断谱系:任何运行中的后代都会让该行显示蓝色活动指示器,并在悬停与无障碍文本中给出确切数量,同时不会把空闲 parent 描述为正在运行。普通 fork 会开启单独的聚合子树。待处理交互优先于 parent 的运行中状态;二者无论哪一项存在都会保持为主要状态,而后代活动则成为悬停与无障碍状态中的第二项。两者均不存在时,后代活动优先于未查看的完成提醒;最后一个运行中的后代停止后,该提醒会恢复。每个健康的直接目录行都携带读取时的 `hasChildren` 提示,该值只根据持久化 `origin: 'subagent'` 的直接谱系 header 派生;正常的健康与 diagnostic subagent 候选都会携带该标记,而普通 fork 不会。该预查不读取任何后代事件日志,展开后仍以描述符支撑的目录为权威依据。当摘要在该目录尚不存在时或在一次陈旧的空响应后确认已有后代时,该操作会保持可见,并且在打开它以刷新目录之前仅显示禁用的加载行;仅由摘要支撑的行绝不会提供导航能力。UI 会在交互前就省略已知叶子节点的展开控件;该提示不承诺 child 会一直是叶子。已展开的直接目录加载期间,已知谱系会为每个直接后代预留一行禁用的加载行,而不会递归获取后代目录。随后树会呈现可继续与 one-shot 行;one-shot 的可选 label 缺失时,回退到其会话 id。损坏、不受支持或不可用的候选仍以禁用的 diagnostic 行显示。 +只有当完整的直接目录空响应与会话摘要投影相符,二者均表明没有已知的 subagent 后代时,才不显示后代数量控件。普通会话会用斜杠分隔当前 title 和该数量控件。每一级 subagent 面包屑都将紧凑的 12px title 与固定显示的双向箭头组合成一个控件:当前面包屑使用主标签颜色和 500 字重,祖先则使用三级标签颜色和 400 字重。悬停组合控件 150ms 后会打开其直接 parent 目录;浮层菜单具有短暂的跨越宽限时间,ArrowDown 保留为键盘入口。点击祖先会取消待触发的悬停、关闭已打开的目录,并且只向上导航。每个直接 parent 目录都可切换 sibling,并会加粗其选中行;目录 label 优先于会话摘要 title,缺失的每个切换器目录都会自动刷新。只有当前 subagent 会追加自己的后代数量控件。过长的 title 会在固定箭头之前截断。数量触发器会统计经不间断的 `origin: 'subagent'` 谱系可达的每个已知会话摘要后代,在普通 fork 处停止,并在任一计入统计的后代处于 `running` 时显示活动仍在进行。由于普通侧边栏行会隐藏 origin 为 subagent 的会话,Workspace 浏览器会在每个可见的普通行上索引同一条不间断谱系:任何运行中的后代都会让该行显示共享的三级灰色 ongoing loading,并在悬停与无障碍文本中给出确切数量,同时不会把空闲 parent 描述为正在运行。普通 fork 会开启单独的聚合子树。待处理交互优先于 parent 的运行中状态;二者无论哪一项存在都会保持为主要状态,而后代活动则成为悬停与无障碍状态中的第二项。普通 Workspace 行存在待处理的审批、计划审阅或问题时,会用紧凑的「待批准」「计划待审」或「待回答」替换相对时间;悬停与无障碍详情仍保留完整状态和相对时间。两者均不存在时,后代活动优先于未查看的完成提醒;最后一个运行中的后代停止后,该提醒会恢复。每个健康的直接目录行都携带读取时的 `hasChildren` 提示,该值只根据持久化 `origin: 'subagent'` 的直接谱系 header 派生;正常的健康与 diagnostic subagent 候选都会携带该标记,而普通 fork 不会。该预查不读取任何后代事件日志,展开后仍以描述符支撑的目录为权威依据。当摘要在该目录尚不存在时或在一次陈旧的空响应后确认已有后代时,该操作会保持可见,并且在打开它以刷新目录之前仅显示禁用的加载行;仅由摘要支撑的行绝不会提供导航能力。UI 会在交互前就省略已知叶子节点的展开控件;该提示不承诺 child 会一直是叶子。已展开的直接目录加载期间,已知谱系会为每个直接后代预留一行禁用的加载行,而不会递归获取后代目录。随后树会呈现可继续与 one-shot 行;one-shot 的可选 label 缺失时,回退到其会话 id。损坏、不受支持或不可用的候选仍以禁用的 diagnostic 行显示。 `running` 表示在 Host 采样边界,确切 child Agent driver 正在处理工作;`inactive` 表示该 driver 空闲或不存在。UI 不会把任一值解释为成功、失败、取消、完成状态或可恢复性。`subagent.list` 提供当前 driver 状态基线,`host/session-status` 会就地更新已知活动状态,请求内回放会阻止更早发起但尚未完成的列表响应覆盖较新的状态转换,`host/session-removed` 则会使已知行恢复为 `inactive`;重连时会读取新的基线。直接 subagent 的 `host/session-added` 帧会立即把任何已加载的 parent 行翻转为 `hasChildren: true`,并使这项正向提示不被更早发起但尚未完成的目录响应覆盖;受影响分支打开期间,成员、label、mode、diagnostic 与权威快照仍需要通过去抖动的 `subagent.list` 刷新来更新。消息投递时仍以提示词响应为权威依据。 diff --git a/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml b/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml deleted file mode 100644 index 9ae953b44a..0000000000 --- a/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.i18n.yaml +++ /dev/null @@ -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/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md -2026-08-03-web-sticky-collapsible-headers.md: 5bbe91681e656a8278b9d65f12e20a4d9dc779d0 -2026-08-03-web-sticky-collapsible-headers.zh.md: c75846fa7f5ca1088dab530b63243553e50f9579 diff --git a/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md b/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md deleted file mode 100644 index 5bbe91681e..0000000000 --- a/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.md +++ /dev/null @@ -1,41 +0,0 @@ -# Agent Note: Web sticky collapsible headers — Think and compaction toggles pin while scrolling - -Status: implemented - -English | [中文](2026-08-03-web-sticky-collapsible-headers.zh.md) - -## Problem - -Two conversation blocks render their expanded body uncapped, flowing with the page instead of scrolling inside a bounded surface: the Web Think row (`.thinkBody`) and the compaction marker (`.compactionBody`). Every other tool row caps its body and scrolls it inside its own card, so its disclosure header stays visible. The two uncapped blocks do not. A long chain of thought or a long compaction summary carries its own disclosure header off the top of the viewport, so a reader who wants to collapse the block again must scroll the full body back up to reach the toggle. - -## Decision - -The disclosure header of each uncapped block sticks to the conversation scroll container's top while the block is open. The header remains in normal flow when collapsed, so a collapsed block scrolls away like any other row. - -Both blocks already scroll against the shared conversation scroll container (`[data-conversation-scroll]`), not an inner box, so `position: sticky; top: 0` on the header pins it against that container. A base-token background masks the prose that scrolls under the pinned header. - -The pinned header's stacking rank differs by block, because their bodies differ. The Think body uses [compact Markdown](../bug-fix/2026-09-17-thinking-markdown.md) with code banners in normal flow, so `z-index: 1` suffices. The compaction body renders markdown, and a fenced code block in the summary pins its own banner at `z-index: 6` (`packages/client/ui-primitives/src/markdown/CodeBlock.module.css`) with a Copy control inside it; the compaction header therefore uses `z-index: 7` and holds that banner below its own band, so the header never covers the Copy control and the banner never covers the toggle. The band's height is one component-local measurement (`--dsh-compaction-header-height` on `.compactionRow`) shared by the toggle's `height` and the banner's `top`; the banner rule wins over CodeBlock's `top: 0` by specificity, because the two sheets load in their own packages. The pinned header also overrides its hover fill to the opaque `--dsw-alias-interactive-bg-hover-solid` token and squares its corners for as long as the row is open: the default translucent hover token would let the scrolling prose show through the moment the pointer lands on the toggle, and the base 6px radius would leave the same prose visible at the corners. `:hover` raises the rule's specificity above the later base hover rule, so declaration order does not decide the winner. Two other elements take `z-index: 7` to clear the same code banners: the composer seat and the turn-navigation rail slot (`TurnNavigator.module.css`, ui-chat). Their order among equal ranks is stated once, in the composer seat's comment (`packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css`). - -The rules are scoped so only the two uncapped blocks are affected; the capped tool rows keep their existing behavior, since stacking sticky headers across a run of tool rows would pile them at the top. The Think rule is `packages/client/ui-chat/src/client/chat/ReasoningRow.module.css` `.root[data-expanded] [data-open] [data-disclosure-row]` — gated on `DisclosureRow`'s `data-open` so a collapsed Think row never sticks, and scoped under the Think row's own root so no tool-call variant is touched. The compaction rule is `packages/client/ui-chat/src/client/chat/MessageItem.module.css` `.compactionRow:has(.compactionBody) .compactionButton` — the body sibling exists in the DOM only while open, so `:has()` gates the stick on the open state. - -No session, wire, durable event, or model-visible contract changes; this is a presentation-only CSS change owned by the existing components. - -## Alternatives considered - -**Cap the two bodies with `max-height` + internal scroll, matching the tool cards.** Rejected: the Think body is deliberately uncapped so reasoning reads as ordinary message prose ([web-thinking-tail-scroll](../../archived/feature/2026-08-02-web-thinking-tail-scroll.md) and the `.thinkBody` comment own that intent), and the compaction summary is a reading surface. An inner scrollport introduces nested scrolling — the wheel switches from page to box under the cursor — and compresses long technical prose into a small window that is worse to read. Sticky headers keep the flowing-prose reading model and still keep the toggle reachable. - -**Add sticky headers to every collapsible row for consistency.** Rejected: the capped tool rows already keep their header visible because their body scrolls internally, so they have no problem to solve. Making their headers sticky against the page would stack one pinned header per open row at the top of the viewport during a scroll through a run of tool calls, which is visual noise, not consistency. - -**Offset the summary's code banner from `CodeBlock` rather than from the compaction rule.** Rejected: `CodeBlock` could read a property such as `--dsl-code-block-banner-top` that this consumer sets, which would leave the code block the owner of its own geometry and avoid a `:has()` selector reaching into another package's DOM. It also routes every consumer's layout through a property only this block sets, and the offset is this block's presentation concern rather than the code block's: the local rule states the offset beside the toggle that creates it, and `[data-code-block-banner]` is the hook `CodeBlock` already publishes for owner styling. - -**A shared sticky rule on the `DisclosureRow` primitive.** Rejected: `DisclosureRow` backs Think, every tool-call variant, and the context-injection row; a rule there would hit the capped rows too. The behavior belongs only to the uncapped consumers, so each scopes the rule to its own block. - -## Consequences - -The collapse toggle for a long Think block or compaction summary stays reachable without scrolling the body back to its start, while both bodies keep flowing as page prose. The change is CSS-only: no timer, subscription, durable state, DOM structure change, or transport traffic. The compaction rule uses the CSS `:has()` selector, supported across the browsers the Web UI targets. - -## Testing - -The unit specs pin the DOM anchors the selectors key on: `packages/client/ui-chat/tests/reasoning-row.client.spec.tsx` asserts an open Think row nests `[data-disclosure-row]` under `[data-variant='think'][data-expanded] [data-open]` and that a collapsed row has no `[data-open]`; `packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx` asserts the compaction body appears under `.compactionRow` only while open. `packages/client/ui-chat/tests/sticky-header-styles.client.spec.ts` reads the rules as CSS text and pins the declarations the pinning depends on: `position: sticky`, `top: 0`, the square corners, the `z-index` rank of each side, the single measurement shared by the toggle's height and the code banner's offset, and the opaque hover token, because jsdom computes no sticky layout and the render specs cannot fail on a changed declaration. - -The real-browser evidence is two keyless Chromium e2e paths. `apps/web/tests/lifecycle-chrome.e2e.ts` expands the settled turn's process row, opens the Think row, and asserts its header computes `position: sticky`, `top: 0px` (and is not sticky while collapsed); that fixture's recorded reasoning is a single line, too short to overflow, so it proves only that the CSS resolves onto the Think header. `apps/web/tests/seeded-history.e2e.ts` carries the pinned-while-scrolling evidence: it seeds a compaction whose summary length this suite controls (a fenced code block plus 40 list items), shrinks the viewport to force overflow, scrolls the marker into its pinned state, and asserts the header computes `position: sticky`, `top: 0px`, a `z-index` greater than the code block banner's, that it holds at the scrollport top after the scroll, that its own center is the topmost hit-tested element (the toggle stays clickable), and that its hover fill stays fully opaque (alpha 1). The same case scrolls the summary's code banner into its own stuck position and asserts the banner stops below the toggle's band and that the banner's Copy control owns its center, so the offset cannot regress into a covered control. The case also writes a keyless geometry golden (`snapshots/web/seeded-history/sticky-geometry.expected.md`) fixing these platform-independent semantic facts, since this is a user-visible CSS behavior that changes no DOM and no accessible name, so the aria goldens cannot capture it. The PR demo GIF, recorded against a real server and a real model round, carries the visual evidence that the pinned header stays at the top while the body scrolls. diff --git a/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.zh.md b/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.zh.md deleted file mode 100644 index c75846fa7f..0000000000 --- a/.agents/notes/implemented/feature/2026-08-03-web-sticky-collapsible-headers.zh.md +++ /dev/null @@ -1,41 +0,0 @@ -# Agent Note:Web 可折叠块的钉住标题 —— Think 与压缩标记的折叠按钮在滚动时钉住 - -Status: implemented - -[English](2026-08-03-web-sticky-collapsible-headers.md) | 中文 - -## 问题 - -会话里有两个块的展开正文不封顶,随整页滚动,而不是在有界的表面内部滚动:Web Think 行(`.thinkBody`)和压缩标记(`.compactionBody`)。其他每个工具行都给正文封顶并在自己的卡片内部滚动,所以折叠标题始终可见。这两个不封顶的块做不到。一段很长的思维链或很长的压缩摘要会把自己的折叠标题顶出视口上方,想再次折叠该块的读者必须把整段正文滚回顶部才能够到折叠按钮。 - -## 决策 - -每个不封顶块的折叠标题在块展开时钉在会话滚动容器的顶部。折叠时标题保持在正常文档流中,所以折叠的块会像其他行一样滚走。 - -这两个块本来就是相对共享的会话滚动容器(`[data-conversation-scroll]`)滚动,而非某个内层框,所以在标题上加 `position: sticky; top: 0` 就把它钉在该容器上。一个 base token 背景遮住在钉住的标题下方滚过的正文。 - -钉住的标题的层叠级别按块而异,因为两者正文不同。Think 正文使用[紧凑 Markdown](../bug-fix/2026-09-17-thinking-markdown.zh.md),代码栏处于正常文档流中,`z-index: 1` 就够。压缩正文渲染 markdown,摘要里的围栏代码块会把自己的 banner 钉在 `z-index: 6`(`packages/client/ui-primitives/src/markdown/CodeBlock.module.css`),banner 里带一个 Copy 控件;因此压缩标题用 `z-index: 7`,并让那个 banner 停在自己的标题带下方,这样标题永远不会盖住 Copy 控件,banner 也不会盖住折叠按钮。标题带高度是一个组件局部量(`.compactionRow` 上的 `--dsh-compaction-header-height`),由折叠按钮的 `height` 与 banner 的 `top` 共用;banner 规则靠特异性压过 CodeBlock 的 `top: 0`,因为两张样式表分属不同的包。钉住的标题还把 hover 底覆盖为不透明的 `--dsw-alias-interactive-bg-hover-solid` token,并在整行展开期间都保持直角:默认的半透明 hover token 会在指针落到折叠按钮准备折叠的瞬间让滚动的正文透出,而基础的 6px 圆角会让同样的正文在四角露出来。`:hover` 把该规则的特异性抬到文件更靠后的 hover 基础规则之上,所以胜负不由声明顺序决定。另外还有两个元素为了避开同一批代码 banner 取 `z-index: 7`:输入框座与轮次导航轨道槽(`TurnNavigator.module.css`,ui-chat)。同级之间的先后顺序只写在一处:`packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css` 中输入框座的注释。 - -规则被限定作用域,只影响这两个不封顶的块;封顶的工具行保持原有行为,因为让一连串工具行的 sticky 标题层层堆叠会把它们全挤在顶部。Think 规则是 `packages/client/ui-chat/src/client/chat/ReasoningRow.module.css` 的 `.root[data-expanded] [data-open] [data-disclosure-row]`,用 `DisclosureRow` 的 `data-open` 门控,折叠的 Think 行绝不钉住,并限定在 Think 行自己的 root 之下,不触及任何工具调用 variant。压缩规则是 `packages/client/ui-chat/src/client/chat/MessageItem.module.css` 的 `.compactionRow:has(.compactionBody) .compactionButton`,正文兄弟节点只在展开时存在于 DOM,所以 `:has()` 就以展开状态门控钉住。 - -不改动任何 session、wire、durable event 或 model-visible 契约;这是一处纯展示层的 CSS 改动,由既有组件拥有。 - -## 曾考虑的替代方案 - -**用 `max-height` 加内部滚动给这两个正文封顶,与工具卡片一致。** 否决:Think 正文是刻意不封顶的,好让推理读起来像普通消息正文([web-thinking-tail-scroll](../../archived/feature/2026-08-02-web-thinking-tail-scroll.md) 和 `.thinkBody` 注释拥有这一意图),压缩摘要是一个阅读表面。内层滚动框会引入嵌套滚动,滚轮在光标下从整页切换到框内,并把很长的技术性正文压进一个更难读的小窗口。钉住标题保留了流式正文的阅读模型,同时让折叠按钮依然够得着。 - -**为一致性给每个可折叠行都加钉住标题。** 否决:封顶的工具行因为正文在内部滚动,标题本来就一直可见,没有需要解决的问题。让它们的标题相对整页钉住,会在滚过一连串工具调用时把每个展开行各自钉住的标题堆叠在视口顶部,这是视觉噪音,不是一致性。 - -**把摘要内代码栏的偏移交给 `CodeBlock` 承担。** 否决:可以让 `CodeBlock` 读取使用方设置的 `--dsl-code-block-banner-top` 之类的属性,这样代码块继续拥有自己的几何,也避免用 `:has()` 选择器伸进另一个包的 DOM。代价是把所有使用方的布局都接到一个只有这个块会设置的属性上,而这段偏移是这个块的呈现问题、不是代码块的:局部规则把偏移写在造成它的折叠按钮旁边,而 `[data-code-block-banner]` 本来就是 `CodeBlock` 为使用者样式发布的钩子。 - -**在 `DisclosureRow` 基元上加一条共享的 sticky 规则。** 否决:`DisclosureRow` 支撑 Think、每个工具调用 variant 以及 context-injection 行;在那里加规则会一并命中封顶行。该行为只属于不封顶的消费者,所以各自把规则限定在自己的块上。 - -## 后果 - -很长的 Think 块或压缩摘要的折叠按钮无需把正文滚回起点就能够到,同时两个正文都保持作为整页正文流动。改动是纯 CSS:没有计时器、订阅、durable state、DOM 结构改动或传输流量。压缩规则使用 CSS `:has()` 选择器,Web UI 所面向的各浏览器均支持。 - -## 测试 - -单元测试钉住选择器所依赖的 DOM 锚点:`packages/client/ui-chat/tests/reasoning-row.client.spec.tsx` 断言展开的 Think 行在 `[data-variant='think'][data-expanded] [data-open]` 之下嵌套了 `[data-disclosure-row]`,且折叠行没有 `[data-open]`;`packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx` 断言压缩正文只在展开时出现在 `.compactionRow` 之下。`packages/client/ui-chat/tests/sticky-header-styles.client.spec.ts` 把这两条规则当作 CSS 文本读取,逐条固定钉住所依赖的声明:`position: sticky`、`top: 0`、直角圆角、两侧各自的 `z-index` 级别、折叠按钮高度与代码 banner 偏移共用的那一个量,以及不透明的 hover token——因为 jsdom 不计算 sticky 布局,渲染类测试也无法在某条声明被改动时变红。 - -真实浏览器证据由两条 keyless Chromium e2e 路径承载。`apps/web/tests/lifecycle-chrome.e2e.ts` 展开已结束轮次的 process 行、展开 Think 行,并断言其标题计算出 `position: sticky`、`top: 0px`(折叠时非 sticky);该 fixture 录制的 reasoning 只有一行,太短不足以溢出,所以它只证明 CSS 解析到了 Think 标题。`apps/web/tests/seeded-history.e2e.ts` 承载「钉住态随滚动」的证据:它种入一个摘要长度由本套件控制的压缩(一个围栏代码块加 40 个列表项),把视口压小以强制溢出,滚动到 marker 的钉住态,断言标题计算出 `position: sticky`、`top: 0px`、`z-index` 大于代码块 banner、滚动后仍停在滚动口顶边、其自身中心是命中测试命中的最上层元素(折叠按钮保持可点击),以及其 hover 底保持完全不透明(alpha 1)。同一用例还把摘要里的代码 banner 滚到它自己的钉住位置,断言 banner 停在标题带下方、且 banner 的 Copy 控件在它的中心点命中自身,使这条偏移不会退化成控件被盖住。同一用例还写出一份 keyless 几何 golden(`snapshots/web/seeded-history/sticky-geometry.expected.md`),把这些与平台无关的语义事实固定下来,因为这是一处用户可见、但不改动 DOM 与无障碍名称的 CSS 行为,无障碍 golden 捕获不到它。PR demo GIF 用真实服务器加真实模型轮次录制,承载视觉证据:钉住的标题在正文滚动时停留在顶部。 diff --git a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml index c79578f47b..a8b84e3d55 100644 --- a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml @@ -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-06-manager-owned-subagent-settlement-delivery.md -2026-08-06-manager-owned-subagent-settlement-delivery.md: 5553d4c06a6776f0ef025e19ae7f4f67fe277a56 -2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 6d7ee756ad531e123508e8667adc4e5f99c2b9ea +2026-08-06-manager-owned-subagent-settlement-delivery.md: 7f6cfc90862b5bb32368b4b0e1fbe068ba0cbf66 +2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 23c5506f79d4d95308b0533e8d507ac856c4d85e diff --git a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md index 5553d4c06a..7f6cfc9086 100644 --- a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md +++ b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md @@ -90,7 +90,7 @@ The refusal and interruption wordings are pinned verbatim in unit tests rather t **Filter the canonical child output.** Removing nontext blocks in `AssistantOutputFold` would discard assistant content needed by SDK and UI consumers. Only the parent notice requires a text projection. -**Relax the Messages serializer.** Accepting or silently discarding invalid user-role blocks would conceal the producer's role conversion error. Notice construction supplies content that every parent provider can represent while protocol validation remains strict. +**Rely on serializer tolerance for notice construction.** Notice construction supplies text that every parent provider can represent; adapter filtering cannot replace that producer responsibility. The [saved-input compatibility decision](../bug-fix/2026-09-18-messages-input-history-compatibility.md) supersedes the rejection of Messages input filtering for already recorded notices, which a producer fix cannot change. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md index 6d7ee756ad..23c5506f79 100644 --- a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md +++ b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md @@ -90,7 +90,7 @@ Status: implemented **过滤规范的子级输出。** 在 `AssistantOutputFold` 中删除非文本块,会丢弃 SDK 和 UI 消费方需要的 assistant 内容。只有父级通知需要文本投影。 -**放宽 Messages 序列化器。** 接受或静默丢弃用户角色中的无效块,会掩盖生产方的角色转换错误。通知构建提供每种父级提供方都能表示的内容,协议校验仍保持严格。 +**依赖序列化器容错来构建通知。** 通知构建提供每种父级提供方都能表示的文本;适配器过滤不能替代生产方的责任。[已保存输入兼容决策](../bug-fix/2026-09-18-messages-input-history-compatibility.zh.md)取代了对已记录通知进行 Messages 输入过滤的拒绝,因为生产方修复无法改变这些通知。 ## 后果 diff --git a/.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md b/.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md deleted file mode 100644 index c4e506294c..0000000000 --- a/.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: Web clickable-link language — link alias, dotted hover underline, category glyphs - -Status: implemented - -English | [中文](2026-09-04-web-clickable-link-styles.zh.md) - -## Problem - -Clickable artifact links in the chat transcript wore four different costumes: markdown anchors and prose file mentions were business-primary blue with a solid hover underline, web search/fetch links matched that pair, produced-file chips were grey pills (label-secondary text on interactive-bg-hover, 96px max width), and workflow member links carried a resting solid underline. Nothing marked what a link opens (browser, host app, Finder, in-app view), and link color was coupled to `--dsw-alias-state-business-primary`, which also drives focus rings and state dots, so tuning link color risked unrelated surfaces. - -## Decision - -One link language across the transcript's clickable-link surfaces — markdown anchors (including reference links, mailto, and URL-promoted inline code), prose file mentions, web search source links and the fetch URL, produced-file chips, and workflow member links: - -- Color comes through a dedicated `--dsw-alias-link` alias in `design-platform.css` (light `deepseek-500`, dark `deepseek-400`), decoupled from `state-business-primary`; links render at `font-weight: 500` with no underline at rest and `underline dotted` at 3px offset on hover/focus. -- A leading category glyph — the new `LinkIcon` in ui-primitives with kinds `url`, `folder`, `code`, `image`, `document`, and `other` (paper) — renders `currentColor` only; the `url` glyph is the globe, or the destination site's own mark when its host is a known site ([known-site link marks](2026-09-16-known-site-link-marks.md)); `classifyLinkPath` folds the [shared detailed file-type classification](2026-09-08-shared-file-type-icons.md) into those six link categories, and code, web, and data extensions share the code glyph by design. Two anchor shapes carry no glyph: workflow member links (an in-app member view fits no file or URL category) and anchors wrapping only images (a badge or thumbnail — a dangling globe beside the picture leads no text). Inline glyphs sit at 1.1em with a −0.25em baseline offset; the flex-centered produced-file glyphs instead nudge 1.2px down because the 22px text box carries its glyphs below box center. -- Produced-file chips drop the grey pill and the 96px cap: plain link-blue text at natural width that shrinks with ellipsis only when the row overflows; the container-query bands still budget 96px per chip when choosing how many chips to show. -- [Compact Thinking Markdown](../bug-fix/2026-09-17-thinking-markdown.md) retains tertiary text color and a resting dotted underline to preserve secondary emphasis. -- Deliberately untouched: ToolRow's grey dotted file links, and the grey "Show in folder" action (it gains the folder glyph but keeps its grey style). -- In the same pass, the inline-code chip tint moved from `neutral-bluish-100` to `neutral-50` (dark: `neutral-800`) and gained a 0.5px l1 border. - -Coverage: a LinkIcon unit spec (one distinct glyph per kind, classification table), refreshed markdown-dom fixtures, and the `clickable-links-gallery` web e2e — one settled keyless turn rendering every clickable link form — registered in the host compiler face (`tsconfig.host.json`) like its other scaffold-importing siblings. - -## Alternatives considered - -- **Colored Word/Excel/PPT/PDF link glyphs.** Rejected: fixed brand fills break the icon set's currentColor-only rule, so those extensions fold into the single outline `document` glyph. The larger file-card primitive uses distinct current-color silhouettes instead. -- **Per-extension link icons.** Collapsed to six categories: more glyphs than the eye can parse at 14px adds noise. The 28px `FileTypeIcon` owns the more detailed file identities, and per-site marks live behind the same `url` category ([known-site link marks](2026-09-16-known-site-link-marks.md)). -- **Keeping links on `state-business-primary`.** Darker link blues (blue-600/650/700 were auditioned and reverted) would have dragged focus rings and state dots along; the dedicated alias localizes any future tuning to one line. -- **Glyphs on ToolRow path links.** Rejected: tool rows keep their quieter grey dotted affordance, and leading glyphs there would stack icons in already dense rows. - -## Consequences - -- A new clickable artifact surface should consume `--dsw-alias-link` and the LinkIcon vocabulary rather than introduce another color or underline form; the rule lives in [docs/web-styling.md](../../../../docs/web-styling.md). -- Long produced-file names take their natural width; when a row overflows, flex shrinks all chips proportionally, so several long names shrink together instead of the last one yielding first. -- mailto links currently share the `url` globe; a distinct mail category is a one-line addition if ever wanted. diff --git a/.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md b/.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md deleted file mode 100644 index 01258674b4..0000000000 --- a/.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: Web 可点击链接语言——链接别名、hover 点状下划线、分类图标 - -Status: implemented - -[English](2026-09-04-web-clickable-link-styles.md) | 中文 - -## 问题 - -会话记录里的可点击产物链接有四套互不一致的样式:Markdown 锚点和正文文件引用是 business-primary 蓝加 hover 实线下划线,网页搜索/抓取链接与之相同,产物 chips 是灰色药丸(interactive-bg-hover 底上的 label-secondary 文字,96px 最大宽度),workflow 成员链接则带常驻实线下划线。没有任何标记说明链接点开后去哪儿(浏览器、宿主应用、Finder、应用内视图),而且链接颜色耦合在 `--dsw-alias-state-business-primary` 上——它同时驱动焦点环和状态点,调链接色会波及无关表面。 - -## 决策 - -会话记录的可点击链接表面——Markdown 锚点(含引用式链接、mailto、被提升为链接的 inline code)、正文文件引用、网页搜索来源链接与抓取 URL、产物 chips、workflow 成员链接——统一为一套链接语言: - -- 颜色经由 `design-platform.css` 中专用的 `--dsw-alias-link` 别名(亮色 `deepseek-500`,暗色 `deepseek-400`),与 `state-business-primary` 解耦;链接以 `font-weight: 500` 呈现,默认无下划线,hover/focus 时为 3px offset 的 `underline dotted`。 -- 前置分类图标——ui-primitives 新增的 `LinkIcon`,kind 为 `url`、`folder`、`code`、`image`、`document`、`other`(纸张)——只渲染 `currentColor`;`url` 图形为地球,目的地主机属于已知站点时则为该站点自己的标记([已知站点链接标记](2026-09-16-known-site-link-marks.zh.md));`classifyLinkPath` 把[共享精细文件类型分类](2026-09-08-shared-file-type-icons.zh.md)折叠进这六种链接类别,代码、网页、数据扩展名按设计共用 code 图形。两类锚点不带图标:workflow 成员链接(应用内成员视图不属于任何文件或 URL 类别)和只包图片的锚点(徽章或缩略图——图片旁悬着的地球没有可引导的文字)。行内图标为 1.1em、基线偏移 −0.25em;flex 居中的产物图标则下移 1.2px,因为 22px 文字盒的字形低于盒中心。 -- 产物 chips 去掉灰色药丸和 96px 上限:纯链接蓝文字按自然宽度展示,仅当整行溢出时才收缩出省略号;容器查询档位在决定展示几个 chip 时仍按每个 96px 预算。 -- [紧凑 Thinking Markdown](../bug-fix/2026-09-17-thinking-markdown.zh.md) 保留 tertiary 文字色和默认点状下划线,以保持次级强调。 -- 刻意不动:ToolRow 的灰色点线文件链接,以及灰色的「在文件夹中显示」操作(它获得文件夹图标但保持灰色样式)。 -- 同一批次中,inline code 底色从 `neutral-bluish-100` 换到 `neutral-50`(暗色:`neutral-800`),并新增 0.5px l1 描边。 - -覆盖:LinkIcon 单测(每个 kind 一个独立图形、分类表)、刷新后的 markdown-dom 夹具,以及 `clickable-links-gallery` web e2e——一个 settled 的 keyless 回合渲染全部可点击链接形态——像其他引 scaffold 的同类一样注册进 host 编译面(`tsconfig.host.json`)。 - -## 备选方案 - -- **彩色 Word/Excel/PPT/PDF 链接图形。** 否决:固定品牌填充违反图标集 currentColor-only 规则,因此这些扩展名并入单一的 outline `document` 图形。较大的文件卡片 primitive 改用各自不同的 current-color 轮廓。 -- **每个扩展名一个链接图标。** 收敛为六个类别:14px 下超出肉眼可分辨数量的图形只会增加噪音。28px 的 `FileTypeIcon` 拥有更精细的文件身份;按站点标记位于同一 `url` 类别之下([已知站点链接标记](2026-09-16-known-site-link-marks.zh.md))。 -- **链接继续用 `state-business-primary`。** 更深的链接蓝(试过 blue-600/650/700 又回退)会连带焦点环和状态点;专用别名把未来的调色收敛到一行。 -- **给 ToolRow 路径链接加图形。** 否决:工具行保持更安静的灰色点线示能,在已经很密的行里加前置图形会造成图标堆叠。 - -## 后果 - -- 新的可点击产物表面应消费 `--dsw-alias-link` 和 LinkIcon 词汇,而不是引入另一种颜色或下划线形态;规则记录在 [docs/web-styling.md](../../../../docs/web-styling.zh.md)。 -- 长产物文件名按自然宽度展示;整行溢出时 flex 按比例收缩所有 chip,几个长名字一起收缩,而不是最后一个先让位。 -- mailto 链接目前共用 `url` 地球图形;若将来需要独立的邮件类别,一行即可加上。 diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml index a2f18c11cb..702e3bf1fc 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.i18n.yaml @@ -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-05-sidebar-text-preview-and-file-tree.md -2026-09-05-sidebar-text-preview-and-file-tree.md: 0142b325004a1fc018e944ed908d600e75f0ab0d -2026-09-05-sidebar-text-preview-and-file-tree.zh.md: d40ca8c790c9a7208a2243422c0973fc293a5915 +2026-09-05-sidebar-text-preview-and-file-tree.md: 8ddb1a336307011d8f6ce9bbe927f00642a83e60 +2026-09-05-sidebar-text-preview-and-file-tree.zh.md: 294a2b456008837882f7f82e55e8e9e435254554 diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md index 0142b32500..8ddb1a3363 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.md @@ -20,7 +20,7 @@ Three tab types ship with the Sidebar: the **guide** (`ui-sidebar-right`), the * The guide is what a pane shows before it holds content. Its registration is `{ id: '@deepseek-ai/dsh-client-ui-sidebar-right/guide', kind: 'guide', priority: 'builtin', title }` with no `patterns`: a guide views nothing, so it is opened by kind through `openTab` and recorded under the page address `sidebar://guide`, which is the registry's bookkeeping and never composed by a caller. The tab's title is `开始` / `Start`, captured into the layout record when the pane is seeded, so a later language change relabels the type and not tabs already open. -The body projects every registered type's `guide[]` in `order` through the registry's observable `guide()` list, so a type registering later appears without the guide knowing it. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the compass, optional descriptions, fallback glyph, and current capsule layout. Picking a capsule calls `tabActions.openTab(entry.kind, { replaceTab: true })`: the picked type opens in the guide's own tab, and the guide is gone. The guide is a doorway, not a page that stays open beside what it opened. +The body projects every registered type's `guide[]` in `order` through the registry's observable `guide()` list, so a type registering later appears without the guide knowing it. The [Sidebar README](../../../../packages/client/ui-sidebar-right/README.md#the-guide) describes the compass, optional descriptions, fallback glyph, and current capsule layout. Picking a capsule calls `tabActions.openTab(entry.kind, { replaceTab: true })`: the picked type opens in the guide's own tab, and the guide is gone. The guide is a doorway, not a page that stays open beside what it opened. The body is also the replacement seam. It renders the `sidebar.right.tab.guide` chain with the shipped guide as the chain's fallback, so a product that registers its own entry takes the whole body, and with no entry, or every entry declining, the shipped guide draws. Because the shipped guide is the fallback and not a chain entry, there is always exactly one body and it cannot be outvoted by accident. @@ -48,7 +48,7 @@ A failed read keeps content already shown and adds a localized failure with a re ### The file tree -`files` is a page type, not a viewer: it claims no address. Its registration is `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }` — no `patterns`, because nothing navigates *to* a file tree by address; the guide's entry box opens the type itself. `FolderSheetGlyph` adapts the shared coloured folder sheet to the guide's requested glyph size; the [guide refinement](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns that presentation choice. `id` is the implementation's identity in the Tab system and doubles as the `key` of the body seat `sidebar.right.pane.tab`, so the same string names the type and the component that draws it. `register()` returns a disposer and goes through `ctx.effect`, as every registration does. +`files` is a page type, not a viewer: it claims no address. Its registration is `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }` — no `patterns`, because nothing navigates *to* a file tree by address; the guide's entry box opens the type itself. `FolderSheetGlyph` adapts the shared coloured folder sheet to the guide's requested glyph size; the [Sidebar README](../../../../packages/client/ui-sidebar-right/README.md#the-guide) describes that presentation rule. `id` is the implementation's identity in the Tab system and doubles as the `key` of the body seat `sidebar.right.pane.tab`, so the same string names the type and the component that draws it. `register()` returns a disposer and goes through `ctx.effect`, as every registration does. The root is the session's working directory as the Host reports it in the session list (`useSessions().byId[sessionId].cwd`), labelled by `workspaceTitleOf` from `dsh-util-workspace-path` — the final non-empty path segment — with the root string itself as the label when the path is separator-only. A session without a working directory shows one line (`noWorkspace`) and issues no request. There is no root chooser and no way to browse upward: the Host's `list` refuses paths outside the Session's workspace root, so the one directory the client can list is the one it shows. diff --git a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md index d40ca8c790..294a2b4560 100644 --- a/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md +++ b/.agents/notes/implemented/feature/2026-09-05-sidebar-text-preview-and-file-tree.zh.md @@ -20,7 +20,7 @@ Sidebar 随包交付三个 tab 类型:**引导页**(`ui-sidebar-right`)、 引导页是 pane 承载内容之前显示的东西。它的注册定义是 `{ id: '@deepseek-ai/dsh-client-ui-sidebar-right/guide', kind: 'guide', priority: 'builtin', title }`,没有 `patterns`:引导页不查看任何东西,所以经 `openTab` 按 kind 打开,并记在页地址 `sidebar://guide` 之下——那是注册表自己的记账,调用方从不拼它。tab 标题是 `开始` / `Start`,在 pane 播种时捕获进布局记录,于是之后切换语言只重标类型,不改已开着的 tab。 -正文按 `order` 从每个已注册类型的 `guide[]` 投影入口,并通过注册表可观察的 `guide()` 列表更新,因此后注册的类型无需引导页感知即可出现。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责罗盘、可选描述、兜底图标和当前胶囊布局。点选胶囊会调用 `tabActions.openTab(entry.kind, { replaceTab: true })`:被选的类型在引导页自己的 tab 里打开,引导页随之消失。引导页是一扇门,不是留在被打开者旁边的一页。 +正文按 `order` 从每个已注册类型的 `guide[]` 投影入口,并通过注册表可观察的 `guide()` 列表更新,因此后注册的类型无需引导页感知即可出现。[Sidebar README](../../../../packages/client/ui-sidebar-right/README.zh.md#the-guide)说明罗盘、可选描述、兜底图标和当前胶囊布局。点选胶囊会调用 `tabActions.openTab(entry.kind, { replaceTab: true })`:被选的类型在引导页自己的 tab 里打开,引导页随之消失。引导页是一扇门,不是留在被打开者旁边的一页。 体同时也是替换接缝。它渲染 `sidebar.right.tab.guide` 链,并以随包交付的引导页作为链的 fallback,于是注册了自己入口的产品接管整个体,而没有入口、或每个入口都拒绝时,随包交付的引导页照常绘制。因为随包交付的引导页是 fallback 而不是链上的一个入口,所以永远恰有一个体,也不可能被意外投掉。 @@ -48,7 +48,7 @@ store 是 Slot 标准件:每会话一个独占实例,按 tab id 分桶,持 ### 文件树 -`files` 是页类型,不是查看器:它不认领任何地址。注册定义是 `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }`——没有 `patterns`,因为没有谁按地址导航*到*一棵文件树;引导页的入口框打开的是类型本身。`FolderSheetGlyph` 把共享的彩色文件夹页适配到引导页请求的图标尺寸;[引导页细化记录](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责这项展示选择。`id` 是这个实现在 Tab 系统里的唯一键,同时也是体坑位 `sidebar.right.pane.tab` 的 `key`,于是同一个串既命名类型也命名画它的组件。`register()` 返回 disposer 并经 `ctx.effect` 注册,与所有注册一致。 +`files` 是页类型,不是查看器:它不认领任何地址。注册定义是 `{ kind: 'files', id: '@deepseek-ai/dsh-client-ui-sidebar-files', priority: 'builtin', title, guide: [{ order: 10, title, description, icon: FolderSheetGlyph }] }`——没有 `patterns`,因为没有谁按地址导航*到*一棵文件树;引导页的入口框打开的是类型本身。`FolderSheetGlyph` 把共享的彩色文件夹页适配到引导页请求的图标尺寸;[Sidebar README](../../../../packages/client/ui-sidebar-right/README.zh.md#the-guide)说明这项展示规则。`id` 是这个实现在 Tab 系统里的唯一键,同时也是体坑位 `sidebar.right.pane.tab` 的 `key`,于是同一个串既命名类型也命名画它的组件。`register()` 返回 disposer 并经 `ctx.effect` 注册,与所有注册一致。 根是 Host 在会话列表里上报的会话工作目录(`useSessions().byId[sessionId].cwd`),标签由 `dsh-util-workspace-path` 的 `workspaceTitleOf` 给出——路径最后一个非空段——路径只有分隔符时用根串本身作标签。没有工作目录的会话只显示一行(`noWorkspace`),不发请求。没有根选择器,也不能往上浏览:Host 的 `list` 拒绝会话工作区根之外的路径,所以客户端能列的那一个目录就是它显示的目录。 diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml index a84c4d396a..0c2c513623 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml @@ -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-07-composer-session-stats-pills.md -2026-09-07-composer-session-stats-pills.md: 22f78dfdde854baaf1ced8c20c55042952df8c26 -2026-09-07-composer-session-stats-pills.zh.md: 6b71c6e7cf03cbd32fa4da9edf38f6aa4814310c +2026-09-07-composer-session-stats-pills.md: d165aab471bc5958cb7a0b287d278a0ae48485ee +2026-09-07-composer-session-stats-pills.zh.md: dac136d4e60e50b026ed082eb7a006a80059c328 diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md index 22f78dfdde..d165aab471 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md @@ -12,7 +12,8 @@ The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `co `StatsPills` (packages/client/ui-chat/src/client/chat/StatsPills.tsx) replaces `StatsLine` on the same `conversation.composer.dock` slot; the losing variant is deleted, its shared helpers (`deriveStats`, `formatDuration`, `cacheHitPercent`, `billedInputTokens`) absorbed into the new module, and the dead `stats.llm`, `stats.toolCall`, `stats.ttftAverage`, `stats.tokensPerSecond`, and `stats.tokens` locale keys removed. -- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, output, and cache write when non-zero — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually. [Guide start page and stat pill refinements](2026-09-10-guide-start-page-and-stat-pill-refinements.md) owns the zero cache-write omission. +- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutlineRegular`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutlineRegular`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, output, and cache write when non-zero — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually. [StatsPills](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) owns the zero cache-write omission. +- **Shared information tier.** Composer stats and per-turn tail metadata use the secondary font size minus one pixel. Assistant-tail metadata uses the action icons' tertiary label color and starts after an extra 8px separation from the action cluster. - **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy remains owned by ui-conversation's `ContextMeter`, with its ring and percentage after the two stats pills below the input card. The common dock groups statistics together and leaves the toolbar for input actions; ui-chat does not import the context component. The context panel renders through a portal and uses ui-primitives for viewport-clamped positioning and outside-pointer dismissal, including when the statistics contribution is absent. - **Render discipline.** The row folds settled nodes only (`chat.legacy.nodes` identity), so streaming chunk frames cause zero rerenders — pinned by a render-count unit test. A session with no closed step and no billed tokens renders nothing. - **The composer owns dock spacing.** `InputBar` places slot contributions and `ContextMeter` in one centered flex row, supplies 4px above the dock and 4px below it even when the slot has no visible contribution. The hero keeps no bottom padding and hides its empty dock. `StatsPills` supplies shrinkable time and billing content; it does not claim the full row width or add outer padding. diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md index 6b71c6e7cf..dac136d4e6 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md @@ -12,7 +12,8 @@ Status: implemented `StatsPills`(packages/client/ui-chat/src/client/chat/StatsPills.tsx)在同一 `conversation.composer.dock` 插槽上取代 `StatsLine`;落选变体已删除,其共享工具函数(`deriveStats`、`formatDuration`、`cacheHitPercent`、`billedInputTokens`)并入新模块,废弃的 `stats.llm`、`stats.toolCall`、`stats.ttftAverage`、`stats.tokensPerSecond`、`stats.tokens` 文案键一并移除。 -- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、输出,以及非零时的缓存写入——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。[引导起始页与统计 pill 的细化](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md)负责缓存写入为零时省略该行的规则。 +- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutlineRegular`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutlineRegular`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、输出,以及非零时的缓存写入——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。[StatsPills](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx)负责缓存写入为零时省略该行的规则。 +- **统一信息层级。** 输入框底部统计与单轮回答末尾信息都使用次级字号减 1px。回答末尾信息使用与操作图标一致的 tertiary 文字颜色,并与操作图标组额外拉开 8px。 - **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用仍归 ui-conversation 的 `ContextMeter` 所有,在输入卡片下方、两个统计 pill 之后显示圆环和百分比。共用 dock 将统计信息放在一起,工具栏保留给输入操作;ui-chat 不导入上下文组件。上下文面板通过 portal 渲染,复用 ui-primitives 的视口内定位与外部指针关闭工具,统计贡献项缺席时也不会越界。 - **渲染纪律。** 该行只折叠已定稿节点(`chat.legacy.nodes` 身份),流式 chunk 帧零重渲染——由渲染计数单测钉住。无已完成步且无计费 token 的会话什么都不渲染。 - **输入框负责 dock 间距。** `InputBar` 将 slot 贡献项和 `ContextMeter` 放在同一个居中的 flex 行中,在 dock 上下各提供 4px 留白,即使 slot 没有可见贡献项也保持该间距。hero 保持无底部留白,并隐藏空 dock。`StatsPills` 提供可收缩的时间与计费内容,不占满整行宽度,也不添加外部 padding。 diff --git a/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.i18n.yaml index ddba9ebf1e..961abe5984 100644 --- a/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.i18n.yaml @@ -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-07-deepseek-messages-adapter.md -2026-09-07-deepseek-messages-adapter.md: c667ec3afdd9956282a532e748ad2e2488d26f5b -2026-09-07-deepseek-messages-adapter.zh.md: 0e3425e41d8a9585a1638eedeefcf6ead76a5a78 +2026-09-07-deepseek-messages-adapter.md: caec59feb3a84140427bd3ae8b738274c3bc5c47 +2026-09-07-deepseek-messages-adapter.zh.md: 030c91cb9a5f566ce09892bb054b63b1d243f6bb diff --git a/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.md b/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.md index c667ec3afd..caec59feb3 100644 --- a/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.md +++ b/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.md @@ -36,6 +36,6 @@ Both transports use the existing [request-extension registry](../architecture/20 ## Consequences -The package owns wire validation, stop-reason mapping, cancellation, and error classification, so protocol changes require adapter maintenance. Unsupported content and incomplete streams fail explicitly. The existing retry consumer owns retries; the existing assembler drops incomplete tool calls at the output limit. Messages defaults cover the shared base, Web, and standalone first-party compositions. Protocol changes preserve the provider id and saved model selections, but an explicit endpoint override must support the selected protocol. +The package owns wire validation, stop-reason mapping, cancellation, and error classification, so protocol changes require adapter maintenance. User and tool-result input follows the [saved-input compatibility rule](../bug-fix/2026-09-18-messages-input-history-compatibility.md); other unsupported content and incomplete streams fail explicitly. The existing retry consumer owns retries; the existing assembler drops incomplete tool calls at the output limit. Messages defaults cover the shared base, Web, and standalone first-party compositions. Protocol changes preserve the provider id and saved model selections, but an explicit endpoint override must support the selected protocol. Verification covers wire fixtures, real Loader composition, per-file unit coverage, [recorded Session replay](../../../../snapshots/session/deepseek-messages-replay/snapshot.yml) with [unknown replay versions](../../../../snapshots/session/deepseek-messages-degraded-replay/snapshot.yml), a Web Messages Session replay, and credential-gated text, thinking, tool continuation, image, and cancellation requests. Live gateway checks establish compatibility with the configured gateway; they do not establish compatibility with every Anthropic proxy. diff --git a/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.zh.md b/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.zh.md index 0e3425e41d..030c91cb9a 100644 --- a/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.zh.md +++ b/.agents/notes/implemented/feature/2026-09-07-deepseek-messages-adapter.zh.md @@ -36,6 +36,6 @@ Web 始终显示 DeepSeek,不提供协议选择器。两个协议共用 `baseU ## 结果 -该包负责协议校验、停止原因映射、取消和错误分类,因此协议变化需要维护适配器。不支持的内容和不完整的流会明确报错。现有重试消费者负责重试;现有装配器在输出达到上限时丢弃未完成的工具调用。Messages 默认配置覆盖共享 base、Web 和独立的官方组合。协议变化保留 provider id 与已保存的模型选择,但显式端点覆盖必须支持所选协议。 +该包负责协议校验、停止原因映射、取消和错误分类,因此协议变化需要维护适配器。用户与工具结果输入遵循[已保存输入兼容规则](../bug-fix/2026-09-18-messages-input-history-compatibility.zh.md);其他不支持的内容和不完整的流会明确报错。现有重试消费者负责重试;现有装配器在输出达到上限时丢弃未完成的工具调用。Messages 默认配置覆盖共享 base、Web 和独立的官方组合。协议变化保留 provider id 与已保存的模型选择,但显式端点覆盖必须支持所选协议。 验证覆盖协议夹具、真实 Loader 组合、逐文件单元覆盖率、[已记录 Session 回放](../../../../snapshots/session/deepseek-messages-replay/snapshot.yml)与[未知回放版本](../../../../snapshots/session/deepseek-messages-degraded-replay/snapshot.yml),Web Messages Session 回放,以及凭证控制的文本、思考、工具续接、图片和取消请求。真实网关检查证明与已配置网关的兼容性,不能证明与所有 Anthropic 代理兼容。 diff --git a/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.i18n.yaml b/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.i18n.yaml index a42e0ee0bd..93a617d08b 100644 --- a/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.i18n.yaml @@ -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-08-composer-menu-sections-and-localized-rows.md -2026-09-08-composer-menu-sections-and-localized-rows.md: 3cccb3c038ae4c358860f09533e966e9121b987e -2026-09-08-composer-menu-sections-and-localized-rows.zh.md: 31c879b17ac6788bfade50a3245903c3e2e56224 +2026-09-08-composer-menu-sections-and-localized-rows.md: 50b97ce2c7e59d53bf5607fef74a799a359d27c8 +2026-09-08-composer-menu-sections-and-localized-rows.zh.md: 92bcb04bfe9b4f7b5581c80d7c0b843f98096e52 diff --git a/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.md b/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.md index 3cccb3c038..50b97ce2c7 100644 --- a/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.md +++ b/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.md @@ -18,9 +18,9 @@ A menu pick fills the locale's claim token: under Chinese, picking Plan fills `/ The File row is an `action` contribution: a bare invocation consumes the trigger token and runs a client callback without submitting a message. Conversation owns that registration, its live availability, and the hidden file input. The menu replaces the separate paperclip button; the `+` button's accessible name and tooltip read "Add files or run commands". -`ui-input-trigger` renders the new row anatomy: `InputTriggerCandidate.label` is the title and a second search key of the shared `rankByName`, the name renders as a trailing alias when the label differs from it, `icon` accepts an icon component beside the reference glyph tokens, and the description is right-aligned. `ui-primitives` gains the Plan glyph from the design doc, a static ring for Compact, and the permission shield contour. +`ui-input-trigger` renders the new row anatomy: `InputTriggerCandidate.label` is the title and a second search key of the shared `rankByName`, the name renders as a trailing alias when the label differs from it, `icon` accepts an icon component beside the reference glyph tokens, and the description is right-aligned. Aliases and descriptions use a font one pixel below the primary labels. `ui-primitives` gains the Plan glyph from the design doc, a static ring for Compact, and the permission shield contour. -The menu uses a 400 px border-box height cap, which fits both headings and the eight built-in rows before the viewport clamp reduces it. A real overflow keeps a 10 px draggable WebKit rail around a 4 px visible thumb, insets the track from the rounded ends, and shows a bottom fade until the viewport reaches the final row; Firefox keeps its standard thin scrollbar. +The menu uses a 400 px border-box height cap, which fits both headings and the eight built-in rows before the viewport clamp reduces it. A real overflow keeps a 6 px draggable WebKit rail around a 2 px visible thumb, insets the track from the rounded ends, and shows a bottom fade until the viewport reaches the final row; Firefox keeps its standard thin scrollbar. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.zh.md b/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.zh.md index 31c879b17a..92bcb04bfe 100644 --- a/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.zh.md +++ b/.agents/notes/implemented/feature/2026-09-08-composer-menu-sections-and-localized-rows.zh.md @@ -18,9 +18,9 @@ composer 的 `+` 按钮与键入的 `/` 按宿主注册顺序把每个命令列 「文件」行是一个 `action` 贡献项:裸调用消费触发 token 后运行客户端回调,不提交消息。Conversation 负责该注册、实时可用性和隐藏文件输入框。菜单取代单独的回形针按钮,`+` 按钮的无障碍名称与提示为「添加文件或调用指令」。 -`ui-input-trigger` 渲染新的行结构:`InputTriggerCandidate.label` 是标题,也是共享 `rankByName` 的第二个搜索键;label 与名字不同时名字渲染为尾随别名;`icon` 在引用图标 token 之外接受图标组件;说明右对齐。`ui-primitives` 新增设计稿给出的计划图标、压缩用的静态环形,以及权限盾形轮廓。 +`ui-input-trigger` 渲染新的行结构:`InputTriggerCandidate.label` 是标题,也是共享 `rankByName` 的第二个搜索键;label 与名字不同时名字渲染为尾随别名;`icon` 在引用图标 token 之外接受图标组件;说明右对齐。别名与说明使用比主标签小 1px 的字号。`ui-primitives` 新增设计稿给出的计划图标、压缩用的静态环形,以及权限盾形轮廓。 -菜单采用 400 px 的 border-box 高度上限,在视口限制缩小高度前可容纳两个小节标题与八个内置入口。内容确实溢出时,WebKit 系浏览器用 10 px 的可拖动区域承载 4 px 的可见滑块,轨道避开圆角两端,视口抵达最后一行前显示底部渐隐;Firefox 保留标准细滚动条。 +菜单采用 400 px 的 border-box 高度上限,在视口限制缩小高度前可容纳两个小节标题与八个内置入口。内容确实溢出时,WebKit 系浏览器用 6 px 的可拖动区域承载 2 px 的可见滑块,轨道避开圆角两端,视口抵达最后一行前显示底部渐隐;Firefox 保留标准细滚动条。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.i18n.yaml b/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.i18n.yaml index bf3876efca..35f8f4dc56 100644 --- a/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.i18n.yaml @@ -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-08-shared-file-type-icons.md -2026-09-08-shared-file-type-icons.md: 1e3fe7267961a8525bbcdf946eacaa933bcf3e4f -2026-09-08-shared-file-type-icons.zh.md: fd1f38ebb33f80c73d17cc48f6f9640c309f97f2 +2026-09-08-shared-file-type-icons.md: 5dbe0ade14c15e1d509f56eba6e0266c61089c46 +2026-09-08-shared-file-type-icons.zh.md: 9dbccdf10f188b30dcbbbe11d45b238af42ca112 diff --git a/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.md b/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.md index 1e3fe72679..5dbe0ade14 100644 --- a/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.md +++ b/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.md @@ -6,25 +6,25 @@ English | [中文](2026-09-08-shared-file-type-icons.zh.md) ## Problem -Client feature plugins can share React components only through `@deepseek-ai/dsh-client-ui-primitives`, but file cards had no shared file-type presentation. `LinkIcon` owned the only extension table and deliberately collapsed paths into six link categories, while attachment cards, sent-message attachments, queued files, and workspace file rows used one generic document glyph. Two of those consumers also carried separate extension-display helpers. Adding a precise file icon anywhere else would have required another extension table or a runtime import between feature plugins. +Client feature plugins can share React components only through `@deepseek-ai/dsh-client-ui-primitives`, but file cards had no shared file-type presentation. The package-owned classifier provides the only extension table and deliberately folds detailed file types into six link categories for `LinkIconMedium`, while attachment cards, sent-message attachments, queued files, and workspace file rows use detailed file glyphs. Adding a precise file icon anywhere else would require another extension table or a runtime import between feature plugins. ## Decision `ui-primitives` owns one Cordis-free file classification and rendering API. `fileExtension(path)` applies the shared basename and final-dot semantics to either path separator. `classifyFileType(path)` matches without case sensitivity and returns the closed `FileType` union: the traditional `code`, `excel`, `folder`, `html`, `image`, `markdown`, `other`, `pdf`, `ppt`, `video`, and `word` categories plus the detailed `CodeFileType` categories rendered by `CodeFileIcon`. Exact filenames, filename prefixes, filename suffixes, optional project context, and extensions run in that order. The path classifier returns every member except `folder`; callers use the explicit `FileTypeIcon` `kind` override when they know an entry is a directory. Unknown extensions, missing extensions, and trailing dots resolve to `other`, except the shared table recognizes named files such as `Dockerfile`, `Makefile`, `package.json`, `.gitignore`, `README`, and `CHANGELOG`. -`FileTypeIcon` accepts a path, shared `IconProps`, the explicit `kind`, and an optional project-file snapshot. Traditional file types render the supplied 28px document and folder contours as inline SVG. Excel, Markdown, PDF, PPT, and Word foreground marks scale to 122% around their visual center; the remaining marked traditional glyphs use 112%, while the file body and folded corner retain their source geometry and the generic file has no invented center mark. The sheet is a solid category color, the foreground mark and ordinary folded corner are white, and the generic file has a darker grey corner. CSS assigns the supplied category palette through static design tokens: DeepSeek blue for code/HTML/Markdown, the lighter DeepSeek blue for Word, green for Excel, two amber steps for folder/PPT, red for PDF, and neutral grey for unknown files. Image and video share the supplied violet through a component-local variable because the design platform has no matching violet token. A caller may override a traditional sheet through `--dsh-file-type-icon-color`. +`FileTypeIcon` accepts a path, shared `IconProps`, the explicit `kind`, and an optional project-file snapshot. Traditional file types render the supplied 28px document and folder contours as inline SVG. The spreadsheet glyph uses the supplied unscaled 1.2px table grid; Markdown, PDF, PPT, and Word foreground marks scale to 122% around their visual center, and the remaining marked traditional glyphs use 112%. The file body and folded corner retain their source geometry, and the generic file has no invented center mark. The sheet is a solid category color, the foreground mark and ordinary folded corner are white, and the generic file has a darker grey corner. CSS assigns the supplied category palette through static design tokens: DeepSeek blue for code/HTML/Markdown, the lighter DeepSeek blue for Word, green for spreadsheets, two amber steps for folder/PPT, red for PDF, and neutral grey for unknown files. Image and video share the supplied violet through a component-local variable because the design platform has no matching violet token. A caller may override a traditional sheet through `--dsh-file-type-icon-color`. -Recognized code and configuration files render the corresponding 20px square artwork scaled to the requested icon size. The embedded static table contains exactly the 48 established `CodeFileType` entries; archive-only artwork does not add a category, and an adjacent manifest records the responsible design owner and source digests. Tests reject scripts, event attributes, external references, and duplicate ids in that table. `CodeFileIcon` replaces local SVG ids with a per-component prefix before insertion so repeated gradients and clip paths remain independent. These technology marks retain their embedded multicolor fills and are the explicit exception to the ordinary current-color icon rule. The map selects React before TypeScript/JavaScript, Angular filename suffixes before their base extension, Docker/Node/Git/Make/CMake by filename rules, and Flutter only when the optional project snapshot contains a `pubspec.yaml` whose text includes `flutter:`. Markdown and SVG remain owned by the traditional Markdown and image categories. CSV and TSV use the code glyph in file cards, rows, and preview titles; their clickable links also use code. Both `.env` and names ending in `.env` use the environment glyph. Every traditional and technology SVG is `aria-hidden`, and the card, row, or button that owns the file identity supplies the accessible name. +Recognized code and configuration files render the corresponding 20px square artwork scaled to the requested icon size. The embedded static table contains exactly the 48 established `CodeFileType` entries; archive-only artwork does not add a category, and an adjacent manifest records the responsible design owner and source digests. Tests reject scripts, event attributes, external references, and duplicate ids in that table. `CodeFileIcon` replaces local SVG ids with a per-component prefix before insertion so repeated gradients and clip paths remain independent. These technology marks retain their embedded multicolor fills and are the explicit exception to the ordinary current-color icon rule. The map selects React before TypeScript/JavaScript, Angular filename suffixes before their base extension, Docker/Node/Git/Make/CMake by filename rules, and Flutter only when the optional project snapshot contains a `pubspec.yaml` whose text includes `flutter:`. Markdown and SVG remain owned by the traditional Markdown and image categories. CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers use the spreadsheet glyph in file cards, rows, and preview titles; their clickable links use the document category. Both `.env` and names ending in `.env` use the environment glyph. Every traditional and technology SVG is `aria-hidden`, and the card, row, or button that owns the file identity supplies the accessible name. -`LinkIcon` delegates extension classification to `classifyFileType` and folds the detailed result into its existing link vocabulary: code and HTML use `code`, images use `image`, PDF/Word/Excel/PPT use `document`, and Markdown/video/unknown files use `other`. Extensionless names remain `other` in link contexts, so the 14px clickable-link appearance defined by the [clickable-link decision](2026-09-04-web-clickable-link-styles.md) does not change. +`LinkIconMedium` delegates extension classification to `classifyFileType` and folds the detailed result into its existing link vocabulary: code and HTML use `code`, images use `image`, PDF/Word/Excel/PPT use `document`, and Markdown/video/unknown files use `other`. Extensionless names remain `other` in link contexts, so the 14px clickable-link appearance defined by the [link styling rules](../../../../docs/web-styling.md) does not change. -Attachment upload cards, sent-message file cards, queued-file rows, and workspace file rows render `FileTypeIcon`. The Files tab title renders its explicit `folder` kind at 16px. Explicit delivery cards use `FileTypeIcon` at 20px and `fileExtension` for their fallback metadata. The two metadata rows use `fileExtension` rather than local parsers; a leading-dot basename such as `.env` therefore displays `ENV`, while an absent or trailing suffix displays no extension label. Image content continues to render as a preview rather than a file-type glyph, and produced-file links and Markdown file mentions continue to use `LinkIcon` because they are link surfaces. +Attachment upload cards, sent-message file cards, queued-file rows, and workspace file rows render `FileTypeIcon`. The Files tab title renders its explicit `folder` kind at 16px. Explicit delivery cards use `FileTypeIcon` at 20px and `fileExtension` for their fallback metadata. The two metadata rows use `fileExtension` rather than local parsers; a leading-dot basename such as `.env` therefore displays `ENV`, while an absent or trailing suffix displays no extension label. Image content continues to render as a preview rather than a file-type glyph, and produced-file links and Markdown file mentions continue to use `LinkIconMedium` because they are link surfaces. ## Alternatives considered -**Use `LinkIcon` for every file surface.** Rejected. Its six categories and 14px outline drawings communicate link destinations at text size; a 28px file card has room for the supplied HTML, Markdown, PDF, Word, Excel, PPT, and video identities. +**Use `LinkIconMedium` for every file surface.** Rejected. Its six categories and 14px outline drawings communicate link destinations at text size; a 28px file card has room for the supplied HTML, Markdown, PDF, Word, Excel, PPT, and video identities. -**Keep a second extension table beside `LinkIcon`.** Rejected. The same path could drift to different categories as either list grows. One detailed table plus an explicit detailed-to-link adapter preserves both consumers' semantics. +**Keep a second extension table beside `LinkIconMedium`.** Rejected. The same path could drift to different categories as either list grows. One detailed table plus an explicit detailed-to-link adapter preserves both consumers' semantics. **Put the supplied traditional-file colors directly in each SVG path.** Rejected. Traditional SVG geometry stays reusable and follows the icon set's `currentColor` rule; the component stylesheet owns the default category palette, and render sites retain one CSS-variable override instead of rewriting path fills. The technology artwork is exempt because its embedded multicolor marks identify the language or tool rather than decorating a generic file silhouette. @@ -32,7 +32,7 @@ Attachment upload cards, sent-message file cards, queued-file rows, and workspac ## Testing -The `ui-primitives` specs cover case-insensitive suffixes, both path separators, leading-dot files, missing and trailing suffixes, common named files, unknown fallback, all detailed code mappings, rule priority, Flutter context, the exact 48-key artwork set, rejected archive-only categories, static-markup safety, instance-safe SVG ids and references, `aria-hidden`, sizing/class forwarding, distinct artwork, the 112% and 122% foreground-mark transforms, and the traditional solid-sheet/contrast-mark layers without literal SVG colors. A stylesheet spec pins every traditional category-to-color mapping, the caller override, and the local violet value. The existing `LinkIcon` classification table pins its coarse output, including `Makefile` remaining `other`. Attachment, chat, queue, and sidebar component suites exercise the migrated render paths; their accessibility output does not change because the glyphs remain decorative. +The `ui-primitives` specs cover case-insensitive suffixes, both path separators, leading-dot files, missing and trailing suffixes, common named files, unknown fallback, all detailed code mappings, rule priority, Flutter context, the exact 48-key artwork set, rejected archive-only categories, static-markup safety, instance-safe SVG ids and references, `aria-hidden`, sizing/class forwarding, distinct artwork, the supplied spreadsheet grid, the 112% and 122% foreground-mark transforms, and the traditional solid-sheet/contrast-mark layers without literal SVG colors. A stylesheet spec pins every traditional category-to-color mapping, the caller override, and the local violet value. The `LinkIconMedium` classification table pins its coarse output, including `Makefile` remaining `other`. Attachment, chat, queue, and sidebar component suites exercise the migrated render paths; their accessibility output does not change because the glyphs remain decorative. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.zh.md b/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.zh.md index fd1f38ebb3..9dbccdf10f 100644 --- a/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.zh.md +++ b/.agents/notes/implemented/feature/2026-09-08-shared-file-type-icons.zh.md @@ -6,25 +6,25 @@ Status: implemented ## 问题 -客户端功能插件只能通过 `@deepseek-ai/dsh-client-ui-primitives` 共享 React 组件,但文件卡片没有共享的文件类型呈现。`LinkIcon` 拥有唯一一份扩展名表,并有意把路径折叠为六种链接类别;附件卡片、已发送消息附件、排队文件和工作区文件行则统一使用一个通用文档图形。其中两个消费方还各自带着一份扩展名展示辅助函数。在其他位置增加精细文件图标,需要再复制一份扩展名表或让功能插件之间产生运行时 import。 +客户端功能插件只能通过 `@deepseek-ai/dsh-client-ui-primitives` 共享 React 组件,但文件卡片没有共享的文件类型呈现。本包拥有唯一一份扩展名分类表,并为 `LinkIconMedium` 将细分文件类型有意折叠为六种链接类别;附件卡片、已发送消息附件、排队文件和工作区文件行使用细分文件图形。在其他位置增加精细文件图标,需要再复制一份扩展名表或让功能插件之间产生运行时 import。 ## 决策 `ui-primitives` 统一拥有一套 Cordis-free 的文件分类与渲染 API。`fileExtension(path)` 对两种路径分隔符采用共享的 basename 与最终点号语义。`classifyFileType(path)` 不区分大小写,并返回闭合的 `FileType` 联合:传统的 `code`、`excel`、`folder`、`html`、`image`、`markdown`、`other`、`pdf`、`ppt`、`video`、`word` 类别,以及由 `CodeFileIcon` 渲染的细分 `CodeFileType` 集合。解析按完整文件名、文件名前缀、文件名后缀、可选项目上下文、扩展名的顺序执行。路径分类器返回 `folder` 之外的全部成员;调用方确认条目是目录时,通过 `FileTypeIcon` 的显式 `kind` 覆盖指定目录。未知扩展名、无扩展名和末尾点号回退到 `other`,但共享表识别 `Dockerfile`、`Makefile`、`package.json`、`.gitignore`、`README`、`CHANGELOG` 等具名文件。 -`FileTypeIcon` 接受路径、共享 `IconProps`、显式 `kind`和可选的项目文件快照。传统文件类型把所提供的 28px 文档与文件夹轮廓渲染为 inline SVG。Excel、Markdown、PDF、PPT、Word 的前景标记围绕自身视觉中心缩放至 122%,其余带标记的传统图形使用 112%;文件底板与折角保持源图几何,通用文件不凭空增加中心标记。底板使用实色分类颜色,前景标记与普通折角使用白色,通用文件使用较深的灰色折角。CSS 通过静态设计 token 分配所提供的分类调色板:code/HTML/Markdown 使用 DeepSeek 蓝,Word 使用较浅的 DeepSeek 蓝,Excel 使用绿色,folder/PPT 使用两档琥珀色,PDF 使用红色,未知文件使用中性灰。image 与 video 通过组件本地变量共用所提供的紫色,因为设计平台没有匹配的紫色 token。调用方可通过 `--dsh-file-type-icon-color` 覆盖传统底板颜色。 +`FileTypeIcon` 接受路径、共享 `IconProps`、显式 `kind`和可选的项目文件快照。传统文件类型把所提供的 28px 文档与文件夹轮廓渲染为 inline SVG。表格图形使用所提供的未缩放 1.2px 表格网格;Markdown、PDF、PPT、Word 的前景标记围绕自身视觉中心缩放至 122%,其余带标记的传统图形使用 112%。文件底板与折角保持源图几何,通用文件不凭空增加中心标记。底板使用实色分类颜色,前景标记与普通折角使用白色,通用文件使用较深的灰色折角。CSS 通过静态设计 token 分配所提供的分类调色板:code/HTML/Markdown 使用 DeepSeek 蓝,Word 使用较浅的 DeepSeek 蓝,表格使用绿色,folder/PPT 使用两档琥珀色,PDF 使用红色,未知文件使用中性灰。image 与 video 通过组件本地变量共用所提供的紫色,因为设计平台没有匹配的紫色 token。调用方可通过 `--dsh-file-type-icon-color` 覆盖传统底板颜色。 -已识别的代码与配置文件把对应的 20px 方形图稿缩放到请求的图标尺寸。内嵌静态表只包含现有 48 个 `CodeFileType` 条目;资源包中额外的图稿不会新增类别,相邻 manifest 记录负责的设计归属方与来源摘要。测试会拒绝该表中的脚本、事件属性、外部引用与重复 id。`CodeFileIcon` 在插入前为本地 SVG id 加上组件实例前缀,使重复渐变与裁剪路径互不干扰。这些技术标记保留自身内嵌的多色填充,是普通 current-color 图标规则的明确例外。映射让 React 优先于 TypeScript/JavaScript、Angular 文件名后缀优先于基础扩展名,并按文件名识别 Docker/Node/Git/Make/CMake;只有可选项目快照包含内容带 `flutter:` 的 `pubspec.yaml` 时才选择 Flutter。Markdown 与 SVG 仍由传统 Markdown 和图片类别拥有。CSV 和 TSV 在文件卡片、文件行及预览标题中使用 code 图标,其可点击链接也使用 code。`.env` 和以 `.env` 结尾的文件名均使用环境配置图标。所有传统与技术 SVG 都是 `aria-hidden` 的,拥有文件身份的卡片、行或按钮提供无障碍名称。 +已识别的代码与配置文件把对应的 20px 方形图稿缩放到请求的图标尺寸。内嵌静态表只包含现有 48 个 `CodeFileType` 条目;资源包中额外的图稿不会新增类别,相邻 manifest 记录负责的设计归属方与来源摘要。测试会拒绝该表中的脚本、事件属性、外部引用与重复 id。`CodeFileIcon` 在插入前为本地 SVG id 加上组件实例前缀,使重复渐变与裁剪路径互不干扰。这些技术标记保留自身内嵌的多色填充,是普通 current-color 图标规则的明确例外。映射让 React 优先于 TypeScript/JavaScript、Angular 文件名后缀优先于基础扩展名,并按文件名识别 Docker/Node/Git/Make/CMake;只有可选项目快照包含内容带 `flutter:` 的 `pubspec.yaml` 时才选择 Flutter。Markdown 与 SVG 仍由传统 Markdown 和图片类别拥有。CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers 在文件卡片、文件行及预览标题中使用表格图标,其可点击链接使用文档类别。`.env` 和以 `.env` 结尾的文件名均使用环境配置图标。所有传统与技术 SVG 都是 `aria-hidden` 的,拥有文件身份的卡片、行或按钮提供无障碍名称。 -`LinkIcon` 委托 `classifyFileType` 做扩展名分类,再把精细结果折叠进原有链接词汇:code 与 HTML 使用 `code`,图片使用 `image`,PDF/Word/Excel/PPT 使用 `document`,Markdown、video 与未知文件使用 `other`。无扩展名文件在链接语境中仍是 `other`,因此[可点击链接决策](2026-09-04-web-clickable-link-styles.zh.md)定义的 14px 外观不变。 +`LinkIconMedium` 委托 `classifyFileType` 做扩展名分类,再把精细结果折叠进原有链接词汇:code 与 HTML 使用 `code`,图片使用 `image`,PDF/Word/Excel/PPT 使用 `document`,Markdown、video 与未知文件使用 `other`。无扩展名文件在链接语境中仍是 `other`,因此[链接样式规则](../../../../docs/web-styling.zh.md)定义的 14px 外观不变。 -附件上传卡片、已发送消息文件卡片、排队文件行和工作区文件行渲染 `FileTypeIcon`。Files 标签页标题使用显式的 `folder` 类别,尺寸为 16px。显式交付卡片使用 20px 的 `FileTypeIcon`,并通过 `fileExtension` 提供默认元数据。两处元数据行使用 `fileExtension`,不再保留本地解析器;`.env` 这样的前导点 basename 会显示 `ENV`,无后缀或末尾点号则不显示扩展名 label。图片内容继续渲染为预览而不是文件类型图形,产物文件链接与 Markdown 文件提及继续使用 `LinkIcon`,因为它们属于链接表面。 +附件上传卡片、已发送消息文件卡片、排队文件行和工作区文件行渲染 `FileTypeIcon`。Files 标签页标题使用显式的 `folder` 类别,尺寸为 16px。显式交付卡片使用 20px 的 `FileTypeIcon`,并通过 `fileExtension` 提供默认元数据。两处元数据行统一使用 `fileExtension`;`.env` 这样的前导点 basename 会显示 `ENV`,无后缀或末尾点号则不显示扩展名 label。图片内容继续渲染为预览而不是文件类型图形,产物文件链接与 Markdown 文件提及继续使用 `LinkIconMedium`,因为它们属于链接表面。 ## 备选方案 -**所有文件表面都使用 `LinkIcon`。** 否决。它的六种类别和 14px outline 图形用于在文字尺寸下表达链接目标;28px 文件卡片有空间表达所提供的 HTML、Markdown、PDF、Word、Excel、PPT 与 video 身份。 +**所有文件表面都使用 `LinkIconMedium`。** 否决。它的六种类别和 14px outline 图形用于在文字尺寸下表达链接目标;28px 文件卡片有空间表达所提供的 HTML、Markdown、PDF、Word、Excel、PPT 与 video 身份。 -**在 `LinkIcon` 旁保留第二份扩展名表。** 否决。任一列表增长后,同一路径可能漂移到不同类别。一份精细表加一个显式的精细到链接适配,能同时保住两类消费方的语义。 +**在 `LinkIconMedium` 旁保留第二份扩展名表。** 否决。任一列表增长后,同一路径可能漂移到不同类别。一份精细表加一个显式的精细到链接适配,能同时保住两类消费方的语义。 **把所提供的传统文件颜色直接写入每一条 SVG path。** 否决。传统 SVG 几何保持可复用,并遵守图标集的 `currentColor` 规则;组件样式表拥有默认分类调色板,渲染点只需一个 CSS 变量即可覆盖颜色,不需要重写 path 填充。技术图稿是例外,因为其内嵌的多色标记用于识别语言或工具,而不是装饰通用文件轮廓。 @@ -32,7 +32,7 @@ Status: implemented ## 测试 -`ui-primitives` 测试覆盖不区分大小写的后缀、两种路径分隔符、前导点文件、无后缀与末尾点号、常见具名文件、未知回退、全部细分代码映射、规则优先级、Flutter 上下文、精确的 48 键图稿集、被拒绝的资源包额外类别、静态 markup 安全性、实例安全的 SVG id 与引用、`aria-hidden`、尺寸/class 转发、不同图稿、112% 与 122% 前景标记变换,以及不含 SVG 字面颜色的传统实色底板/对比标记层。样式表测试钉住每一项传统类别到颜色的映射、调用方覆盖变量与本地紫色值。既有 `LinkIcon` 分类表钉住它的粗粒度输出,包括 `Makefile` 仍为 `other`。附件、聊天、队列和侧边栏组件测试覆盖迁移后的渲染路径;由于图形仍是装饰性的,其无障碍输出不变。 +`ui-primitives` 测试覆盖不区分大小写的后缀、两种路径分隔符、前导点文件、无后缀与末尾点号、常见具名文件、未知回退、全部细分代码映射、规则优先级、Flutter 上下文、精确的 48 键图稿集、被拒绝的资源包额外类别、静态 markup 安全性、实例安全的 SVG id 与引用、`aria-hidden`、尺寸/class 转发、不同图稿、所提供的表格网格、112% 与 122% 前景标记变换,以及不含 SVG 字面颜色的传统实色底板/对比标记层。样式表测试钉住每一项传统类别到颜色的映射、调用方覆盖变量与本地紫色值。`LinkIconMedium` 分类表钉住它的粗粒度输出,包括 `Makefile` 仍为 `other`。附件、聊天、队列和侧边栏组件测试覆盖迁移后的渲染路径;由于图形仍是装饰性的,其无障碍输出不变。 ## 后果 diff --git a/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.md b/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.md deleted file mode 100644 index f0f5cb478d..0000000000 --- a/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: Turn duration labels gain an hour unit - -Status: implemented - -English | [中文](2026-09-09-turn-duration-hour-unit.zh.md) - -## Problem - -The Web chat's turn duration labels counted minutes without bound. `formatRunDuration` in [message-chrome.ts](../../../../packages/client/ui-chat/src/client/chat/message-chrome.ts) split elapsed milliseconds into seconds and minutes only, so a turn that ran for 90 minutes read `90分05秒` / `90m 05s` in all three places sharing the formatter: the `Deep diving...` running clock, the settled `Ran for {duration}` footer, and the turn-time dialog's total. The archived [turn run time decision](../../archived/feature/2026-08-03-web-turn-run-time.md) fixed the clock's anchor and the shared whole-second floor; it left the formatter at two units, which stops reading correctly once a turn crosses an hour. - -## Decision - -`formatRunDuration` carries an hour branch: elapsed time at or above 3600 seconds renders through `duration.hours` with zero-padded minutes and seconds — `1小时05分03秒` / `1h 05m 03s` — while everything below an hour keeps the existing second and minute branches unchanged. Hours appear only at or above 3600 seconds, so 3599 seconds still reads `59分59秒` and `60分00秒` never appears. Seconds are retained rather than dropped once hours appear, because the running clock ticks every second and a `1小时05分` label would sit still for a minute at a time. Negatives still clamp to zero and partial seconds still floor. `duration.hours` joins both dictionaries in [locale.ts](../../../../packages/client/ui-chat/src/client/locale.ts), and `RunDurationTranslate` widens to three keys. - -The change is confined to the turn formatter. `StatsPills.formatDuration` — the session-wide aggregate pill reading `45.2s` / `2m42s` — keeps its own two-unit format. - -## Alternatives considered - -**Pair hours with minutes only.** Dropping seconds at the hour boundary matches the `ui-jobs` job-duration format and keeps the label short. It loses the second-level figure from the settled footer, and it makes the live clock look frozen: the label would change once a minute while the turn is still running. - -**Three units while running, two once settled.** Rejected because both readings come from one function by design — the archived decision pins that — so the same turn would report different precision before and after it settles. - -**Change the aggregate pill in the same change.** `StatsPills.formatDuration` measures session-wide aggregates — LLM time, tool time, and average TTFT — from projections rather than one turn's boundaries. It can exceed an hour too, but folding it in would mix two independent formatters and their tests into one change. - -**Leave minutes unbounded.** `90分05秒` is technically correct and costs nothing to keep, but it is the reading that prompted this change and grows harder to parse the longer a turn runs. - -## Consequences - -Long turns now read in hours without new session events or new timing state; the labels remain derived from the logged `turn/start` and `turn/end` boundaries and keep the 15-second clock delay. The unit spec covers the 3599-second and 3600-second boundary plus the English template. No recorded-session snapshot changes: every shipped `Deep diving...` expectation is captured before the clock appears. diff --git a/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.zh.md b/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.zh.md deleted file mode 100644 index c9c94eef2a..0000000000 --- a/.agents/notes/implemented/feature/2026-09-09-turn-duration-hour-unit.zh.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: 回合耗时标签增加小时单位 - -Status: implemented - -[English](2026-09-09-turn-duration-hour-unit.md) | 中文 - -## 问题 - -Web 聊天的回合耗时标签分钟数无界增长。[message-chrome.ts](../../../../packages/client/ui-chat/src/client/chat/message-chrome.ts) 里的 `formatRunDuration` 只把毫秒拆成秒和分两级,于是一个跑了 90 分钟的回合在共用一个格式化器的三处都读作 `90分05秒` / `90m 05s`:`Deep diving...` 后的实时计时、收尾的 `Ran for {duration}` 页脚,以及耗时详情弹窗里的总用时。已归档的[回合运行时长决策](../../archived/feature/2026-08-03-web-turn-run-time.md)确定了计时锚点和共用的整秒向下取整;它把格式化器留在两级单位,一旦回合跨过一小时就不再正确。 - -## 决定 - -`formatRunDuration` 增加小时分支:总时长达到或超过 3600 秒时经 `duration.hours` 渲染,分和秒补零——`1小时05分03秒` / `1h 05m 03s`——不足一小时的时长保持原有的秒、分两级不变。小时只在达到或超过 3600 秒时出现,因此 3599 秒仍读作 `59分59秒`,不会出现 `60分00秒`。出现小时后保留秒而不丢弃,因为实时计时每秒都在走,`1小时05分` 这样的标签会整整一分钟静止不动。负值仍然钳到 0,不足一秒仍然向下取整。[locale.ts](../../../../packages/client/ui-chat/src/client/locale.ts) 的两套字典都新增 `duration.hours`,`RunDurationTranslate` 扩为三个键。 - -改动只限于回合计时器。`StatsPills.formatDuration`——会话级聚合胶囊,读作 `45.2s` / `2m42s`——保留自己的两级格式。 - -## 考虑过的替代方案 - -**小时只与分配对。** 在小时边界丢掉秒与 `ui-jobs` 的任务时长格式一致,标签也更短。代价是收尾页脚失去秒级数字,并且实时计时看起来像卡住了:回合还在跑,标签却每分钟才变一次。 - -**运行中三单位、收尾两单位。** 拒绝,因为两处读数按设计来自同一个函数——归档决策锁定了这一点——同一个回合会在结束前后报告不同的精度。 - -**在同一次改动里改聚合胶囊。** `StatsPills.formatDuration` 统计的是来自投影的会话级聚合值——LLM 耗时、工具耗时与平均 TTFT——而不是单个回合的边界。它同样可能超过一小时,但把它折进来会让两个独立的格式化器及其测试混进同一次改动。 - -**保留分钟无界。** `90分05秒` 在技术上没错,保留也不花成本,但它正是引发这次改动的读数,而且回合跑得越久越难解析。 - -## 后果 - -长回合现在按小时展示,没有新增会话事件或计时状态;标签仍由日志中的 `turn/start` 与 `turn/end` 边界派生,并保留 15 秒后才出现计时的延迟。单元 spec 覆盖 3599 秒与 3600 秒的边界以及英文模板。录制会话快照没有变化:所有已发布的 `Deep diving...` 期望都在计时出现之前就被捕获。 diff --git a/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.i18n.yaml b/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.i18n.yaml deleted file mode 100644 index 7d4eb35fa1..0000000000 --- a/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.i18n.yaml +++ /dev/null @@ -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/implemented/feature/2026-09-10-connection-indicator-refinements.md -2026-09-10-connection-indicator-refinements.md: 3b6b4c7fd0962edbb87be17c2fa45eded46ee86b -2026-09-10-connection-indicator-refinements.zh.md: 0bd5dc3e0b5b69cf2d9491e2d3ce5711c96898f5 diff --git a/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.md b/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.md deleted file mode 100644 index 3b6b4c7fd0..0000000000 --- a/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: Connection indicator state and interaction refinements - -Status: implemented - -English | [中文](2026-09-10-connection-indicator-refinements.zh.md) - -## Problem - -The sidebar connection pill hid its affordance behind a hover swap: outage and retry-attempt states replaced their label with **Reconnect now** on hover or focus, so every state had to reserve the widest supplied label to keep the control from resizing. A retry that resolved in under a second flickered the connecting pill in and out, and state changes and unmounts jumped with no transition. - -## Decision - -**The disconnected pill shows its action statically.** [ConnectionIndicator.tsx](../../../../packages/client/ui-primitives/src/ConnectionIndicator.tsx) renders a permanent retry glyph (`IconRefreshOutline14`) beside the outage copy (`连接异常,刷新重试` / `Disconnected`; the Chinese copy also names the retry action); clicking the pill still reconnects immediately. The hover label swap and the hidden widest-label size-reservation spans are gone, so the pill sizes to its current label. The connecting state shows a rotating-arc spinner instead of the exclamation glyph. Appearance and removal fade over 150ms — swaps between visible states replace content in place: `EXIT_MS` delays unmount to match the stylesheet's `.leaving` transition, and `prefers-reduced-motion` disables every animation and transition. Chrome settles at 28px height, 8px horizontal padding, 4px icon gap, 13px radius, and a 1px border of the label color at 20% alpha. - -**The shell owns attempt pacing.** [SettingsRoot.tsx](../../../../packages/client/ui-settings-general/src/client/SettingsRoot.tsx) keeps the connecting pill visible for at least `CONNECTING_MIN_VISIBLE_MS` (800ms) so sub-second retries do not flicker; every attempt, manual or automatic, reads the one label `重新连接中` (`connection.connecting`). The two-second recovery confirmation (`RECOVERY_CONFIRMATION_MS`) starts when the recovered pill becomes visible, so a hold that delays its appearance never shortens the confirmation. Both timings are built-in presentation constants of their owners, not configuration. - -## Alternatives considered - -**Animating width changes.** A FLIP-style measured pixel transition (remember the old width, pin it, transition to the new measurement) needs a layout effect and imperative style writes; the fade-only change reads calm enough without them. - -**Swapping to the retry glyph only on hover.** Showing the retry glyph permanently states the affordance without requiring any pointer interaction, matching the static label; a hover cross-fade adds interaction-dependent state and conveys nothing extra. - -**Scaling on enter/exit.** A 0.98 scale beside the opacity fades reads as jitter at 12px text, so only opacity animates. - -**Naming manual and automatic attempts differently.** `ConnectionController.emitState` deduplicates repeated `connecting` states across backoff attempts, so the shell cannot observe attempt boundaries: a shell-held manual-retry flag either flips the label mid-hold or sticks across later automatic attempts. Distinguishing the copy correctly requires the connection layer to expose the attempt origin, which this change does not need — both attempt kinds read the same label. - -## Consequences - -`ConnectionIndicator`'s `reconnectLabel` prop and its size-reservation spans are removed from the pre-stable API; the sole consumer (`ui-settings-general`) is updated in the same change. `settings-root.client.spec.tsx` pins the 800ms hold, the single attempt label held steady through the hold, and the visibility-based confirmation window; `atoms.client.spec.tsx` pins the exit-duration unmount; `lifecycle-chrome.e2e.ts` and its ARIA golden replay the recovery flow in a real browser. Both packages' READMEs restate the interaction. diff --git a/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.zh.md b/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.zh.md deleted file mode 100644 index 0bd5dc3e0b..0000000000 --- a/.agents/notes/implemented/feature/2026-09-10-connection-indicator-refinements.zh.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: 连接指示器状态与交互细化 - -Status: implemented - -[English](2026-09-10-connection-indicator-refinements.md) | 中文 - -## Problem - -侧边栏连接药丸把操作提示藏在悬停切换里:断连与重试状态在悬停或聚焦时把文案替换为**立即重连**,因此每个状态都要为最宽的 label 预留空间以避免控件变形。一次不到一秒就恢复的重试会让连接中药丸闪现闪没,状态切换和消失也没有任何过渡、十分突兀。 - -## Decision - -**断连药丸静态地展示其动作。** [ConnectionIndicator.tsx](../../../../packages/client/ui-primitives/src/ConnectionIndicator.tsx) 在断连文案旁常驻渲染重试图形(`IconRefreshOutline14`),文案为 `连接异常,刷新重试` / `Disconnected`(中文文案同时点明重试动作);点击药丸仍会立即重连。悬停换文案和隐藏的最宽 label 占位 span 全部移除,药丸宽度随当前 label 自适应。连接中状态改用旋转圆弧 spinner 取代感叹号图形。出现与移除以 150ms 淡入淡出——可见状态之间的切换则原地替换内容:`EXIT_MS` 延迟卸载以匹配样式表的 `.leaving` 过渡,`prefers-reduced-motion` 会禁用全部动画与过渡。外观定为高 28px、水平内边距 8px、图标间距 4px、圆角 13px,以及 label 颜色 20% 透明度的 1px 边框。 - -**外壳拥有尝试节奏。** [SettingsRoot.tsx](../../../../packages/client/ui-settings-general/src/client/SettingsRoot.tsx) 让连接中药丸至少可见 `CONNECTING_MIN_VISIBLE_MS`(800ms),亚秒级重试不再闪动;无论手动还是自动,每次尝试都显示同一个文案`重新连接中`(`connection.connecting`)。2 秒恢复确认(`RECOVERY_CONFIRMATION_MS`)从恢复药丸实际可见时起算,驻留推迟其出现也不会缩短确认时长。两个时长是各自持有方的内置展示常量,不是配置。 - -## Alternatives considered - -**给宽度变化加动画。** FLIP 式的像素测量过渡(记住旧宽度、钉住、过渡到新测量值)需要一个 layout effect 和命令式样式写入;纯淡入淡出已足够平静,无需这些。 - -**仅在悬停时切换为重试图形。** 常驻显示重试图形无需任何指针交互就说明了操作,与静态文案一致;悬停交叉渐变引入依赖交互的状态却不传达更多信息。 - -**进出场缩放。** 0.98 的缩放叠加在透明度淡入淡出上,在 12px 文字上读起来像抖动,因此只保留透明度。 - -**手动与自动尝试使用不同命名。** `ConnectionController.emitState` 会去重退避尝试之间重复的 `connecting` 状态,外壳观察不到尝试边界:外壳自持的手动重试标志要么在驻留中途翻转文案,要么在后续自动尝试中一直滞留。要正确区分文案需要连接层暴露尝试来源,而本次变更并不需要——两种尝试显示同一文案。 - -## Consequences - -`ConnectionIndicator` 的 `reconnectLabel` prop 及其占位 span 从 pre-stable API 中移除;唯一消费者(`ui-settings-general`)在同一变更中更新。`settings-root.client.spec.tsx` 固定 800ms 驻留、驻留期间保持不变的单一尝试文案,以及按可见时刻起算的确认窗口;`atoms.client.spec.tsx` 固定退出时长后的卸载;`lifecycle-chrome.e2e.ts` 及其 ARIA golden 在真实浏览器中回放恢复流程。两个包的 README 重述了该交互。 diff --git a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml deleted file mode 100644 index dd2b16d82c..0000000000 --- a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.i18n.yaml +++ /dev/null @@ -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/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md -2026-09-10-guide-start-page-and-stat-pill-refinements.md: 987e46fd597b11046843a943c60187fdb1a769d0 -2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md: 590f18a2963507a8918a0aca30997aac23c6e477 diff --git a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md deleted file mode 100644 index 987e46fd59..0000000000 --- a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: Guide start page and stat pill refinements - -Status: implemented - -English | [中文](2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md) - -## Problem - -The right Sidebar's guide tab was a bare list of entry capsules: no visual anchor above them, a capsule could only say its title, and an entry whose type registered no glyph rendered with no icon at all, so a mixed list read as broken rather than sparse. Separately, the session token-usage dialog under the composer printed a `Cache write 0 tok` row for sessions that never wrote cache. - -## Decision - -**The guide is a compass over self-describing capsules.** This refines the guide contracts in [Right Sidebar tab types and navigation](../architecture/2026-09-05-sidebar-tab-types-and-navigation.md) and [Sidebar text preview and file tree](2026-09-05-sidebar-text-preview-and-file-tree.md). [GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) draws a muted 56px compass hero over the entry capsules and no heading, as a browser start page shows its doors without a caption. [`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) gains an optional thunked `description` — read fresh on every render like `title`, so language changes need no re-registration. A capsule shows its description under the title only while the guide lists at most `MAX_DESCRIBED_ENTRIES` (4) entries; a longer list drops every description to stay light, so a type must stand on its title. The glyph rides the capsule's height: 22px beside a bare title, 26px beside two lines. - -**Icon-less entries fall back to a shipped cube placeholder.** The fallback is decided at the render site (`entry.icon ?? CubeGlyph`), not at registration, so every contributor — builtin or extension — gets it uniformly and a chain replacement of the body replaces the rule with it. `CubeGlyph` lives beside `CompassGlyph` in [GuideTitle.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx): an isometric box in 1.1px straight strokes with rounded joins on `currentColor`, drawn on `--dsw-alias-label-tertiary` — one step quieter than a registered glyph's ink — to mark the slot as unclaimed. The files type registers a description and the shared folder glyph in [definition.tsx](../../../../packages/client/ui-sidebar-files/src/client/definition.tsx). - -**The session usage dialog drops the zero cache-write row.** This refines [Composer session stats](2026-09-07-composer-session-stats-pills.md). [StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) renders the `Cache write` row only when `cacheWriteTokens !== 0`, as the per-turn panel already drops its absent optional fields; the always-present buckets (input, cache read, output) keep their rows. - -## Alternatives considered - -**Register the cube in `ui-primitives`.** Its `icons/index.tsx` is the imported figma `ic_ds_*` set, and the cube has a single consumer; `CompassGlyph` set the precedent of package-local guide glyphs. - -**Default the icon at registration time.** A `?? default` inside the registry would hide the fallback from the body and make "registered no glyph" undetectable, losing the quieter placeholder ink; explicit render-site fallback keeps registrations honest. - -**Show `Cache write 0`.** A session on a provider that never writes cache would carry the row forever; zero here means "not a thing", not a measurement. - -## Consequences - -`description` is new pre-stable registry API; every consumer was updated (the files entry registers one). The 4-entry threshold is a shipped constant of the guide body, not configuration. Guide-body specs cover the placeholder (size and ink), the description threshold on both sides, and the registered-glyph path; chat-stats specs cover the dropped and present cache-write row. The `ui-sidebar-right` and `ui-sidebar-files` READMEs restate the guide rules. diff --git a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md b/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md deleted file mode 100644 index 590f18a296..0000000000 --- a/.agents/notes/implemented/feature/2026-09-10-guide-start-page-and-stat-pill-refinements.zh.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: 引导起始页与统计 pill 的细化 - -Status: implemented - -[English](2026-09-10-guide-start-page-and-stat-pill-refinements.md) | 中文 - -## Problem - -右侧边栏的引导 tab 只是一列光秃秃的入口胶囊:上方没有视觉锚点,胶囊只能显示标题,而类型没注册图标的入口干脆不画图标,混合列表看起来像坏了而不是稀疏。另外,输入框下方的会话 token 用量弹窗会给从未写入缓存的会话打印一行 `Cache write 0 tok`。 - -## Decision - -**引导页是罗盘压着能自我说明的胶囊。** 本决议细化[右侧 Sidebar tab 类型与导航](../architecture/2026-09-05-sidebar-tab-types-and-navigation.zh.md)和 [Sidebar 文本预览与文件树](2026-09-05-sidebar-text-preview-and-file-tree.zh.md)中的引导页约定。[GuideBody.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx) 在入口胶囊上方画一枚弱化的 56px 罗盘,不加标题,如同浏览器起始页不给它的入口配说明文字。[`SidebarRightGuideEntry`](../../../../packages/client/ui-sidebar-right/src/client/tab-registry.ts) 新增可选的 thunk 化 `description`——与 `title` 一样每次渲染重新读取,语言切换无需重新注册。仅当引导页列出的入口不超过 `MAX_DESCRIBED_ENTRIES`(4)个时,胶囊才在标题下显示描述;更长的列表去掉所有描述以保持轻盈,所以类型必须靠标题立得住。图标随胶囊高度变化:单行标题旁 22px,两行旁 26px。 - -**没有图标的入口回退到内置的立方体占位符。** 回退在渲染点决定(`entry.icon ?? CubeGlyph`)而不在注册时,因此每个贡献者——内置或扩展——得到统一的占位符,链式替换 body 时规则随之整体替换。`CubeGlyph` 与 `CompassGlyph` 一起放在 [GuideTitle.tsx](../../../../packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx):一只等距视角的盒子,1.1px 直线描边、圆角拼接、走 `currentColor`,用 `--dsw-alias-label-tertiary` 着色——比注册图标的墨色浅一档——标记这个槽位无人认领。files 类型在 [definition.tsx](../../../../packages/client/ui-sidebar-files/src/client/definition.tsx) 注册了描述和共享的文件夹图标。 - -**会话用量弹窗去掉为零的缓存写入行。** 本决议细化[输入框下的会话统计](2026-09-07-composer-session-stats-pills.zh.md)。[StatsPills.tsx](../../../../packages/client/ui-chat/src/client/chat/StatsPills.tsx) 仅当 `cacheWriteTokens !== 0` 时渲染 `Cache write` 行,与 per-turn 面板去掉缺失可选字段的做法一致;始终存在的桶(输入、缓存读取、输出)保留各自的行。 - -## Alternatives considered - -**把立方体注册进 `ui-primitives`。** 其 `icons/index.tsx` 是导入的 figma `ic_ds_*` 集合,而立方体只有一个消费者;`CompassGlyph` 已开了引导图标包内自持的先例。 - -**在注册时默认图标。** 注册表内部的 `?? default` 会对 body 隐藏回退,让"没注册图标"无法辨认,浅色占位墨色随之丢失;显式的渲染点回退让注册保持诚实。 - -**显示 `Cache write 0`。** 用不写缓存的 provider 的会话会永远挂着这一行;这里的零意味着"没有这回事",不是一次测量。 - -## Consequences - -`description` 是新的 pre-stable 注册表 API;所有消费者已同步更新(files 入口注册了一个)。4 个入口的阈值是引导 body 的内置常量,不是配置。guide-body 用例覆盖占位符(尺寸与墨色)、描述阈值两侧和已注册图标路径;chat-stats 用例覆盖缓存写入行的去除与保留。`ui-sidebar-right` 与 `ui-sidebar-files` 的 README 重述了引导页规则。 diff --git a/.agents/notes/implemented/feature/2026-09-16-known-site-link-marks.md b/.agents/notes/implemented/feature/2026-09-16-known-site-link-marks.md deleted file mode 100644 index 9d029af06f..0000000000 --- a/.agents/notes/implemented/feature/2026-09-16-known-site-link-marks.md +++ /dev/null @@ -1,38 +0,0 @@ -# Agent Note: Known-site link marks - -Status: implemented - -English | [中文](2026-09-16-known-site-link-marks.zh.md) - -## Problem - -Every transcribed anchor led with the same globe, so a transcript full of GitHub, npm, and documentation links gave no signal about where a link goes until the reader parsed its label. The [clickable-link vocabulary](2026-09-04-web-clickable-link-styles.md) reserved that leading seat for one category glyph and left per-site marks as a possible later extension of the `url` category. - -## Decision - -`LinkIcon` takes an optional `href`. For the `url` category, a destination whose host is a well-known site draws that site's own mark; every other `url` destination keeps the globe, and the file categories ignore `href` because their destination is a path, not a site. - -`SiteGlyph.tsx` in ui-primitives owns the mapping: forty host suffixes resolve to thirty-four marks. The developer sites a transcript usually cites: GitHub (`github.com`, `github.io`, `raw.githubusercontent.com`), GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News (`news.ycombinator.com`), YouTube (`youtube.com`, `youtu.be`), X (`x.com`, `twitter.com`), Bilibili, Zhihu, Juejin, CSDN. The mainstream sites a general audience links: search (Google, Baidu, DuckDuckGo), video and audio (TikTok, Netflix, Spotify), social and messaging (Facebook, Instagram, Reddit, Telegram with its `t.me` short links, WhatsApp with `wa.me`, WeChat via `weixin.qq.com`, QQ, Weibo), shopping (Taobao, AliExpress, eBay), reference and community (Quora, V2EX), and Apple. A host matches a suffix when it equals it or is a subdomain of it, and the longest matching suffix wins, so `gist.github.com` and `en.wikipedia.org` need no entry while `weixin.qq.com` keeps WeChat rather than the QQ mark its `qq.com` suffix would also select. Only absolute `http:` and `https:` destinations can match; anything else falls back to the globe. - -The artwork is the [Simple Icons](https://simpleicons.org) set (CC0-1.0) consumed as a devDependency of ui-primitives, so adding a site is one host-map entry and the artwork version comes from the lockfile rather than a copied path. Each mark renders as that set's single path on its 24-unit viewBox, inset to a 28-unit box so it keeps the ~8% margin the 20-unit `ic_ds_*` glyphs draw inside their own box, and takes the link's `currentColor` rather than the brand fill the set records. Every mark is `aria-hidden`; the anchor's own text remains the accessible name. - -Taking `currentColor` is deliberate for a functional link glyph rather than brand presentation: YouTube, X, Instagram, and other brands publish guidelines against recoloring their marks, and the first alternative below weighs the fixed-brand-fill option. CC0-1.0 covers the path data; every mark remains its owner's trademark, and the generated third-party notices cover the package rather than this artwork. - -Two consumers pass their destination: the markdown renderer's `renderSafeLink`, which covers authored anchors, reference links, and URL-promoted inline code, and the web card's source and fetch links. Both already hold the sanitized destination. - -## Alternatives considered - -- **Fetching each site's favicon** from the site itself or a favicon service. This covers arbitrary sites, but rendering a transcript would issue network requests to every linked host, which discloses reading activity and turns a text render into a network operation; it also fails offline and behind a strict `img-src` policy. Rejected: a fixed local vocabulary keeps rendering deterministic and private, and the globe remains the honest fallback. -- **Filling the mark with the site's brand color.** Rejected: the link glyphs are `currentColor`-only so they follow the link alias, dark mode, and hover; a fixed fill would be the first exception and would fight the link's own hover color. -- **Copying the path data into this module.** Rejected: a third-party artwork set that changes upstream is exactly what a pinned dependency tracks; fourteen embedded copies would leave no update path. -- **Adding site values to `LinkIconKind`.** Rejected: the kind is the category the consumer states, while the site is derived from the destination; folding both into one union would make every consumer spell out a site it does not know. -- **A monogram tile for every unmapped host.** Rejected as noise: a generated letter capsule claims a site identity without carrying one, and 14px leaves no room for readable initials. - -## Consequences - -- Adding a site needs a host-map entry in `SiteGlyph.tsx` and, to be checked, a URL in the `link-icon` spec: that spec lists a representative URL per mapped site, requires thirty-four distinct marks across them, and pins the GitHub, npm, YouTube, Wikipedia, X, Telegram, and WhatsApp aliases. A dropped or duplicated entry fails the count, while a site added to the map alone stays uncovered by the spec. -- The vocabulary stays bounded by category rather than by demand: each site costs one map entry, one upstream mark, and one spec URL, and the thirty-four marks add roughly 23 KB of path data to the source, which the shell build reduces to a few KB in the entry chunk after tree-shaking, minification, and compression. -- `simple-icons` enters the browser build graph as a development dependency of ui-primitives; the shell build tree-shakes the imported marks instead of the complete set. -- Unknown hosts, non-http schemes, unparseable destinations, and file categories keep the existing globe or category glyph; the `mailto` link in a transcript still shows the globe. -- The vocabulary is deliberately finite. Sites such as `example.com` never get a mark from this mechanism; recognizing them would require the network fetching the alternatives reject. -- Coverage: the LinkIcon spec covers the aliases, the fallback branches, and the sizing seat; the markdown spec pins one known and one unknown anchor; the web-card spec pins a source and a fetch link against the same marks. diff --git a/.agents/notes/implemented/feature/2026-09-16-known-site-link-marks.zh.md b/.agents/notes/implemented/feature/2026-09-16-known-site-link-marks.zh.md deleted file mode 100644 index 2c7630d122..0000000000 --- a/.agents/notes/implemented/feature/2026-09-16-known-site-link-marks.zh.md +++ /dev/null @@ -1,38 +0,0 @@ -# Agent Note: 已知站点链接标记 - -Status: implemented - -[English](2026-09-16-known-site-link-marks.md) | 中文 - -## Problem - -转写内容中的每个锚点都以同一个地球图形开头,因此满是 GitHub、npm 与文档链接的对话在读者解析标签之前,无法从图形看出链接去向。[可点击链接词汇](2026-09-04-web-clickable-link-styles.zh.md) 把该前置位置留给一个分类图形,并把按站点区分的标记留作 `url` 类别日后可能的扩展。 - -## Decision - -`LinkIcon` 接受可选的 `href`。对于 `url` 类别,目的地主机属于已知站点时改画该站点自己的标记;其余 `url` 目的地仍使用地球,文件类别忽略 `href`,因为它们的目的地是路径而不是站点。 - -ui-primitives 的 `SiteGlyph.tsx` 持有该映射:四十个主机后缀解析为三十四个标记。转写内容常引用的开发者站点:GitHub(`github.com`、`github.io`、`raw.githubusercontent.com`)、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News(`news.ycombinator.com`)、YouTube(`youtube.com`、`youtu.be`)、X(`x.com`、`twitter.com`)、Bilibili、知乎、掘金与 CSDN。普通用户常访问的主流站点:搜索(Google、百度、DuckDuckGo)、视频与音频(TikTok、Netflix、Spotify)、社交与通讯(Facebook、Instagram、Reddit、Telegram 及 `t.me` 短链、WhatsApp 及 `wa.me`、`weixin.qq.com` 上的微信、QQ、微博)、购物(淘宝、速卖通、eBay)、参考资料与社区(Quora、V2EX)以及 Apple。主机等于某后缀或为其子域即匹配,且最长匹配后缀优先,因此 `gist.github.com` 与 `en.wikipedia.org` 无需单独登记,而 `weixin.qq.com` 得到微信标记而不是其 `qq.com` 后缀同样会选中的 QQ 标记。只有绝对 `http:` 与 `https:` 目的地能够匹配,其余一律回退到地球。 - -图形取自 [Simple Icons](https://simpleicons.org) 图标集(CC0-1.0),以 ui-primitives 的 devDependency 形式引入,因此新增站点只是主机映射中的一条记录,图形版本由 lockfile 固定,而不是复制路径数据。每个标记以该图标集的单条 path 绘制在其 24 单位 viewBox 上,并内缩到 28 单位盒中,以保留 20 单位 `ic_ds_*` 图形在自身盒内约 8% 的留白;填充使用链接的 `currentColor`,而不是图标集记录的品牌填充色。所有标记均为 `aria-hidden`,锚点自身文本仍是可访问名称。 - -对功能性链接图形采用 `currentColor` 是刻意的取舍,而非品牌呈现:YouTube、X、Instagram 等品牌公布的规范禁止为其标记改色,下方第一个备选方案比较了固定品牌填充的做法。CC0-1.0 覆盖路径数据;每个标记仍属其所有者的商标,生成的第三方声明覆盖该 npm 包本身,而不是这份图形。 - -两处消费者传入自己的目的地:markdown 渲染器的 `renderSafeLink`(覆盖作者书写的锚点、引用式链接与提升为链接的行内代码)以及 web 卡片的来源与抓取链接。两者本已持有经安全校验的目的地。 - -## Alternatives considered - -- **抓取各站点的 favicon**,无论取自站点本身还是 favicon 服务。这能覆盖任意站点,但渲染一份转写内容会向每个被链接的主机发起网络请求,泄露阅读行为并把文本渲染变成网络操作;它在离线与严格 `img-src` 策略下同样失败。拒绝:固定的本地词汇表让渲染保持确定与私密,地球仍是诚实的回退。 -- **以站点品牌色填充标记。** 拒绝:链接图形只使用 `currentColor`,以跟随链接 alias、暗色模式与 hover;固定填充会成为首个例外,并与链接自身的 hover 颜色冲突。 -- **把路径数据复制进本模块。** 拒绝:上游会持续更新的第三方图形集正是需要固定版本依赖来跟踪的内容;十四个内嵌副本没有更新路径。 -- **把站点值加进 `LinkIconKind`。** 拒绝:kind 是消费者陈述的分类,而站点由目的地推导;把两者折进同一联合类型会迫使每个消费者写出自己并不知晓的站点。 -- **为每个未映射主机生成首字母方块。** 以噪音为由拒绝:自动生成的字母胶囊宣称了一种站点身份却没有承载它,14px 也没有留下可读字母的空间。 - -## Consequences - -- 新增站点需要在 `SiteGlyph.tsx` 的主机映射中加一条记录,并在 `link-icon` spec 中补一个 URL 才会被检查:该 spec 为每个已映射站点列出一个代表 URL、要求它们共产生三十四个互不相同的标记,并固定 GitHub、npm、YouTube、Wikipedia、X、Telegram 与 WhatsApp 的别名。记录丢失或重复会让数量断言失败,而只改主机映射、不补 spec URL 的新站点仍未被 spec 覆盖。 -- 词汇表按类别而非按需求增长:每个站点需要一条主机映射、一个上游标记与一个 spec URL;这三十四个标记在源码中增加约 23 KB 路径数据,经 shell 构建的 tree-shake、压缩与 gzip 后进入入口 chunk 的只有几 KB。 -- `simple-icons` 作为 ui-primitives 的开发依赖进入浏览器构建图;shell 构建会 tree-shake 所导入的标记,而不是整套图标。 -- 未收录主机、非 http scheme、无法解析的目的地与文件类别都保留原有的地球或分类图形;转写内容中的 `mailto` 链接仍显示地球。 -- 该词汇表刻意有限。像 `example.com` 这样的站点不会由此机制获得标记;要识别它们就需要被上述备选方案拒绝的网络抓取。 -- 覆盖:LinkIcon spec 覆盖别名、各回退分支与尺寸座位;markdown spec 固定一个已知锚点与一个未知锚点;web 卡片 spec 用同一批标记固定一个来源链接与一个抓取链接。 diff --git a/.agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.i18n.yaml similarity index 55% rename from .agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.i18n.yaml rename to .agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.i18n.yaml index b25a4b72b4..cf44de1cb6 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-desktop-policy-login-loading.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.i18n.yaml @@ -1,6 +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-16-desktop-policy-login-loading.md -2026-09-16-desktop-policy-login-loading.md: 26fd635c6cc5b92cad8011919382a11497d2e835 -2026-09-16-desktop-policy-login-loading.zh.md: 16de25323afc971b3142d5359c769df50c08800d +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.md +2026-09-17-compact-translucent-menu-surfaces.md: 08fcb2d851e4c75ce9069fc797e8dbcb070acc5b +2026-09-17-compact-translucent-menu-surfaces.zh.md: 8e13634995d46b7414f17bbb51443df85cb3ac3c diff --git a/.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.md b/.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.md new file mode 100644 index 0000000000..08fcb2d851 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.md @@ -0,0 +1,29 @@ +# Agent Note: Compact translucent menu surfaces + +Status: implemented + +English | [中文](2026-09-17-compact-translucent-menu-surfaces.zh.md) + +## Problem + +Shared dropdowns and feature-owned menu panels used the same elevated color token but retained unrelated padding, row height, corner radius, and scrollbar geometry. The opaque menu fill also made nested and portalled surfaces read as solid cards rather than one elevation family, while narrowing individual menus locally would keep those differences distributed across packages. + +## Decision + +`ui-theme` defines `--dsw-specific-menu` as a translucent light or dark fill and defines `--dsw-menu-backdrop-filter` as `blur(40px) saturate(150%)`. Every elevated package surface that paints the menu fill also applies that backdrop filter, uses `border: 0`, and takes an elevation shadow with its rebindable hairline stroke. A surface containing fixed-position overlays paints the fill and filter on an isolated background pseudo-element, because a filtered ancestor would otherwise change those overlays' containing block. Descendant sticky rows may repaint the inherited fill without another filter. Browsers without backdrop filtering still render the theme-owned translucent fill. + +The shared `Menu` and the composer input-trigger menu use the same compact baseline: a 16px outer radius, 3px frame padding, 34px ordinary rows, 13px primary text on a 20px line, 6px icon/text gaps, and 8px row radii. Dense and compact variants reduce from that baseline instead of retaining the former ordinary geometry. The composer menu keeps its feature-owned grouping and 400px height cap; its aliases and descriptions use 12px text on an 18px line. + +The global WebKit scrollbar width is 5px. The composer menu overrides it with a 6px draggable rail and a 2px visible thumb through the existing scrollbar geometry variables; Firefox continues to use its standard thin scrollbar path. Track insets and elevated l2 thumb colors remain surface-owned rebindings. + +## Alternatives considered + +**Keep an opaque shared menu fill.** Rejected because every consumer would need a separate translucent override to share one material, recreating the theme branch outside `ui-theme`. + +**Compact only the composer command menu.** Rejected because the shared `Menu` renders the same action, settings, and navigation vocabulary; keeping its former 40px rows would preserve two densities for the same control family. + +**Use the 6px padded rail for every scrollbar.** Rejected because ordinary scroll regions need the direct 5px thumb, while the command menu needs a wider hit area around a visually quieter thumb. The existing geometry variables express that difference without another scrollbar implementation. + +## Consequences + +Elevated menu-fill consumers must pair the fill and backdrop-filter tokens in the same rule, either on the surface or on its isolated background pseudo-element; the stylesheet gate rejects a material layer that omits the filter. Shared menu geometry changes affect every `Menu` render site, while the composer menu retains its own content and interaction rules. Browser and component snapshots cover assembled structure, and theme specs pin the tokens, compact metrics, scrollbar geometry, and complete menu-filter pairing. diff --git a/.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.zh.md b/.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.zh.md new file mode 100644 index 0000000000..8e13634995 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.zh.md @@ -0,0 +1,29 @@ +# Agent Note: 紧凑半透明菜单表面 + +Status: implemented + +[English](2026-09-17-compact-translucent-menu-surfaces.md) | 中文 + +## Problem + +共享下拉菜单与功能包自有菜单面板使用同一个高层级颜色 token,却保留了不同的内边距、行高、圆角和滚动条几何。不透明菜单填充也让嵌套与 portal 表面像彼此独立的实色卡片;如果只在各功能菜单中局部收紧尺寸,这些差异仍会分散在多个包中。 + +## Decision + +`ui-theme` 将 `--dsw-specific-menu` 定义为明暗主题各自的半透明填充,并将 `--dsw-menu-backdrop-filter` 定义为 `blur(40px) saturate(150%)`。每个绘制菜单填充的包级高层级表面同时应用该 backdrop filter、设置 `border: 0`,并使用带可重绑发丝描边的 elevation 投影。包含 fixed 定位浮层的表面把填充与滤镜绘制在隔离的背景伪元素上,因为带滤镜的祖先会改变这些浮层的包含块。后代 sticky 行可以再次绘制继承的填充,无需重复 filter。不支持 backdrop filtering 的浏览器仍渲染主题持有的半透明填充。 + +共享 `Menu` 与 composer 输入触发菜单使用同一套紧凑基准:16px 外圆角、3px 边框内距、34px 普通行、13px 主文字配 20px 行高、6px 图文间距,以及 8px 行圆角。dense 与 compact 变体在该基准上继续缩小,不再保留旧的普通几何。composer 菜单保留其功能包持有的分组与 400px 高度上限;别名和说明使用 12px 字号与 18px 行高。 + +WebKit 系全局滚动条宽度为 5px。composer 菜单通过既有滚动条几何变量覆盖为 6px 可拖动轨道与 2px 可见滑块;Firefox 继续走标准 thin scrollbar 路径。轨道内缩与高层级 l2 滑块颜色仍由表面自行重绑。 + +## Alternatives considered + +**保留不透明的共享菜单填充。** 否决,因为每个消费方都需要单独覆盖成半透明,导致同一种材质再次在 `ui-theme` 之外分叉。 + +**只收紧 composer 指令菜单。** 否决,因为共享 `Menu` 渲染的是同一套操作、设置与导航词汇;保留原 40px 行会让同一个控件家族继续存在两种密度。 + +**所有滚动条都使用 6px 带内边距轨道。** 否决,因为普通滚动区域需要直接的 5px 滑块,而指令菜单需要在更安静的可见滑块外保留更宽点击区域。现有几何变量可以表达该差异,无需第二套滚动条实现。 + +## Consequences + +高层级菜单填充消费方必须在同一规则中同时使用填充与 backdrop-filter token,规则可以属于表面本身,也可以属于其隔离的背景伪元素;样式表门禁会拒绝漏掉 filter 的材质层。共享菜单几何影响每个 `Menu` 渲染点,composer 菜单仍保留自身内容与交互规则。浏览器与组件快照覆盖装配结构,主题测试固定 token、紧凑尺寸、滚动条几何与完整的菜单 filter 配对。 diff --git a/.agents/notes/implemented/feature/2026-09-16-known-site-link-marks.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml similarity index 58% rename from .agents/notes/implemented/feature/2026-09-16-known-site-link-marks.i18n.yaml rename to .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml index 513a59328a..69db5487b8 100644 --- a/.agents/notes/implemented/feature/2026-09-16-known-site-link-marks.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml @@ -1,6 +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-16-known-site-link-marks.md -2026-09-16-known-site-link-marks.md: 9d029af06f9ca0f3fa8290112c7023ec1b9e0b84 -2026-09-16-known-site-link-marks.zh.md: 2c7630d122d1576ba41cb653cf0b86e9c5a2ddb4 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md +2026-09-17-developer-tools-settings.md: e76cb7f367faff71ef916d8a263dc523583f1cd2 +2026-09-17-developer-tools-settings.zh.md: 5a7d6d59db686f57ef4945b572ea65ffe54b7f9c diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md new file mode 100644 index 0000000000..e76cb7f367 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md @@ -0,0 +1,29 @@ +# Agent Note: Shared developer-tool settings + +Status: implemented + +English | [中文](2026-09-17-developer-tools-settings.zh.md) + +## Problem + +Diagnostic views, preset selection and changed-file summaries add complexity to routine tasks. Scripted HTML also grants capabilities that a basic document preview does not need. Desktop and Web share these renderers, so independent switches would let the same setting produce different results across clients. + +## Decision + +The settings-owned `ctx.settingsScope.developerTools` preference binds a namespace scope and exposes one accepted `ui-developer-tools.enabled` preference, defaulting off. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; loopback Web and desktop follow Host acceptance. Remote Web owns one browser-local observable shared by all consumers until reload, with no Host writes. + +Off exposes only Chat, hides tool Inspect actions when no visible View declares tool-call inspection, removes the conversation View tab bar, hides the new-session preset chip, and omits the changed-files card and its summary read. Turning it off from another View activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization. + +Plugin assembly translates the shared preference into component-specific capabilities (`showCodeDiff`, `showPresetPicker`, and `interactivePreview`). Conversation supplies the inspection callback using the visible target's `toolCallFocus`; Chat does not name the target. The builtin HTML renderer uses DOMPurify for complete-document sanitization and parser-mutation defenses. Off preserves root styling, removes active documents, resource hints, SVG animation and all `href` attributes, and mounts an iframe with no sandbox permissions and a CSP denying scripts and external resources. It reads no related files. On retains the opaque scripted Blob renderer and its bounded related-file reads. Switching modes replaces the browsing context and disposes pending reads. The [document-preview decision](../architecture/2026-09-08-document-preview-operations.md) and [filesystem-read authority](../architecture/2026-09-09-workspace-file-read-authority.md) remain authoritative for advanced rendering and Host read access. + +## Alternatives considered + +**Limit the switch to desktop.** The selected requirement contains no desktop-only restriction, and the affected UI is shared. A desktop check would leave the Web renderer with inconsistent visibility and preview behavior. + +**Hide controls without changing the active View or iframe.** Previously selected diagnostic content and already-running scripts would survive the toggle. Filtering the available View roster and remounting the HTML context enforce the selected presentation immediately. + +## Consequences + +Basic HTML sacrifices JavaScript, external and related resources, and link navigation; inline styles and data images remain available. Advanced mode retains normal browser networking and does not gain parent-origin, popup, form or top-navigation privileges. The setting does not constrain third-party preview implementations or revoke Host filesystem authority. + +Component and settings tests cover defaulting, schema rejection, accepted-state updates, chip disclosure, changed-file visibility and iframe disposal. The recorded-session [document preview](../../../../apps/web/tests/document-preview.e2e.ts) and [changed-files](../../../../apps/web/tests/changed-files-turn.e2e.ts) scenarios exercise the real settings control and the shared renderer. The browser scaffold enables developer tools by default. Only settings chrome, document preview, changed-files and developer-tools scenarios explicitly retain the shipped default; together they cover hidden diagnostic controls, static previews, and switching back to Chat. Refused Host writes recover accepted state and show the localized retry message. Native Electron execution remains unverified. diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md new file mode 100644 index 0000000000..5a7d6d59db --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md @@ -0,0 +1,29 @@ +# Agent Note: 共享开发者工具设置 + +Status: implemented + +[English](2026-09-17-developer-tools-settings.md) | 中文 + +## Problem + +诊断视图、预设选择和改动文件摘要增加了日常任务的复杂度。支持脚本的 HTML 还授予了基础文档预览不需要的能力。桌面端和 Web 共享这些渲染器,分别设置开关会让同一设置在不同客户端产生不同结果。 + +## Decision + +设置域内的 `ctx.settingsScope.developerTools` 偏好对象绑定命名空间 scope,提供一个已接受的 `ui-developer-tools.enabled` 偏好,默认关闭。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。 + +关闭时仅展示 Chat,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,移除会话 View 标签栏,隐藏新会话预设入口,并省略改动文件卡片及其摘要读取。在其他 View 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。 + +插件组装层将共享偏好转换为组件自身的能力(`showCodeDiff`、`showPresetPicker` 和 `interactivePreview`)。Conversation 根据可见目标的 `toolCallFocus` 提供检查回调;Chat 不再指定目标名称。内置 HTML 渲染器使用 DOMPurify 清理完整文档并防护解析变异。关闭时保留根节点样式,移除主动文档、资源提示、SVG 动画及所有 `href` 属性,再放入不授予沙箱权限的 iframe,并通过 CSP 禁止脚本和外部资源。不读取关联文件。开启时保留不透明源的脚本 Blob 渲染器及其有界关联文件读取。切换模式会替换浏览上下文并清理待处理读取。[文档预览决策](../architecture/2026-09-08-document-preview-operations.zh.md)和[文件系统读取权限](../architecture/2026-09-09-workspace-file-read-authority.zh.md)继续定义高级渲染与 Host 读取访问。 + +## Alternatives considered + +**仅在桌面端提供开关。** 所选需求没有仅限桌面的约束,相关 UI 也是共享的。桌面端条件判断会让 Web 渲染器的展示和预览行为不一致。 + +**隐藏控件但不改变激活的 View 或 iframe。** 之前选择的诊断内容和已经运行的脚本会在切换后继续存在。过滤可用 View 并重新挂载 HTML 上下文,可以立即执行所选展示策略。 + +## Consequences + +基础 HTML 放弃 JavaScript、外部与关联资源和链接导航;行内样式与 data 图片仍可用。高级模式保留正常浏览器网络能力,不增加父源、弹窗、表单或顶层导航权限。此设置不约束第三方预览实现,也不撤销 Host 文件系统读取权限。 + +组件和设置测试覆盖默认值、schema 拒绝、已接受状态更新、入口折叠、改动文件展示和 iframe 清理。基于已记录 Session 的[文档预览](../../../../apps/web/tests/document-preview.e2e.ts)与[改动文件](../../../../apps/web/tests/changed-files-turn.e2e.ts)场景验证真实设置控件及共用渲染器。浏览器脚手架默认开启开发者工具,仅设置外壳、文档预览、改动文件和开发者工具场景显式保留产品默认值;这些场景共同覆盖诊断控件隐藏、静态预览和切回 Chat。Host 拒绝写入后会恢复已接受状态,并显示本地化重试提示。原生 Electron 执行尚未验证。 diff --git a/.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.i18n.yaml similarity index 57% rename from .agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.i18n.yaml rename to .agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.i18n.yaml index 6009b3c1ad..d86602a99e 100644 --- a/.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.i18n.yaml @@ -1,6 +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-04-web-clickable-link-styles.md -2026-09-04-web-clickable-link-styles.md: c4e506294c87c6b959cb449403edce09daa11062 -2026-09-04-web-clickable-link-styles.zh.md: 01258674b4b8194ad5e0597cde61cc2186406d49 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.md +2026-09-17-state-dot-visual-language.md: 751abbaa33919721c7e71738ad9b6bed744fcad7 +2026-09-17-state-dot-visual-language.zh.md: 9746ec9af79e15fc41ea8e6601cc52c1cfafba02 diff --git a/.agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.md b/.agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.md new file mode 100644 index 0000000000..751abbaa33 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.md @@ -0,0 +1,31 @@ +# Agent Note: StateDot solid marks and loading spinner + +Status: implemented + +English | [中文](2026-09-17-state-dot-visual-language.zh.md) + +## Problem + +`StateDot` mixed two drawing languages: settled states used a translucent halo around a solid core, while `ongoing` used an eight-cell pixel chase. + +## Decision + +`idle`, `done`, `warning`, and `error` render as one solid 6px circle inside the existing 10px layout slot, with no halo. `idle` uses the neutral `--dsw-alias-state-idle-primary` token, `done` remains success green, `warning` remains amber, and `error` remains red. + +`ongoing` is the sole non-dot member. Its default edge is 14px, while solid states retain their 10px layout slot. It renders a 25%-opacity complete ring behind a grey arc using the tertiary label token. The glyph rotates continuously over 1.5 seconds while the arc grows from 12 to 24 dash units around its center and returns to 12; the arc offset is zero at both ends, so the browser does not reset a second circular movement at the loop boundary. Reduced-motion environments retain an intermediate static arc. The explicit size override, `data-state`, and `aria-hidden` behavior do not change. + +Converted compact status-only presentations use this shared mapping instead of local dots or spinners: waiting or blocked is `warning`, active work is `ongoing`, successful completion is `done`, failure is `error`, and inactive or not-yet-started work is `idle`. Tool rows whose leading slot is a business icon retain the ordinary glyph in every lifecycle state; their collapsed summary turns red for failure and amber for `stopped`, preserving any tool-specific interruption text. The framework-free boot page remains separate because its arc reports aggregate loader progress before React and the shared primitive are available. + +## Alternatives considered + +**Keep the pixel chase.** Rejected because its block animation did not share the circular language of the settled markers and read as a decorative activity glyph rather than a conventional loading state. + +**Fill the complete 10px slot.** Rejected because the former visible core was 6px; retaining that diameter preserves row density while removing only the halo. + +## Testing + +The component specs pin the four solid-state elements, the two-circle loading artwork, both animation tracks, both size paths, the no-halo stylesheet, and every state token. Tool, Bash, and Skill row specs pin the retained business glyph plus visible red failure and amber interruption summaries. Workspace, Job, Workflow, Subagent, Deliverables, terminal, plugin, Schedule, Todo, Team, approval, document-preview, Trajectory, and connection suites pin their state mappings and accessible labels. + +## Consequences + +Consumers keep their accessibility labels while status-only presentations share one visual vocabulary. Every settled marker becomes visually quieter and more compact, completion remains green, and every active marker uses the same tertiary-grey rotating loader. Action glyphs such as reconnect and business glyphs such as file or tool types remain available when the icon itself communicates the operation rather than only its state. diff --git a/.agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.zh.md b/.agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.zh.md new file mode 100644 index 0000000000..9746ec9af7 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-17-state-dot-visual-language.zh.md @@ -0,0 +1,31 @@ +# Agent Note: StateDot 纯色标记与旋转 loading + +Status: implemented + +[English](2026-09-17-state-dot-visual-language.md) | 中文 + +## 问题 + +`StateDot` 混用了两套绘制语言:已结算状态在实心核外带半透明光晕,`ongoing` 则使用八格像素追逐动画。 + +## 决策 + +`idle`、`done`、`warning` 与 `error` 在既有 10px 布局槽内渲染一个 6px 纯色圆点,不再带光晕。`idle` 使用中性的 `--dsw-alias-state-idle-primary` token,`done` 保持成功绿色,`warning` 保持琥珀色,`error` 保持红色。 + +`ongoing` 是唯一不是圆点的成员。它的默认边长为 14px,纯色状态仍保留 10px 布局槽。它在透明度为 25% 的完整圆环上方,使用 tertiary label token 渲染灰色圆弧。图形以 1.5 秒周期持续旋转,圆弧围绕中心从 12 个 dash 单位增长到 24 个、再回到 12 个;圆弧偏移在首尾都为零,因此浏览器不会在循环边界重置第二段圆周运动。减少动态效果的环境保留中间长度的静态圆弧。显式 size 覆盖、`data-state` 与 `aria-hidden` 行为均不改变。 + +已转换的紧凑纯状态展示使用这套共享映射,不再自绘圆点或 spinner:等待或阻塞为 `warning`,活动工作为 `ongoing`,成功完成为 `done`,失败为 `error`,未活动或尚未开始为 `idle`。前置槽为业务图标的工具行在所有生命周期状态中都保留普通图标;收起摘要在失败时变红,在 `stopped` 时变为琥珀色并保留工具自有的中断文本。无框架的启动页保持独立,因为它会在 React 与共享原语可用前用圆弧表达 Loader 总体进度。 + +## 考虑过的替代方案 + +**保留像素追逐动画。** 否决,因为方块动画没有沿用已结算标记的圆形语言,看起来更像装饰性活动图形,而不是常规 loading 状态。 + +**填满完整 10px 槽位。** 否决,因为原来的可见实心核就是 6px;保留该直径只移除光晕,同时维持行密度。 + +## 测试 + +组件测试固定四种纯色状态元素、双圆环 loading 图稿、两条动画轨道、两条尺寸路径、无光晕样式和每个状态 token。Tool、Bash 与 Skill 行测试固定保留的业务图形,以及可见的红色失败摘要与琥珀色中断摘要。Workspace、Job、Workflow、Subagent、Deliverables、终端、插件、Schedule、Todo、Team、审批、文档预览、Trajectory 与连接测试固定各自的状态映射和无障碍 label。 + +## 后果 + +消费方保留无障碍 label,纯状态展示共享同一套视觉词汇。所有已结算标记在视觉上更安静、更紧凑,完成态保持绿色,所有活动态使用同一种 tertiary 灰色旋转 loading。重连等动作图标和文件或工具类型等业务图标在图形本身表达操作而不只表达状态时继续保留。 diff --git a/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.i18n.yaml deleted file mode 100644 index 896278e3d6..0000000000 --- a/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.i18n.yaml +++ /dev/null @@ -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/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.md -2026-09-17-web-sidebar-session-title-hover-scroll.md: 07f802f2c64aa40bb13ba679cda2007112b58d69 -2026-09-17-web-sidebar-session-title-hover-scroll.zh.md: aa08bac5018304a0e25e93062f7cc45ca415799e diff --git a/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.md b/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.md deleted file mode 100644 index 07f802f2c6..0000000000 --- a/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.md +++ /dev/null @@ -1,41 +0,0 @@ -# Agent Note: Web sidebar Session title hover scroll - -Status: implemented - -English | [中文](2026-09-17-web-sidebar-session-title-hover-scroll.zh.md) - -## Problem - -A Session title wider than its sidebar row is clipped with an ellipsis, so the part that distinguishes two related Sessions is the part that disappears. Fork makes it visible: the child's persisted title is the parent's plus an increment, so a long parent title leaves the `(1)` off the row's right edge. Opening the hover card shows the full title, but the card needs a dwell, covers neighboring rows, and is not the comparison the user is trying to make. - -## Decision - -A Session row reveals its clipped title in place while the pointer rests on the row. `SessionNodeItem` writes `title.scrollLeft = scrollWidth - clientWidth` on pointer enter and returns it with `scrollTo({ left: 0, behavior: 'instant' })` on pointer leave; the title element keeps `overflow: hidden`, so nothing reflows, no scrollbar appears, and the page never scrolls. Entering glides because the title carries `scroll-behavior: smooth` (the file's reduced-motion block restores `auto`), so the handler owns no timer or animation loop. Returning is deliberately one step: a smooth return would travel under the resting ellipsis and the row's narrowing cell for its whole duration. - -While the pointer is on the row, `text-overflow` switches from `ellipsis` to `clip`: a scrolled `overflow: hidden` box still paints the ellipsis at its right edge, which would cover exactly the characters the scroll just revealed (verified in Chromium before the rule was written). That rule sits behind `@media (hover: hover)`, because a touch tap latches `:hover` on the row after the title has already returned, and an unguarded rule would leave the tapped row hard-clipped at its start. - -The reveal belongs to the Session row, not to a single cell: entering anywhere on the row starts it, and it ends only when the pointer leaves the row. Workspace rows and search results keep their ellipsis behavior — their titles distinguish nothing by a trailing token — and only the Session title carries the scroll behavior. - -The hovered row is also the wider cell: its trailing relative-time label yields to the row menu, so the hovered `clientWidth` is larger than the resting one and the hovered scroll maximum is smaller. A handler that read the resting width would compute a larger delta that the browser clamps to the hovered maximum, reaching the same far edge; the browser scenario therefore polls the live extent instead of the value the handler read. - -## Alternatives considered - -**A moving inner span or a duplicated-copy marquee.** Rejected: `text-overflow: ellipsis` ellipsizes text directly inside the clipping block, so moving the text requires a nested element whose own clipping (or the duplicate copy) moves with it; the resting ellipsis, the existing DOM, and the accessible text would all change. Scrolling the existing element keeps every one of them. - -**A rAF or CSS-loop marquee.** Rejected: a continuous animation keeps moving while the user reads and needs per-row lifetime management. One scroll to the far edge and one back is the whole reveal. - -**`scrollTo({ behavior: 'smooth' })` for both moves.** Rejected: it needs a `matchMedia` probe for reduced motion plus a capability branch, and it would keep the return animating under the resting ellipsis. The stylesheet supplies the entering glide, and the leaving move asks for `instant` explicitly. - -**A handler-owned clip state cleared on `scrollend`.** Considered for the return transient; rejected because returning in one step removes the transient without a second state, an event listener, or a browser capability the jsdom lane lacks. - -**`direction: rtl` on hover.** Rejected: it jumps instead of scrolling and reorders punctuation inside mixed-direction titles. - -**Leaving the reveal to the hover card.** Rejected: the card opens only after its dwell and covers neighboring rows, so it cannot support comparing two adjacent rows. - -## Consequences - -A long title still occupies exactly one row line; the scroll range is measured from the element's own box, so a width change (the sidebar resizing, or the hovered row's own cell change) is picked up on the next pointer enter, and the instant return leaves the resting ellipsis and the text consistent whenever the pointer leaves. Revealing depends on a pointer resting on the row, so touch and keyboard users still rely on the hover card and the Rename dialog for the full title; removing the reveal leaves an ordinary ellipsized row. - -## Testing - -`packages/client/ui-workspace/tests/rows.client.spec.tsx` pins the scroll position written on pointer enter, the explicit instant return, and that a title that fits stays at its start. `packages/client/ui-workspace/tests/browser-styles.client.spec.ts` pins the stylesheet contract for the hovered row. `apps/web/tests/sidebar-title-hover-scroll.e2e.ts` hovers a seeded clipped row in the assembled application and asserts that the title really overflows, reaches its own live extent under the pointer, stops ellipsizing, returns to its start within one step, and that the computed scroll behavior is `smooth` in the default media state and `auto` under reduced motion. diff --git a/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.zh.md b/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.zh.md deleted file mode 100644 index aa08bac501..0000000000 --- a/.agents/notes/implemented/feature/2026-09-17-web-sidebar-session-title-hover-scroll.zh.md +++ /dev/null @@ -1,41 +0,0 @@ -# Agent Note: Web 侧栏 Session 标题悬停滚动 - -Status: implemented - -[English](2026-09-17-web-sidebar-session-title-hover-scroll.md) | 中文 - -## 问题 - -标题宽于侧栏行时会被省略号裁切,于是区分两个相关 Session 的那一部分恰好消失。Fork 会让这件事暴露出来:子会话持久化标题是父标题加一个递增序号,父标题一长,`(1)` 就落到行的右边界之外。悬停卡片能显示完整标题,但它需要停留等待、会盖住相邻行,而且用户想做的比较并不是看卡片。 - -## 决策 - -指针停在 Session 行上时,该行就地显示被裁切的标题。`SessionNodeItem` 在指针进入时写入 `title.scrollLeft = scrollWidth - clientWidth`,在指针离开时用 `scrollTo({ left: 0, behavior: 'instant' })` 一次性回到起点;标题元素保持 `overflow: hidden`,因此不会重排、不会出现滚动条,页面本身也不滚动。进入时之所以平滑,是因为标题带有 `scroll-behavior: smooth`(本文件的减少动效块恢复为 `auto`),处理函数因此不持有定时器或动画循环。返回被有意做成一步到位:平滑返回会让静止态省略号和收窄后的格子在整个返回过程中一直覆盖着文字。 - -指针停留在行上期间,`text-overflow` 由 `ellipsis` 切换为 `clip`:已滚动的 `overflow: hidden` 盒子仍会在右边界绘制省略号,而那正是刚刚被滚动显示出来的字符(写这条规则前已在 Chromium 中验证)。这条规则位于 `@media (hover: hover)` 之内,因为触屏点按会在标题已经返回之后仍让该行的 `:hover` 保持钉住,不设保护就会让被点按的行停在起点且硬裁切、失去省略号。 - -该行为属于 Session 行,而不是某一格:指针进入行的任意位置都会开始,只有离开该行才结束。Workspace 行与搜索结果保留省略号行为——它们的标题没有靠尾部记号区分的含义——只有 Session 标题带有滚动行为。 - -悬停中的行同时也是更宽的格子:行尾的相对时间标签会让位给行操作菜单,因此悬停时的 `clientWidth` 比静止时更大,可滚动上限更小。若处理函数读到的是静止宽度,它会算出一个更大的位移,浏览器会把它夹到悬停状态的上限,最终同样到达远端;因此浏览器场景断言的是实时到达范围,而不是处理函数读到的数值。 - -## 备选方案 - -**移动的内层元素或复制副本的跑马灯。** 否决:`text-overflow: ellipsis` 只对裁切块内直接包含的文本生效,因此移动文本需要嵌套元素,而它自身的裁切(或复制副本)会随之移动;静止态的省略号、现有 DOM 和可访问文本都会改变。滚动现有元素则三者都保留。 - -**rAF 或 CSS 循环跑马灯。** 否决:持续动画会在用户阅读时继续移动,并且需要逐行管理生命周期。滚到远端再滚回一次,就是这个揭示所需的全部动作。 - -**两个方向都用 `scrollTo({ behavior: 'smooth' })`。** 否决:它需要一个 `matchMedia` 探测来处理减少动效,还需要一个能力分支,并且会让返回过程在静止态省略号之下持续动画。进入的平滑由样式表提供,离开则显式要求 `instant`。 - -**由处理函数持有裁切状态并在 `scrollend` 时清除。** 曾用于消除返回过程的瞬态;否决,因为一步返回直接消除该瞬态,无需第二个状态、事件监听或 jsdom 车道缺少的浏览器能力。 - -**悬停时使用 `direction: rtl`。** 否决:它是跳变而非滚动,并且会重排混合方向标题中的标点。 - -**把揭示交给悬停卡片。** 否决:卡片要等停留时间结束才出现,而且会盖住相邻行,无法支持并排比较两行。 - -## 后果 - -长标题仍然只占一行;滚动范围按元素自身盒宽测量,因此宽度变化(侧栏缩放,或悬停行自身的格子变化)会在下一次指针进入时被重新读取,而一步返回保证指针离开时静止态省略号与文字始终一致。揭示依赖指针停在行上,所以触屏与键盘用户仍需通过悬停卡片和重命名对话框获取完整标题;去掉该行为后,这一行只是普通的省略号行。 - -## 测试 - -`packages/client/ui-workspace/tests/rows.client.spec.tsx` 钉住指针进入时写入的滚动位置、显式的一步返回,以及能放下的标题停留在起点。`packages/client/ui-workspace/tests/browser-styles.client.spec.ts` 钉住悬停行的样式表契约。`apps/web/tests/sidebar-title-hover-scroll.e2e.ts` 在组装后的应用中悬停一条被裁切的种子会话行,断言标题确实溢出、到达指针下的实时范围、停止省略、一步回到起点,并断言默认媒体状态下计算出的滚动行为为 `smooth`、减少动效下为 `auto`。 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index b6d13dd1aa..23d245c8c7 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -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: 10123fe1999c0ad03f977e1cc7c69d788a679c2b -2026-07-26-ci-failover-runbook.zh.md: cff99e6f644bf945f8366c2a723b32222ab902b4 +2026-07-26-ci-failover-runbook.md: c2fd2fa6cf673ec4d202702c16a9a85a3a609b8d +2026-07-26-ci-failover-runbook.zh.md: 7fa5887bb552afd98cebd7502202fb5e9f37b1ee diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 10123fe199..c2fd2fa6cf 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -32,7 +32,7 @@ The two switches are independent: flip only the one whose platform is degraded. 1. Repository **Settings → Secrets and variables → Actions → Variables → New repository variable**: name `DSH_CI_FAILOVER_LINUX` (Linux pool outage) or `DSH_CI_FAILOVER_WINDOWS` (Windows pool outage), value `selfhosted`. 2. Retrigger the required jobs so they re-resolve their pool. Jobs already **queued** for the hosted labels do not retarget and cannot be re-run in place, so for the documented indefinite-queue outage, cancel the stuck run and re-run all jobs, or push a new commit; "Re-run failed jobs" only helps once a job has actually failed rather than queued. -3. That is the entire switch. Under the `selfhosted` Linux failover value the workflow also drops `DSH_SNAPSHOT_MAX_CONCURRENCY` to 12 for the shared VM and skips the hosted-path pnpm cache restores because the VM's persistent store serves warm installs. Coverage uses the same four single-worker instrumented partitions and two exempt workers on both Linux pools. The Windows switch has no concurrency or cache branches; it only retargets the native Windows jobs' pool. +3. That is the entire switch. Under the `selfhosted` Linux failover value the workflow also drops `DSH_SNAPSHOT_MAX_CONCURRENCY` to 12 for the shared VM and skips the hosted-path pnpm cache restores because the VM's persistent store serves warm installs. Self-hosted coverage uses four single-worker instrumented partitions and two exempt workers; isolated hosted runners derive worker defaults from available CPUs. The Windows switch selects the native Windows pool and retains explicit worker limits on shared self-hosted runners. **Dependabot exception.** Both switches' `selfhosted` legs deliberately exclude `dependabot[bot]`: under self-hosted failover, Dependabot PRs stay queued for the hosted pool rather than executing dependency-supplied code on the persistent VMs. A Dependabot PR that remains queued during an outage is expected behavior, not a failed switch; it completes when the hosted pool recovers. The `blacksmith` value's branches carry no such exclusion, because Blacksmith runners are ephemeral (see the [blacksmith failover leg note](2026-09-09-blacksmith-failover-leg.md)). diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index cff99e6f64..7fa5887bb5 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -32,7 +32,7 @@ Status: implemented 1. 仓库 **Settings → Secrets and variables → Actions → Variables → New repository variable**:名称 `DSH_CI_FAILOVER_LINUX`(Linux 池故障)或 `DSH_CI_FAILOVER_WINDOWS`(Windows 池故障),值 `selfhosted`。 2. 重新触发必需作业,使其重新解析运行器池。已经为托管标签**排队**的作业不会重定向,也无法原地 re-run,因此对于本手册所述的无限排队故障,应取消卡住的运行并 re-run all jobs,或推送一个新提交;“Re-run failed jobs”只有在作业真正失败(而非仍在排队)时才有用。 -3. 切换到此完成。在 `selfhosted` 的 Linux 故障切换取值下,工作流还会把 `DSH_SNAPSHOT_MAX_CONCURRENCY` 降为 12,以限制共享虚拟机上的争抢,并跳过托管路径的 pnpm 缓存恢复,因为虚拟机的持久 store 会直接提供热安装。覆盖率在两个 Linux 池上都使用 4 个单 worker 插桩分区与 2 个豁免 worker。Windows 开关没有并发或缓存分支;它只重定向原生 Windows 作业的运行器池。 +3. 切换到此完成。在 `selfhosted` 的 Linux 故障切换取值下,工作流还会把 `DSH_SNAPSHOT_MAX_CONCURRENCY` 降为 12,以限制共享虚拟机上的争抢,并跳过托管路径的 pnpm 缓存恢复,因为虚拟机的持久 store 会直接提供热安装。自托管覆盖率使用 4 个单 worker 插桩分区与 2 个豁免 worker;独占的托管运行器根据可用 CPU 计算默认 worker 数。Windows 开关选择原生 Windows 运行器池,并在共享的自托管运行器上保留显式 worker 限制。 **Dependabot 例外。**两个开关的 `selfhosted` 腿都刻意排除 `dependabot[bot]`:自托管故障切换期间,Dependabot 拉取请求继续在托管池排队,而不是把依赖项提供的代码放到持久化虚拟机上执行。故障期间 Dependabot PR 持续排队是预期行为而非切换失败;托管池恢复后它会自行完成。`blacksmith` 取值下的分支不带此类排除,因为 Blacksmith 运行器是临时的(见 [blacksmith 故障切换支路笔记](2026-09-09-blacksmith-failover-leg.zh.md))。 diff --git a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.i18n.yaml index 5b23d66658..c8bfb9d43c 100644 --- a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.i18n.yaml @@ -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-frozen-agent-note-archive.md -2026-07-26-frozen-agent-note-archive.md: 0c139c4a5d892de5bdace76b4935edf32586c4c1 -2026-07-26-frozen-agent-note-archive.zh.md: 2c2751c56a9b20423b00eac7a90c02286ce3e8b0 +2026-07-26-frozen-agent-note-archive.md: e9ecfa78f53a5912608201654e2c56e399defccd +2026-07-26-frozen-agent-note-archive.zh.md: 9a3ce2e6e21864f2754f627be6f7576bfc1f9076 diff --git a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md index 0c139c4a5d..e9ecfa78f5 100644 --- a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md +++ b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.md @@ -10,9 +10,9 @@ Implemented Agent Notes are maintained as current decision records, so every pat ## Decision -Only implemented Agent Notes can be archived. An implemented note moves when its shipped decision is complete and its rationale, alternatives, consequences, negative guarantees, and reintroduction conditions are unlikely to guide future work. Foundational boundaries, durable and wire semantics, security rules, recurring design temptations, and unresolved reintroduction conditions remain active regardless of age or word count. Proposed notes never enter the archive; an obsolete proposal becomes rejected. A rejected note remains only while it prevents a tempting, meaningful mistake and is otherwise deleted as a complete triplet. +Implemented notes that only describe small UI adjustments or purely mechanical changes are deleted as complete triplets, with inbound links repaired or removed. A small implementation can still carry lasting naming, compatibility, ownership, or behavior decisions; those records remain eligible for retention. Only other implemented Agent Notes can be archived: the shipped decision is complete, its rationale is unlikely to guide future work, and it still has historical decision value. Foundational boundaries, durable and wire semantics, security rules, recurring design temptations, and unresolved reintroduction conditions remain active regardless of age or word count. Proposed notes never enter the archive; an obsolete proposal becomes rejected. A rejected note remains only while it prevents a tempting, meaningful mistake and is otherwise deleted as a complete triplet. -The archive uses `.agents/notes/archived/{kind}/yyyy-mm-dd-topic.md`; the redundant `implemented` segment is absent. The archival change moves the complete English, Chinese, and consistency-sidecar triplet, leaves `Status: implemented` intact, and inserts `Archived: YYYY-MM-DD` immediately below it in both language files. Relocation, that metadata line, the corresponding sidecar re-record, and mechanical inbound-link repair are the only permitted archival edits. +The archive uses `.agents/notes/archived/{kind}/yyyy-mm-dd-topic.md`; the redundant `implemented` segment is absent. The archival change moves the complete English, Chinese, and consistency-sidecar triplet, leaves `Status: implemented` intact, and inserts `Archived: YYYY-MM-DD` immediately below it in both language files. Relocation, that metadata line, the corresponding sidecar re-record, and mechanical inbound-link repair are the only permitted archival edits. Archival admission checks lifecycle metadata and completeness, not title punctuation, blank-line layout, or language-switcher wording; requiring those repairs would rewrite the historical record. The root `.rgignore` excludes the archive from searches that traverse a parent directory. Historical queries name the archive directory explicitly, so intentional access remains available without mixing frozen facts into active decision discovery. @@ -42,4 +42,4 @@ Supersession is checked while a new Agent Note is being written, not deferred to ## Consequences -The active corpus becomes a set of decisions expected to influence future work, while low-value implemented history remains explicitly searchable and linkable without consuming maintenance attention or appearing in parent-directory searches. Writing a new note includes a scoped supersession check, so replacement decisions cannot silently leave redundant active records behind. Rejected clutter can disappear when it no longer protects a meaningful choice, and proposed work cannot quietly evade a verdict through archival. The archive adds a manifest, a dedicated verifier, and an explicit one-time metadata step. Archived facts and outbound links can become stale by design, so readers and agents must treat active code and documentation as authority and cite an archived note only as history. +The active corpus becomes a set of decisions expected to influence future work, while substantive historical decisions with little future guidance remain explicitly searchable and linkable without consuming maintenance attention or appearing in parent-directory searches. Writing a new note includes a scoped supersession check, so replacement decisions cannot silently leave redundant active records behind. Rejected clutter can disappear when it no longer protects a meaningful choice, and proposed work cannot quietly evade a verdict through archival. The archive adds a manifest, a dedicated verifier, and an explicit one-time metadata step. Archived facts and outbound links can become stale by design, so readers and agents must treat active code and documentation as authority and cite an archived note only as history. diff --git a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.zh.md b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.zh.md index 2c2751c56a..9a3ce2e6e2 100644 --- a/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-frozen-agent-note-archive.zh.md @@ -10,9 +10,9 @@ implemented Agent Note 作为当前决策记录持续维护,因此活跃记录 ## 决策 -只有 implemented Agent Note 可以归档。当一份已实施记录的交付决策已经完整落地,且其决策依据、备选方案、后果、否定性保证和重新引入条件不太可能再指导未来工作时,将其移入归档。基础性边界、持久语义与协议语义、安全规则、反复出现且看似诱人的设计选择和尚未解决的重新引入条件,无论记录的存续时间或字数如何,都继续作为活跃记录保留。proposed Agent Note 绝不进入归档;过时的提案应转为 rejected。仅当 rejected Agent Note 仍能避免一种诱人且影响重大的错误时保留,否则将其三个配对文件完整删除。 +仅描述小型 UI 调整或纯机械变更的 implemented Agent Note 会作为完整的三文件配对删除,并修复或移除入站链接。小规模实现仍可能承载长期有效的命名、兼容性、归属或行为决策;此类记录仍可保留。只有其他 implemented Agent Note 可以归档:交付决策已经完整落地,其依据不太可能再指导未来工作,但仍具有历史决策价值。基础性边界、持久语义与协议语义、安全规则、反复出现且看似诱人的设计选择和尚未解决的重新引入条件,无论记录的存续时间或字数如何,都继续作为活跃记录保留。proposed Agent Note 绝不进入归档;过时的提案应转为 rejected。仅当 rejected Agent Note 仍能避免一种诱人且影响重大的错误时保留,否则将其三个配对文件完整删除。 -归档路径为 `.agents/notes/archived/{kind}/yyyy-mm-dd-topic.md`,其中省略了冗余的 `implemented` 层级。归档变更会移动完整的英文、中文和一致性伴随记录三个文件,保留 `Status: implemented`,并在两种语言的文件中紧接该状态行插入 `Archived: YYYY-MM-DD`。归档时只允许做文件迁移、添加该元数据行、相应地重新记录伴随记录,以及机械修复入站链接。 +归档路径为 `.agents/notes/archived/{kind}/yyyy-mm-dd-topic.md`,其中省略了冗余的 `implemented` 层级。归档变更会移动完整的英文、中文和一致性伴随记录三个文件,保留 `Status: implemented`,并在两种语言的文件中紧接该状态行插入 `Archived: YYYY-MM-DD`。归档时只允许做文件迁移、添加该元数据行、相应地重新记录伴随记录,以及机械修复入站链接。 归档准入检查生命周期元数据与完整性,不检查标题标点、空行布局或语言切换栏措辞;要求修复这些格式会改写历史记录。 根目录的 `.rgignore` 会将归档目录排除在从上层目录开始的搜索之外。查找历史内容时会显式指定归档目录,因此仍可按需访问,同时不会把冻结事实混入对活跃决策的检索。 @@ -42,4 +42,4 @@ implemented Agent Note 作为当前决策记录持续维护,因此活跃记录 ## 后果 -活跃记录集合由预计仍会影响未来工作的决策组成;未来指导价值较低的实施历史仍可显式搜索和链接,却不再消耗维护精力,也不会出现在从上层目录开始的搜索中。编写新记录时会包含一项范围明确的取代关系检查,因此取代既有决策的新决策无法悄然留下冗余的活跃记录。当被否决的记录不再保护有意义的选择时,可以清除这类杂项;提案也无法通过归档悄悄逃避明确结论。归档机制增加一份 manifest、一个专用校验器和一个显式的一次性元数据步骤。归档中的事实和出站链接可以按设计逐渐陈旧,因此读者和 agent(智能体)必须以活跃代码与文档为权威依据,并且仅将归档 Agent Note 作为历史引用。 +活跃记录集合由预计仍会影响未来工作的决策组成;未来指导价值较低但具有实质内容的历史决策仍可显式搜索和链接,却不再消耗维护精力,也不会出现在从上层目录开始的搜索中。编写新记录时会包含一项范围明确的取代关系检查,因此取代既有决策的新决策无法悄然留下冗余的活跃记录。当被否决的记录不再保护有意义的选择时,可以清除这类杂项;提案也无法通过归档悄悄逃避明确结论。归档机制增加一份 manifest、一个专用校验器和一个显式的一次性元数据步骤。归档中的事实和出站链接可以按设计逐渐陈旧,因此读者和 agent(智能体)必须以活跃代码与文档为权威依据,并且仅将归档 Agent Note 作为历史引用。 diff --git a/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.i18n.yaml b/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.i18n.yaml deleted file mode 100644 index 37bf7d8e0c..0000000000 --- a/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.i18n.yaml +++ /dev/null @@ -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/implemented/process/2026-09-07-issue-policy-module-ownership.md -2026-09-07-issue-policy-module-ownership.md: 696343ddffa3f6a6e411330d038bb4a470ce78d8 -2026-09-07-issue-policy-module-ownership.zh.md: abe539a5baed35cf2f356194eab8e53d61b67314 diff --git a/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.md b/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.md deleted file mode 100644 index 696343ddff..0000000000 --- a/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: Issue policy module ownership - -Status: implemented - -English | [中文](2026-09-07-issue-policy-module-ownership.zh.md) - -## Problem - -Issue validation and Project lifecycle processing use the same reference parser, metadata rules, and GitHub reads, but they make different decisions and perform different writes. Keeping those responsibilities in the command entry makes it harder to reuse policy rules without also depending on event dispatch and output handling. - -## Decision - -Issue management separates pure decisions, GitHub access, pull-request evaluation, lifecycle mutations, and command dispatch into owner modules. [The owner reference](../../../../.github/issue-management/README.md#module-ownership) maps those responsibilities to source files. Callers and tests import the module that owns the operation rather than using the command entry as an export collection. - -The separation preserves policy results, diagnostics, credentials, API requests, lifecycle mutations, and workflow entry commands. [Selective evaluation](2026-09-07-selective-issue-policy-evaluation.md) remains the independent owner of eligibility, Project-read selection, and event scheduling; this decision does not redefine those behaviors. - -## Alternatives considered - -**Keep one command module.** A single file avoids imports between local owners, but ties reusable decisions and GitHub access to command handling. Separate owners let PR validation and lifecycle processing share their existing rules and reads without sharing command dispatch. - -## Consequences - -Maintainers can locate a rule, network operation, or event handler by responsibility. Reuse occurs through ordinary local ESM imports, not a new package or plugin API. The added modules introduce imports that must stay coordinated when a shared function changes. - -Module separation does not grant stronger credentials, change check authority, serialize Project mutations, or prevent a future behavior regression. Existing lifecycle races and field-management limitations remain documented by their behavior owners. - -## Verification - -[Policy tests](../../../../.github/issue-management/policy.test.mjs) exercise the owner modules and check observable policy and lifecycle behavior. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) cover the command wiring and workflow declarations. Their evidence concerns the checked implementation, not a guarantee that later edits preserve behavior. diff --git a/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.zh.md b/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.zh.md deleted file mode 100644 index abe539a5ba..0000000000 --- a/.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.zh.md +++ /dev/null @@ -1,29 +0,0 @@ -# Agent Note: Issue 策略模块归属 - -Status: implemented - -[English](2026-09-07-issue-policy-module-ownership.md) | 中文 - -## 问题 - -Issue 校验与 Project 生命周期处理使用相同的引用解析器、元数据规则和 GitHub 读取,但各自作出不同决策、执行不同写入。将这些职责放在命令入口中,会使复用策略规则时更难摆脱对事件分派与输出处理的依赖。 - -## 决策 - -Issue 管理将纯决策、GitHub 访问、PR(Pull Request)求值、生命周期 mutation 和命令分派分配给各自的所属模块。[所属参考文档](../../../../.github/issue-management/README.zh.md#module-ownership)将这些职责对应到源文件。调用方和测试直接导入拥有该操作的模块,不把命令入口用作导出集合。 - -此职责分离保留策略结果、诊断、凭据、API 请求、生命周期 mutation 和工作流入口命令。[选择性求值](2026-09-07-selective-issue-policy-evaluation.zh.md)仍独立拥有强制范围、Project 读取选择和事件调度规则;本决策不重新定义这些行为。 - -## 考虑过的替代方案 - -**保留单一命令模块。** 单文件可以避免本地所属模块之间的导入,但会把可复用的决策与 GitHub 访问绑定到命令处理。独立的所属模块让 PR 校验和生命周期处理可以共享既有规则与读取,而无需共享命令分派。 - -## 影响 - -维护者可以按职责定位规则、网络操作或事件处理器。复用通过普通的本地 ESM 导入完成,而不是新增包或插件 API。新增模块引入的导入关系需要在共享函数变更时同步维护。 - -模块分离不会授予更强的凭据、更改检查权威来源、串行化 Project mutation,也不能防止未来的行为回归。既有生命周期竞态与字段管理限制仍由各自的行为文档记录。 - -## 验证 - -[策略测试](../../../../.github/issue-management/policy.test.mjs)运行所属模块,并检查可观察的策略与生命周期行为。[工作流测试](../../../../scripts/ci-workflow.spec.ts)覆盖命令接线与工作流声明。这些证据针对被检查的实现,不保证后续编辑仍保留行为。 diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml index 2b32ebfee1..2c5a3f8554 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml @@ -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-09-15-shipped-optional-bundles.md -2026-09-15-shipped-optional-bundles.md: 40ef69d3880f8d1c925c1e3c423e59a628f22e09 -2026-09-15-shipped-optional-bundles.zh.md: adf2b979b9d23b7510c077f3633e2d030732a7b4 +2026-09-15-shipped-optional-bundles.md: c9f560f5c07ec6fb18030b77a0d1f53bd27dc045 +2026-09-15-shipped-optional-bundles.zh.md: e91b6580e29cd7ae142c9c61ed6aac7e5e1476eb diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md index 40ef69d388..c9f560f5c0 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md @@ -14,7 +14,7 @@ The launcher names in `OPTIONAL_BUNDLES` (`packages/boot/app-boot/src/profile.ts Default-product isolation keeps its rules with one declared exception: an optional bundle's dependency graph is outside the default product. The static gate skips the `dependencies` edge from `@deepseek-ai/dsh` to a listed bundle and still rejects a runtime import, a shipped composition, a preset, or a default template that names it, an experimental dependency the list does not name, and a listed name that is not a runtime dependency or not a bundle. The workspace-constraints check accepts the same `dependencies` edges and no other runtime section, and the packed-install release check skips them from the installed entry package while requiring each listed bundle to be installed. -Agent Teams ships this way, as `@deepseek-ai/dsh-experimental-agent-team-profile` and `@deepseek-ai/dsh-experimental-agent-team-web-profile`. Auto review is a published experimental package the page's install guide names as its example, not an optional bundle. +The [single-bundle decision](../architecture/2026-09-18-agent-teams-single-bundle.md) supersedes the two Agent Teams selections with one `@deepseek-ai/dsh-experimental-agent-team-profile` selection. This note retains the installation-owned optional-bundle policy. Auto review is a published experimental package the page's install guide names as its example, not an optional bundle. ## Alternatives considered @@ -26,4 +26,4 @@ Agent Teams ships this way, as `@deepseek-ai/dsh-experimental-agent-team-profile ## Consequences -Optional bundles are downloaded with the product and stay inactive until selected; the runtime isolation smokes still observe no experimental module in a default composition. Switching on an optional Web layer loads its client plugin through the live client module graph. A bundle that needs a companion layer, such as the Agent Teams Web layer over its Host layer, says so in its description; the manager does not select companions automatically. +Optional bundles are downloaded with the product and stay inactive until selected; the runtime isolation smokes still observe no experimental module in a default composition. Switching on an optional Web layer loads its client plugin through the live client module graph. diff --git a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md index adf2b979b9..e91b6580e2 100644 --- a/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md +++ b/.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md @@ -14,7 +14,7 @@ Web 插件页只管理用户装进 profile 的组合包。像 Agent Teams 这样 默认产品隔离的规则保持不变,只声明一个例外:可选组合包的依赖图在默认产品之外。静态门禁跳过从 `@deepseek-ai/dsh` 到列表中组合包的 `dependencies` 边,仍然拒绝运行时 import、随附组合、preset 或默认模板对它的引用,拒绝列表没有点名的实验依赖,也拒绝不是运行时依赖或不是组合包的列表项。workspace 约束检查接受同样的 `dependencies` 边而不接受其他运行时依赖段;发布时的 packed-install 检查对已安装入口包跳过这些边,并要求列表中的每个组合包都已安装。 -Agent Teams 以这种方式交付,即 `@deepseek-ai/dsh-experimental-agent-team-profile` 与 `@deepseek-ai/dsh-experimental-agent-team-web-profile`。Auto review 是一个已发布的实验包,插件页的安装引导拿它作示例,它不是可选组合包。 +[单组合包决策](../architecture/2026-09-18-agent-teams-single-bundle.zh.md)将两个 Agent Teams 选项替换为一个 `@deepseek-ai/dsh-experimental-agent-team-profile` 选项。本记录保留由安装方决定可选组合包的策略。Auto review 是一个已发布的实验包,插件页的安装引导拿它作示例,它不是可选组合包。 ## 考虑过的替代方案 @@ -26,4 +26,4 @@ Agent Teams 以这种方式交付,即 `@deepseek-ai/dsh-experimental-agent-tea ## 影响 -可选组合包随产品一起下载,选中前保持不活动;运行时隔离 smoke 在默认组合中仍观察不到任何实验模块。开启一个可选 Web 层会通过实时客户端模块图加载它的客户端插件。需要配套层的组合包,例如 Agent Teams Web 层依赖其 Host 层,会在描述里说明;管理器不会自动选中配套层。 +可选组合包随产品一起下载,选中前保持不活动;运行时隔离 smoke 在默认组合中仍观察不到任何实验模块。开启一个可选 Web 层会通过实时客户端模块图加载它的客户端插件。 diff --git a/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.i18n.yaml b/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.i18n.yaml deleted file mode 100644 index 5c2380b3d6..0000000000 --- a/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.i18n.yaml +++ /dev/null @@ -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/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.md -2026-09-17-plugin-inventory-phase-dot.md: e90d49b6a3d2d8e03f662b3de531ef180d350cad -2026-09-17-plugin-inventory-phase-dot.zh.md: d344ede75c7e0dd77a0d57720e0c1366f22e75b8 diff --git a/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.md b/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.md deleted file mode 100644 index e90d49b6a3..0000000000 --- a/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: The plugin inventory reserves its status dot for phases the tag cannot state - -Status: implemented - -English | [中文](2026-09-17-plugin-inventory-phase-dot.zh.md) - -## Problem - -The Settings plugin list marks every Loader entry with an enablement tag (`已启用`, `已停用`, `条件启用`, `预设中启用`, `启动失败`). It also drew a colored root-fiber status dot on enabled rows whose fiber was live. For the common `active` phase the dot and the tag stated the same thing twice on one collapsed card, while `pending`, `loading` and `unloading` — the phases the tag has no words for — were where the dot carried its own information. - -## Decision - -`PluginInventorySettingsTab` renders the status dot only for `pending`, `loading` and `unloading`. `active` and `failed` render no dot: a settled enabled row shows its enablement tag alone, and a failed row has the failure tag and card treatment. `PHASE_DOT_STATES` lists exactly the dotted phases, and `showsPhaseDot` narrows a phase to that union. - -Row enablement, tag tones, grouping, search and list behavior are unchanged, as are the Loader lifecycle and persistence. The expanded card still lists the phase under 运行状态 / Status, so the runtime phase stays readable without the collapsed dot. - -## Alternatives considered - -**Remove the dot for every phase.** Simpler to state, but it drops the only collapsed-state signal that a plugin is still loading or unloading — the one state the tag cannot express. - -**Keep the `active` dot on preset rows only.** The preset group is where the duplication was first noticed, but the global group renders the same tag-and-dot pair. A per-group exception would make one rendering rule depend on which group a row sits in, and would leave the duplicate in the global group. - -**Fold the phase into the enablement tag.** The tag reports the config fact the Host publishes as `enabled`; adding a runtime phase would make one control carry two lifecycles with different owners. - -## Consequences - -- A collapsed card no longer shows that an enabled entry's fiber is currently `active`. A reader who needs the phase expands the row, whose Status fact keeps it. That is the information the change deliberately gives up. -- Rows mid-transition keep their dot, its animation and its `role="img"` name, so the accessible name still says `等待依赖`, `加载中` or `卸载中`. -- Known gap, pre-existing and unchanged: the dot is gated on `row.enabled`, so a row a user just disabled while its fiber is still `unloading` shows `已停用` without a dot. - -## Testing - -The package spec pins the rule in both scopes: `loading` and `pending` (global) and `unloading` (preset) keep a dot, while `active`, `failed` and a row without a live fiber render none. `apps/web/tests/settings-chrome.e2e.ts` asserts the assembled global group contains enabled rows and no `role="img"` element named `运行中`. - -The remaining dots' palette, halo and animation, and the tag capsule itself, stay owned by [the shared client control decision](../architecture/2026-09-05-shared-client-control-primitives.md). diff --git a/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.zh.md b/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.zh.md deleted file mode 100644 index d344ede75c..0000000000 --- a/.agents/notes/implemented/simplification/2026-09-17-plugin-inventory-phase-dot.zh.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: 插件列表只为主题标签无法表达的阶段保留状态圆点 - -Status: implemented - -[English](2026-09-17-plugin-inventory-phase-dot.md) | 中文 - -## 问题 - -设置里的插件列表给每个 Loader 条目配一个启停标签(`已启用`、`已停用`、`条件启用`、`预设中启用`、`启动失败`),同时给 fiber 存活的已启用条目画一个彩色根 fiber 状态圆点。对最常见的 `active` 阶段,圆点与标签在同一张收起卡片上把同一件事说了两遍;而标签没有词可用的 `pending`、`loading`、`unloading`,才是圆点真正携带独立信息的阶段。 - -## 决策 - -`PluginInventorySettingsTab` 只为 `pending`、`loading`、`unloading` 渲染状态圆点。`active` 与 `failed` 不渲染:已稳定启用的条目只显示启停标签,失败条目由失败标签和卡片样式说明。`PHASE_DOT_STATES` 只列出有圆点的阶段,`showsPhaseDot` 把阶段收窄到该联合。 - -条目启停、标签色调、分组、搜索与列表行为不变,Loader 生命周期与持久化也不变。展开卡片仍在「运行状态」下给出阶段名,因此不画收起态圆点也能读到运行时阶段。 - -## 否决的备选方案 - -**所有阶段都不画圆点。** 规则更简单,但会丢掉收起态下唯一能说明插件正在加载或卸载的信号——恰恰是标签无法表达的那个状态。 - -**只在预设分组保留 `active` 圆点。** 重复最先是在预设分组被注意到的,但全局分组渲染的是同一套标签加圆点。按分组开例外会让同一条渲染规则取决于条目落在哪个分组,而且全局分组的重复仍然存在。 - -**把阶段并进启停标签。** 标签报告的是 Host 以 `enabled` 发布的配置事实;再塞一个运行时阶段会让一个控件承载两条归属不同的生命周期。 - -## 后果 - -- 收起卡片不再显示已启用条目的 fiber 当前处于 `active`。需要该阶段的读者展开条目,其「运行状态」事实仍然给出。这是本次变更刻意放弃的信息。 -- 过渡中的条目保留圆点、动画与 `role="img"` 名称,无障碍名称仍会读出 `等待依赖`、`加载中` 或 `卸载中`。 -- 既有且未改变的缺口:圆点以 `row.enabled` 为前提,用户刚停用而 fiber 仍在 `unloading` 的条目只显示 `已停用`,没有圆点。 - -## 测试 - -包级用例在两个作用域钉住规则:`loading` 与 `pending`(全局)、`unloading`(预设)保留圆点,`active`、`failed` 与没有存活 fiber 的条目都不渲染。`apps/web/tests/settings-chrome.e2e.ts` 断言装配后的全局分组含有已启用条目,且没有名为 `运行中` 的 `role="img"` 元素。 - -其余圆点的配色、光晕与动画,以及标签胶囊本身,仍由[共享客户端控件决策](../architecture/2026-09-05-shared-client-control-primitives.zh.md)拥有。 diff --git a/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.i18n.yaml b/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.i18n.yaml new file mode 100644 index 0000000000..35fe5b54d9 --- /dev/null +++ b/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.i18n.yaml @@ -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/proposed/simplification/2026-09-17-defer-office-background-scheduling.md +2026-09-17-defer-office-background-scheduling.md: 82311022a015671b9fb4cf3a0f3eba2714f325cd +2026-09-17-defer-office-background-scheduling.zh.md: ada0146f204fa40e71d48251c231c1b68c5fa5a1 diff --git a/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.md b/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.md new file mode 100644 index 0000000000..82311022a0 --- /dev/null +++ b/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.md @@ -0,0 +1,40 @@ +# Agent Note: Defer Office background scheduling until a consumer needs it + +Status: proposed + +English | [中文](2026-09-17-defer-office-background-scheduling.zh.md) + +## Problem + +The [conversion queue](../../../../packages/document/office-to-pdf/src/queue.ts) implements foreground promotion, demotion after reader cancellation, eviction of queued background work, a reserved foreground reader allowance, and separate background concurrency accounting. These mechanisms protect foreground conversion from speculative prewarming; ordinary conversion still needs the queue's general resource limits. + +The [Office preview](../../../../packages/client/ui-sidebar-documentpreview/src/client/office/index.ts) calls its cache without a priority argument, so the [Client cache](../../../../packages/client/ui-sidebar-documentpreview/src/client/office/cache.ts) forwards the default foreground priority to the Host. Repository searches for `officeToPdf`, `OfficeToPdfRequest`, `OfficeToPdfPriority`, and `maxBackgroundConversions` find no production background request. The [conversion smoke](../../../../packages/bundle/web-app/tests/document-conversion.e2e.ts) also submits foreground requests. Background requests appear in queue, Remote, and Client-cache tests. The exposed API and Client priority plumbing alone do not establish which scheduling policy prewarming requires. The open delivery prewarming PR #4064 already provides a concrete prewarming consumer, but its implementation calls the older `documentRender` API without priority. Removing scheduling must first be reconciled with that consumer; absence from master is insufficient evidence to proceed. + +The [bounded conversion decision](../../implemented/architecture/2026-09-15-bounded-office-conversion.md) deliberately reserves capacity for foreground consumers. That remains the correct rule if speculation exists, but the current caller inventory does not justify maintaining the speculative request class and its lifecycle interactions. + +## Proposal + +Remove `OfficeToPdfPriority`, request `priority`, and configuration `maxBackgroundConversions`. Remove reader/job priority fields, promotion and demotion, speculative eviction, foreground reader reservation, and the queue's background counter. Remove Client-cache priority plumbing and update the Remote signature together. Admit the oldest queued job whose source reservation fits whenever conversion capacity is available; reject a new distinct job when the metadata queue is full. Continue joining an existing source job subject to the shared reader limit. + +Keep the provider, reusable converters, deferred authorized reads, all remaining resource limits, source-version checks, content sharing, bounded result cache, independent reader cancellation, and teardown that waits for actual work to settle. This proposal changes admission policy, not conversion ownership or engine selection. + +Update the request/config definitions, package README pair, subsystem and generated catalogs, and conversion fixtures together. Delete priority-only assertions; retain resource and cancellation regressions embedded in mixed priority tests by expressing them with ordinary requests. The existing bounded conversion note remains active because its cache, authorization, and lifetime rationale still applies; acceptance would replace only its scheduling policy. + +## Alternatives considered + +**Keep priority for a future prewarm consumer.** This avoids a later API change, but retains scheduling states and configuration before a product consumer can define their required behavior. Reintroduce priority with a concrete prewarming caller and measured foreground contention. + +**Disable background conversions by default.** This avoids speculative execution but retains the public request class, configuration, and all scheduling branches. + +**Remove the whole shared provider or cache.** That exceeds the evidence: independent conversion and bounded sharing remain coherent capabilities, and the Loader smoke exercises them without presentation plugins. + +## Acceptance criteria + +- Resolve the scheduling needs of delivery prewarming PR #4064 before implementation, then repeat the caller search. Retain priority if that consumer needs foreground protection; do not remove it merely because the consumer has not merged. +- Priority types, fields, configuration, and scheduling branches disappear from maintained source, tests, documentation, and generated catalogs. Decision records may describe the removed policy. +- Focused queue/provider tests preserve bounded admission, source/digest sharing, cancellation isolation, output ownership, and delayed capacity release. The real conversion Loader smoke still passes. +- Verify the foreground Office preview path; update its recorded-session snapshot if affected. Run the relevant type checks, generated-reference checks, documentation checks, and lint. + +## Risks + +Out-of-tree callers can use the exposed pre-stable API even though repository searches find no background consumer. Removing priority and its config field requires callers to update; this proposal offers no compatibility alias. Speculative callers would lose foreground protection and must stop prewarming until an explicit scheduling requirement is implemented. No runtime behavior changes until this proposal is accepted and implemented. diff --git a/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.zh.md b/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.zh.md new file mode 100644 index 0000000000..ada0146f20 --- /dev/null +++ b/.agents/notes/proposed/simplification/2026-09-17-defer-office-background-scheduling.zh.md @@ -0,0 +1,40 @@ +# Agent Note: 待调用方需要时再引入 Office 后台调度 + +Status: proposed + +[English](2026-09-17-defer-office-background-scheduling.md) | 中文 + +## 问题 + +[转换队列](../../../../packages/document/office-to-pdf/src/queue.ts) 实现了前台优先级提升、读取方取消后的降级、排队后台任务的驱逐、预留前台读取名额,以及独立的后台并发计数。这些机制保护前台转换不受推测性预热影响;普通转换仍需要队列的通用资源限制。 + +[Office 预览](../../../../packages/client/ui-sidebar-documentpreview/src/client/office/index.ts) 调用缓存时未传优先级,因此[Client 缓存](../../../../packages/client/ui-sidebar-documentpreview/src/client/office/cache.ts) 向 Host 转发默认前台优先级。在仓库中搜索 `officeToPdf`、`OfficeToPdfRequest`、`OfficeToPdfPriority` 和 `maxBackgroundConversions`,未发现生产后台请求。[转换冒烟测试](../../../../packages/bundle/web-app/tests/document-conversion.e2e.ts) 也提交前台请求。后台请求出现在队列、Remote 和 Client 缓存测试中。仅凭暴露 API 和 Client 优先级传递逻辑,无法确定预热需要哪种调度策略。开放的交付文件预热 PR #4064已经提供具体预热消费者,但其实现仍调用不含优先级的旧 `documentRender` API。删除调度前必须先与该消费者协调;尚未进入 master 不足以支持直接实施。 + +[有界转换决策](../../implemented/architecture/2026-09-15-bounded-office-conversion.zh.md) 有意为前台消费者预留容量。存在推测性任务时,这条规则仍然正确,但当前调用方清单不足以支持维护推测性请求类别及其生命周期交互。 + +## 提案 + +移除 `OfficeToPdfPriority`、请求中的 `priority` 和配置 `maxBackgroundConversions`。删除读取方及任务的优先级字段、升降级、推测性任务驱逐、前台读取名额预留,以及队列的后台计数器。同时删除 Client 缓存的优先级传递逻辑并更新 Remote 签名。在转换容量可用时,接纳源预留容量足够的最早排队任务;元数据队列满时拒绝新的独立任务。仍允许在共享读取方数量限制内加入已有源任务。 + +保留提供者、可复用转换器、延迟授权读取、其余所有资源限制、源版本校验、内容共享、有界结果缓存、读取方独立取消,以及等待实际工作结束的清理。这项提案改变接纳策略,不改变转换所有权或引擎选择。 + +同步更新请求及配置定义、包 README 双语文档、子系统与生成目录,以及转换 fixture(测试前置数据)。删除仅针对优先级的断言;将混合优先级测试中的资源与取消回归改写为普通请求,并保留覆盖。现有有界转换记录继续有效,因为其缓存、授权和生命周期理由仍然成立;采纳本提案只会替换其中的调度策略。 + +## 考虑过的替代方案 + +**为未来的预热调用方保留优先级。** 这可以避免后续 API 变更,但在产品调用方能够定义所需行为之前,就保留了调度状态与配置。应在有具体预热调用方和实测前台资源争用时重新引入优先级。 + +**默认禁用后台转换。** 这可以避免推测性执行,但仍保留公开请求类别、配置及全部调度分支。 + +**删除整个共享提供者或缓存。** 这超出了证据支持的范围:独立转换和有界共享仍是完整的能力,Loader 冒烟测试也在不依赖展示插件的情况下验证它们。 + +## 验收标准 + +- 实施前先明确交付文件预热 PR #4064 的调度需求,再重复调用方搜索。若该消费者需要前台保护,则保留优先级;不能仅因消费者尚未合入而删除。 +- 从维护中的源码、测试、文档和生成目录移除优先级类型、字段、配置及调度分支。决策记录可以描述被移除的策略。 +- 聚焦队列及提供者的测试保留有界接纳、源及摘要共享、取消隔离、输出所有权和延迟容量释放覆盖。真实转换的 Loader 冒烟测试仍然通过。 +- 验证前台 Office 预览路径;若受影响,同步更新其记录会话快照。运行相关类型检查、生成引用检查、文档检查及 lint。 + +## 风险 + +仓库外调用方可能使用已暴露的稳定前 API,即使仓库搜索未发现后台消费者。移除优先级及其配置字段要求调用方更新;本提案不提供兼容别名。推测性调用方会失去前台保护,必须停止预热,直到明确的调度需求得到实现。本提案被接受并实现之前,运行行为不变。 diff --git a/.agents/skills/dsh-archive-agent-notes/SKILL.md b/.agents/skills/dsh-archive-agent-notes/SKILL.md index a08e4b5179..6052cbf1fc 100644 --- a/.agents/skills/dsh-archive-agent-notes/SKILL.md +++ b/.agents/skills/dsh-archive-agent-notes/SKILL.md @@ -1,6 +1,6 @@ --- name: dsh-archive-agent-notes -description: Use when adding, auditing, pruning, archiving, restoring, or reviewing Agent Notes in deepseek-harness; checks every new note for superseded active records, classifies implemented notes by future decision value, deletes rejected notes that no longer prevent a tempting fallacy, and applies the frozen archived/{kind} triplet and manifest rules. +description: Use when adding, auditing, pruning, archiving, restoring, or reviewing Agent Notes in deepseek-harness; checks every new note for superseded active records, deletes small UI and purely mechanical records, classifies other implemented notes by future decision value, deletes rejected notes that no longer prevent a tempting fallacy, and applies the frozen archived/{kind} triplet and manifest rules. --- # Archive DeepSeek Harness Agent Notes @@ -19,8 +19,9 @@ Before drafting, apply the [creation criteria](../../notes/README.md#when-to-wri Apply these lifecycle-specific outcomes: -- **Implemented — keep active:** retain a note when its rationale, alternatives, negative guarantees, durable/wire semantics, ownership boundary, security rule, or reintroduction condition is likely to guide a future change. Length does not matter. -- **Implemented — archive:** archive a note when the shipped decision is complete and its body is unlikely to guide future work, such as one-off UI chrome, a narrow adapter, a minor closed bug, superseded implementation detail, or process history whose current behavior is obvious elsewhere. +- **Implemented — delete:** delete complete triplets that only describe small UI adjustments or purely mechanical changes, and repair or remove inbound links. Local bug fixes, performance changes, new capabilities, and substantive behavior or ownership decisions do not qualify merely because they are implemented. Apply this criterion before keep/archive classification. +- **Implemented — keep active:** retain a note when its rationale, alternatives, negative guarantees, durable/wire semantics, ownership boundary, security rule, or reintroduction condition is likely to guide a future change. Classify the decision, not the mechanics of its implementation: a mechanical rename or type extraction can still record lasting naming, compatibility, or ownership rules. Length does not matter. +- **Implemented — archive:** archive a substantive historical decision when it is complete and unlikely to guide future work, but its historical rationale still warrants preservation. Do not archive records that meet the direct-deletion criterion. - **Proposed — never archive:** keep a live proposal active; if it is no longer worth pursuing, reject it with an honest reason and satisfy the rejected lifecycle format. - **Rejected — keep only as a guardrail:** retain a rejection only when the losing proposal remains a tempting, meaningful mistake and the note explains why it loses. - **Rejected — delete:** delete the whole triplet when the rejected idea is obsolete, superseded, no longer plausible, or unlikely to prevent re-litigation. Repair or delete inbound links. @@ -31,11 +32,10 @@ Do not archive toward a quota. Inspect every note in scope, classify analogous g These examples set the bar; the word counts demonstrate that size is not the test. -Archive implemented notes such as: +Delete implemented notes such as: - collapsed sidebar control rail — 533 words: closed, minor UI behavior; -- Commander argument adapter — 1,498 words: substantial implementation detail with little future design leverage; -- documentation graph atlas — 920 words: completed documentation machinery whose current generators are authoritative. +- issue-policy module ownership — completed function relocation and import rewiring with unchanged behavior. Keep implemented notes such as: @@ -55,7 +55,7 @@ For rejected notes: 1. Move the complete `foo.md`, `foo.zh.md`, and `foo.i18n.yaml` triplet from `implemented//` to `archived//`; `implemented` is deliberately absent from the archive path. 2. Make no body edits. Insert only `Archived: YYYY-MM-DD` immediately below `Status: implemented` in both language files, using the archival date and the same value on both sides. -3. Re-record the sidecar hashes mechanically for the two metadata-only edits. Do not translate, reformat, update facts, or repair links inside the note. +3. Re-record the sidecar hashes mechanically for the two metadata-only edits. Do not translate, reformat, update facts, or repair links inside the note. Existing title punctuation, blank-line layout, and language-switcher wording are preserved, not prerequisites for archival. 4. Search for inbound links from active prose. Redirect them to current authority, retarget them to the archived path only when the historical snapshot is intentionally cited, or delete them. Never verify or repair links out of the archived note. 5. Run `pnpm run verify-archived-agent-notes --write`. Its append-only mode first proves every existing seal still matches, then adds only the new triplet hashes. Run the normal verifier afterward. @@ -65,4 +65,4 @@ After the triplet is sealed, never edit, move, translate, reformat, or delete it Run the archive verifier's focused test, `pnpm run verify-archived-agent-notes`, `pnpm run doc-sync`, `pnpm run lint`, and `git diff --check`; select any additional evidence through [dsh-pre-push-checks](../dsh-pre-push-checks/SKILL.md). -Report active implemented notes kept, implemented notes archived, rejected notes kept/deleted, proposed notes rejected if any, and every genuinely borderline case with its word count and chosen outcome. Do not claim archived outbound links are valid: the archive verifier intentionally never checks them. +Report active implemented notes kept, implemented notes deleted or archived, rejected notes kept/deleted, proposed notes rejected if any, and every genuinely borderline case with its word count and chosen outcome. Do not claim archived outbound links are valid: the archive verifier intentionally never checks them. diff --git a/.agents/skills/dsh-find-simplifications/SKILL.md b/.agents/skills/dsh-find-simplifications/SKILL.md index 6ee736796c..bce148ad18 100644 --- a/.agents/skills/dsh-find-simplifications/SKILL.md +++ b/.agents/skills/dsh-find-simplifications/SKILL.md @@ -93,7 +93,7 @@ Reject or downgrade a candidate when: Audit the Agent Note tree when the user asks to reduce or coalesce it, or when the simplification being implemented makes an owning note obsolete. Do not expand every code-simplification survey into a repository-wide note audit. -Use [`dsh-archive-agent-notes`](../dsh-archive-agent-notes/SKILL.md) for retention judgment and archive mechanics. Low-future-value implemented notes move as frozen triplets to `archived/{kind}`; proposed notes are never archived; rejected notes that no longer prevent a tempting mistake are deleted. Do not edit an archived note while simplifying current prose or code. +Use [`dsh-archive-agent-notes`](../dsh-archive-agent-notes/SKILL.md) for retention judgment and archive mechanics. Implemented notes that only describe small UI adjustments or purely mechanical changes are deleted directly; other completed notes with historical decision value but little future guidance move as frozen triplets to `archived/{kind}`; proposed notes are never archived; rejected notes that no longer prevent a tempting mistake are deleted. Do not edit an archived note while simplifying current prose or code. Follow the deletion rule in the [Agent Note rules](../../notes/README.md#when-to-write-one); do not duplicate or weaken it here. For each candidate chain: diff --git a/.github/issue-management/README.i18n.yaml b/.github/issue-management/README.i18n.yaml index 7d31f8418d..beaf888252 100644 --- a/.github/issue-management/README.i18n.yaml +++ b/.github/issue-management/README.i18n.yaml @@ -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 .github/issue-management/README.md -README.md: a936b9358810fd3a8b8104609a5523e305e1aaa5 -README.zh.md: df743cac581c2125df9ac9e76f04d2528204eb74 +README.md: 49e91859f4b22fda13d0e93c6e6ea492272f6951 +README.zh.md: 8424f90cd2e3a59ba1212f64f3f1081a984b55f8 diff --git a/.github/issue-management/README.md b/.github/issue-management/README.md index a936b93588..49e91859f4 100644 --- a/.github/issue-management/README.md +++ b/.github/issue-management/README.md @@ -61,7 +61,7 @@ Lifecycle processing is event-driven, not a reconciler. Omitted events do not re ## Module ownership -Maintainers reuse the owning module directly; [policy.mjs](policy.mjs) only reads the event file, dispatches commands, and reports command failures. The [module-ownership decision](../../.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.md) explains this separation. +Maintainers reuse the owning module directly; [policy.mjs](policy.mjs) only reads the event file, dispatches commands, and reports command failures.
Implementation owners diff --git a/.github/issue-management/README.zh.md b/.github/issue-management/README.zh.md index df743cac58..8424f90cd2 100644 --- a/.github/issue-management/README.zh.md +++ b/.github/issue-management/README.zh.md @@ -61,7 +61,7 @@ PR 打开时,工作流按配置时区中的 PR 创建日期,为每个被引 ## 模块归属 -维护者直接复用所属模块;[policy.mjs](policy.mjs)仅负责读取事件文件、分派命令并报告命令失败。[模块归属决策](../../.agents/notes/implemented/process/2026-09-07-issue-policy-module-ownership.zh.md)解释此职责分离。 +维护者直接复用所属模块;[policy.mjs](policy.mjs)仅负责读取事件文件、分派命令并报告命令失败。
实现职责 diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index 5042778b72..e53d524ead 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -152,14 +152,15 @@ jobs: if-no-files-found: error retention-days: 7 + # Blacksmith x64 CI builds use 2 vCPU for compilation, bundling, and wheel smokes. build: needs: [plan, sdk-wheel] name: ${{ matrix.target }} runs-on: >- ${{ inputs.ci && !inputs.release && matrix.target == 'node24-linux-x64' - && vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-16vcpu-ubuntu-2404' + && vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-2vcpu-ubuntu-2404' || inputs.ci && !inputs.release && matrix.target == 'node24-win-x64' - && vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-16vcpu-windows-2025' + && vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-2vcpu-windows-2025' || matrix.runner }} timeout-minutes: 45 strategy: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2139231c06..d6bb43c5f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: # vm-backup pool and re-running the failed jobs is the entire switch — # see .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md. # Setting the variable to 'blacksmith' instead routes the same jobs onto - # Blacksmith's hosted runners at the matching vCPU size (see + # Blacksmith's hosted runners sized per workload (see # .agents/notes/implemented/process/2026-09-09-blacksmith-failover-leg.md). The # in-house pool's readiness is re-proven on every master push by the # serial-linux-selfhosted standby lane in ci-master.yml. The Windows failover @@ -42,14 +42,15 @@ jobs: node-24: if: github.event_name == 'pull_request' runs-on: >- - ${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-16vcpu-ubuntu-2404' + ${{ vars.DSH_CI_FAILOVER_LINUX == 'blacksmith' && 'blacksmith-8vcpu-ubuntu-2404' || vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') || 'dsh-ubuntu-24-04-16core' }} name: node 24 / static env: - DSH_GATE_CONCURRENCY: '8' + # Shared hosts expose host CPUs, not an isolated allocation. + DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }} # Stop the aggregate at the first blocking gate failure so a red run # does not keep burning enterprise runner time on the remaining gates. DSH_GATE_FAIL_FAST: '1' @@ -116,11 +117,10 @@ jobs: || 'dsh-ubuntu-24-04-16core' }} name: node 24 / coverage env: - # Partitioning replaces the instrumented share; this budget gives the - # exempt-heavy gate two workers on both hosted and failover runners. - DSH_COVERAGE_MAX_WORKERS: '6' - DSH_COVERAGE_PARTITIONS: '4' - DSH_GATE_CONCURRENCY: '3' + # Shared hosts expose host CPUs, not an isolated allocation. + DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '6' || '' }} + DSH_COVERAGE_PARTITIONS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '4' || '' }} + DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '3' || '' }} # Managed-scope teardown cases run past the default per-test budget when # this lane's partitions, workers, and sibling gates share one host: the # disposal cases in subprocess-local and bash-sandbox exceeded 5000ms on @@ -246,17 +246,17 @@ jobs: || 'dsh-ubuntu-24-04-16core' }} name: node 24 / snapshots and artifacts env: - DSH_GATE_CONCURRENCY: '10' + # Shared hosts expose host CPUs, not an isolated allocation. + DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '10' || '' }} + DSH_OXLINT_THREADS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }} + DSH_PUBLINT_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }} + DSH_WEB_SNAPSHOT_WORKERS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '6' || '' }} + DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '' }} DSH_NODE_COMPAT_SKIP_TYPECHECK: '1' - DSH_OXLINT_THREADS: '8' - DSH_PUBLINT_CONCURRENCY: '8' - DSH_WEB_SNAPSHOT_WORKERS: '6' # A failing gate aborts its running siblings: a failing build stops the # independent Node compatibility smoke, and a failing reader (e.g. # publint) stops the remaining artifact consumers. DSH_GATE_FAIL_FAST: '1' - # Failover halves snapshot concurrency for the shared 64-core VM. - DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }} steps: # Runner cleanup owns tool caches and fixtures, including cancelled runs. - name: Use runner-owned temporary storage @@ -497,6 +497,8 @@ jobs: # per-job process count stays lower. Observational checks are non-blocking. # Dependabot PRs are excluded from the self-hosted pool and stay queued for # the hosted runner. + # Blacksmith uses 2 vCPU for file-serial native tests, 8 for observational + # gates, and 16 for blocking builds and coverage. windows-build: if: github.event_name == 'pull_request' runs-on: >- @@ -553,10 +555,11 @@ jobs: name: windows node 24 / coverage timeout-minutes: 120 env: - DSH_COVERAGE_MAX_WORKERS: '6' - DSH_COVERAGE_PARTITIONS: '4' + # Shared hosts expose host CPUs, not an isolated allocation. + DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '6' || '' }} + DSH_COVERAGE_PARTITIONS: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '4' || '' }} + DSH_GATE_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '3' || '' }} DSH_COVERAGE_TEST_TIMEOUT_MS: '90000' - DSH_GATE_CONCURRENCY: '3' # A gate failure aborts the sibling gate instead of waiting out its # multi-minute instrumented run. DSH_GATE_FAIL_FAST: '1' @@ -622,7 +625,7 @@ jobs: windows-native-tests: if: github.event_name == 'pull_request' runs-on: >- - ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-16vcpu-windows-2025' + ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-2vcpu-windows-2025' || vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && fromJSON('["self-hosted", "dsh-win-ci", "windows"]') @@ -672,7 +675,7 @@ jobs: if: github.event_name == 'pull_request' continue-on-error: true runs-on: >- - ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-16vcpu-windows-2025' + ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'blacksmith' && 'blacksmith-8vcpu-windows-2025' || vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && fromJSON('["self-hosted", "dsh-win-ci", "windows"]') @@ -680,7 +683,8 @@ jobs: name: windows node 24 / observational timeout-minutes: 60 env: - DSH_PUBLINT_CONCURRENCY: '8' + # Shared hosts expose host CPUs, not an isolated allocation. + DSH_PUBLINT_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '' }} steps: - uses: actions/checkout@v6 with: diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 7eb0c36e18..ec698dca33 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -76,6 +76,7 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`commander`](https://github.com/tj/commander.js) | MIT | | [`compression`](https://github.com/expressjs/compression) | MIT | | [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause | +| [`dompurify`](https://github.com/cure53/DOMPurify) | (MPL-2.0 OR Apache-2.0) | | [`electron-updater`](https://github.com/electron-userland/electron-builder) | MIT | | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT | | [`execa`](https://github.com/sindresorhus/execa) | MIT | diff --git a/apps/cli/package.json b/apps/cli/package.json index 8916642cfe..b3cff4af5d 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -48,7 +48,6 @@ "@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-experimental-agent-team-web-profile": "workspace:^", "@deepseek-ai/dsh-fs-local": "workspace:^", "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-goal-round-driver": "workspace:^", diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 2a06e0672b..6a39bb8146 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -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 apps/desktop/README.md -README.md: bb434a88325339c3670e06ab527da132cf01bf36 -README.zh.md: 3e71382627ddb66c56348cbaf002f9852cec4436 +README.md: 34a32f2bb3ac057c15738a8075c9a3f003c2129b +README.zh.md: c53d8adae9528c1b5b7bc6f71d143c20b3bf92dc diff --git a/apps/desktop/README.md b/apps/desktop/README.md index bb434a8832..34a32f2bb3 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -202,7 +202,7 @@ Mac packaging reads three tuning fields from `.env.macos`: The proxy fields are independent and reject URL credentials, paths, queries and fragments. Empty values preserve inherited networking. An explicit download proxy replaces child-process proxy variables and bypasses local hosts only; it does not change system settings. Windows and standalone `release:pack` retain their existing concurrency defaults. Company proxy addresses belong in the ignored local file; see internal documentation for addresses. -Apple tooling uses the active macOS network service's HTTP/HTTPS proxies. Configured notarization routing checks proxy reachability, saves that service's settings, enables the proxy around both artifact lanes, and restores the saved values after both lanes settle. Directory-only builds apply it to App notarization after the signed directory build. This temporarily affects other applications and requires permission to change system proxies; PAC, auto-discovery, SOCKS and authenticated proxy configurations must be disabled first. One per-user lock prevents overlapping proxy transactions across checkouts; other users and network-setting tools must not change these settings concurrently. SIGINT/SIGTERM wait for active work before restoration. After forced termination or a restoration error, stop any remaining notarization processes and run `pnpm --dir apps/desktop run restore:mac-proxy`; the saved record remains until restoration succeeds. Configuration checks validate URL syntax without changing system settings or contacting the proxy. +Apple tooling uses the active macOS network service's HTTP/HTTPS proxies. Configured notarization routing checks proxy reachability, saves that service's settings, enables the proxy around both artifact lanes, and restores the saved values after both lanes settle. Directory-only builds apply it to App notarization after the signed directory build. This temporarily affects other applications and requires permission to change system proxies; PAC, auto-discovery, SOCKS and authenticated proxy configurations must be disabled first. Packaging and recovery acquire the same per-user POSIX file lock before reading recovery data or changing proxies; process exit releases ownership, while the lock file remains in place. This prevents overlapping proxy transactions across checkouts; other users and network-setting tools must not change these settings concurrently. SIGINT/SIGTERM wait for active work before restoration. After forced termination or a restoration error, stop any remaining notarization processes and run `pnpm --dir apps/desktop run restore:mac-proxy`; the saved record remains until restoration succeeds. Configuration checks validate URL syntax without changing system settings or contacting the proxy. ### Unsigned Windows test installer diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 3e71382627..c53d8adae9 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -203,7 +203,7 @@ Mac 打包从 `.env.macos` 读取三个调优字段: 两个代理字段互相独立,拒绝 URL 中的凭证、路径、查询参数和片段。空值沿用继承的网络设置。显式下载代理会替换子进程的代理变量,仅绕过本地主机;它不修改系统设置。Windows 与独立 `release:pack` 保持现有并发默认值。公司代理地址仅写入 Git 忽略的本地文件;具体地址参见内部文档。 -Apple 工具使用 macOS 当前活动网络服务的 HTTP/HTTPS 代理。配置公证代理后,打包会检查代理可达性、保存该服务的设置,在两条产物任务期间启用代理,并在两条任务均结束后恢复原值。仅生成目录的打包会在签名目录构建完成后的 App 公证期间启用代理。这会临时影响其他应用,并要求修改系统代理的权限;必须先禁用 PAC、自动发现、SOCKS 及需要认证的代理配置。用户级锁阻止不同 checkout 的代理事务重叠;其他用户及网络设置工具不得同时修改这些设置。SIGINT/SIGTERM 会等待活动任务结束后恢复。强制终止或恢复失败后,先停止残留公证进程,再运行 `pnpm --dir apps/desktop run restore:mac-proxy`;保存的记录会保留到恢复成功。配置检查仅验证 URL 语法,不修改系统设置或连接代理。 +Apple 工具使用 macOS 当前活动网络服务的 HTTP/HTTPS 代理。配置公证代理后,打包会检查代理可达性、保存该服务的设置,在两条产物任务期间启用代理,并在两条任务均结束后恢复原值。仅生成目录的打包会在签名目录构建完成后的 App 公证期间启用代理。这会临时影响其他应用,并要求修改系统代理的权限;必须先禁用 PAC、自动发现、SOCKS 及需要认证的代理配置。打包和恢复在读取恢复记录或修改代理前获取同一个用户级 POSIX 文件锁;进程退出会释放锁的持有权,锁文件保留。这会阻止不同 checkout 的代理事务重叠;其他用户及网络设置工具不得同时修改这些设置。SIGINT/SIGTERM 会等待活动任务结束后恢复。强制终止或恢复失败后,先停止残留公证进程,再运行 `pnpm --dir apps/desktop run restore:mac-proxy`;保存的记录会保留到恢复成功。配置检查仅验证 URL 语法,不修改系统设置或连接代理。 ### 未签名 Windows 测试安装包 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 08ff31f066..94f5acb822 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -41,6 +41,7 @@ "devDependencies": { "@deepseek-ai/dsh-app-boot": "workspace:^", "@deepseek-ai/dsh-home-paths": "workspace:^", + "@deepseek-ai/node-addon-system": "workspace:^", "@electron/get": "^5.1.0", "@electron/notarize": "2.5.0", "@types/js-yaml": "^4.0.9", diff --git a/apps/desktop/scripts/macos-notarization-proxy.ts b/apps/desktop/scripts/macos-notarization-proxy.ts index 1da03c4d00..b4337b64d2 100644 --- a/apps/desktop/scripts/macos-notarization-proxy.ts +++ b/apps/desktop/scripts/macos-notarization-proxy.ts @@ -1,10 +1,11 @@ /** Temporarily route Apple tools through the active macOS service; retain recovery data before mutation. */ import { execFileSync } from 'node:child_process' -import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { connect } from 'node:net' import { homedir } from 'node:os' import { dirname, join, resolve } from 'node:path' +import { tryLockExclusive } from '@deepseek-ai/node-addon-system/flock' interface ProxyState { readonly enabled: boolean @@ -48,6 +49,20 @@ const operations: ProxyOperations = { }, } +async function withProxyLock(lock: string, action: () => Promise): Promise { + mkdirSync(dirname(lock), { recursive: true, mode: 0o700 }) + // Keep this inode across transactions: unlinking it would allow two independent locks. + const fd = openSync(`${lock}.flock`, 'a', 0o600) + try { + try { await tryLockExclusive(fd) } catch (error) { + const code = (error as NodeJS.ErrnoException).code + if (code !== 'EAGAIN' && code !== 'EWOULDBLOCK') throw error + throw new Error('desktop package: another run or proxy recovery is active; wait for it to finish') + } + return await action() + } finally { closeSync(fd) } +} + function readProxy(service: string, secure: boolean, ops: ProxyOperations): ProxyState { const output = ops.command(NETWORKSETUP, [secure ? '-getsecurewebproxy' : '-getwebproxy', service]) const enabled = /^Enabled: (Yes|No)$/mu.exec(output)?.[1] @@ -106,17 +121,15 @@ function readRecord(lock: string): ProxyRecord { * Restore an interrupted run only after its owner exits; active or incomplete records fail closed. * @param lock Per-user lock shared across checkouts; tests supply a private directory. * @param ops Host operations; tests supply isolated system settings. - * @returns Nothing; retains the record when restoration fails. + * @returns Resolves after restoration under the shared file lock; retains the record on failure. */ -export function restoreMacOSNotarizationProxy(lock: string = LOCK, ops: ProxyOperations = operations): void { - const record = readRecord(lock) - if (ops.ownerAlive(record.pid)) throw new Error('desktop package: proxy owner is still running; wait for packaging to finish') - const recoveryLock = join(lock, 'recovering') - mkdirSync(recoveryLock, { mode: 0o700 }) - try { +export async function restoreMacOSNotarizationProxy(lock: string = LOCK, ops: ProxyOperations = operations): Promise { + await withProxyLock(lock, async () => { + const record = readRecord(lock) + if (ops.ownerAlive(record.pid)) throw new Error('desktop package: proxy owner is still running; wait for packaging to finish') restore(record, ops) rmSync(lock, { recursive: true }) - } finally { rmSync(recoveryLock, { recursive: true, force: true }) } + }) } /** @@ -138,65 +151,66 @@ export async function withMacOSNotarizationProxy( if (proxyUrl === undefined) { report('not-used'); return action() } const url = new URL(proxyUrl) await ops.reachable(url) - mkdirSync(dirname(lock), { recursive: true, mode: 0o700 }) - try { mkdirSync(lock, { mode: 0o700 }) } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error - throw new Error(`desktop package: another run or interrupted proxy record exists; after its owner exits, run ${RECOVERY}`) - } - let original: ProxyRecord | undefined - let saved = false - let workError: unknown - let signal: string | undefined - const interrupted = (received: string) => { - signal = received - process.stderr.write('desktop package: waiting for notarization work to stop before restoring the system proxy\n') - } - process.on('SIGINT', interrupted) - process.on('SIGTERM', interrupted) - try { - const service = activeService(ops) - // PAC, discovery and SOCKS can override explicit proxies; do not silently change those policies. - const pac = ops.command(NETWORKSETUP, ['-getautoproxyurl', service]) - const discovery = ops.command(NETWORKSETUP, ['-getproxyautodiscovery', service]) - const socks = ops.command(NETWORKSETUP, ['-getsocksfirewallproxy', service]) - if (!/^Enabled: No$/mu.test(pac) || !/^Auto Proxy Discovery: Off$/mu.test(discovery) || !/^Enabled: No$/mu.test(socks)) { - throw new Error('desktop package: disable PAC, proxy discovery and SOCKS on the active service before configuring a notarization proxy') + return withProxyLock(lock, async () => { + try { mkdirSync(lock, { mode: 0o700 }) } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error + throw new Error(`desktop package: another run or interrupted proxy record exists; after its owner exits, run ${RECOVERY}`) } - original = { pid: process.pid, service, http: readProxy(service, false, ops), https: readProxy(service, true, ops) } - writeFileSync(join(lock, 'original.json'), `${JSON.stringify(original)}\n`, { flag: 'wx', mode: 0o600, flush: true }) - saved = true - report('restoration-pending') - const state = { enabled: true, server: url.hostname.replace(/^\[|\]$/gu, ''), port: Number(url.port || '80') } - setProxy(service, false, state, ops) - setProxy(service, true, state, ops) - report('enabled') - const result = await action() - if (signal) throw new Error(`desktop package: interrupted by ${signal}`) - return result - } catch (error) { - workError = error - throw error - } finally { + let original: ProxyRecord | undefined + let saved = false + let workError: unknown + let signal: string | undefined + const interrupted = (received: string) => { + signal = received + process.stderr.write('desktop package: waiting for notarization work to stop before restoring the system proxy\n') + } + process.on('SIGINT', interrupted) + process.on('SIGTERM', interrupted) try { - if (saved && original) { - try { restore(original, ops) } catch (error) { report('restore-failed'); throw error } - report('restored') + const service = activeService(ops) + // PAC, discovery and SOCKS can override explicit proxies; do not silently change those policies. + const pac = ops.command(NETWORKSETUP, ['-getautoproxyurl', service]) + const discovery = ops.command(NETWORKSETUP, ['-getproxyautodiscovery', service]) + const socks = ops.command(NETWORKSETUP, ['-getsocksfirewallproxy', service]) + if (!/^Enabled: No$/mu.test(pac) || !/^Auto Proxy Discovery: Off$/mu.test(discovery) || !/^Enabled: No$/mu.test(socks)) { + throw new Error('desktop package: disable PAC, proxy discovery and SOCKS on the active service before configuring a notarization proxy') } - rmSync(lock, { recursive: true }) + original = { pid: process.pid, service, http: readProxy(service, false, ops), https: readProxy(service, true, ops) } + writeFileSync(join(lock, 'original.json'), `${JSON.stringify(original)}\n`, { flag: 'wx', mode: 0o600, flush: true }) + saved = true + report('restoration-pending') + const state = { enabled: true, server: url.hostname.replace(/^\[|\]$/gu, ''), port: Number(url.port || '80') } + setProxy(service, false, state, ops) + setProxy(service, true, state, ops) + report('enabled') + const result = await action() + if (signal) throw new Error(`desktop package: interrupted by ${signal}`) + return result } catch (error) { - if (workError !== undefined) { - throw new AggregateError([workError, error], `desktop package: work and proxy cleanup failed; run ${RECOVERY}`) - } + workError = error throw error } finally { - process.off('SIGINT', interrupted) - process.off('SIGTERM', interrupted) + try { + if (saved && original) { + try { restore(original, ops) } catch (error) { report('restore-failed'); throw error } + report('restored') + } + rmSync(lock, { recursive: true }) + } catch (error) { + if (workError !== undefined) { + throw new AggregateError([workError, error], `desktop package: work and proxy cleanup failed; run ${RECOVERY}`) + } + throw error + } finally { + process.off('SIGINT', interrupted) + process.off('SIGTERM', interrupted) + } } - } + }) } if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) { if (process.platform !== 'darwin') throw new Error('desktop package: proxy recovery requires macOS') - restoreMacOSNotarizationProxy() + await restoreMacOSNotarizationProxy() console.log('desktop package: original system proxy restored') } diff --git a/apps/desktop/tests/macos-notarization-proxy.spec.ts b/apps/desktop/tests/macos-notarization-proxy.spec.ts index a4e64423ee..28303102bd 100644 --- a/apps/desktop/tests/macos-notarization-proxy.spec.ts +++ b/apps/desktop/tests/macos-notarization-proxy.spec.ts @@ -1,10 +1,15 @@ import { existsSync, readFileSync } from 'node:fs' +import { spawn } from 'node:child_process' +import { once } from 'node:events' import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { expect, it, vi } from 'vitest' +import { expect, it as test, vi } from 'vitest' import { restoreMacOSNotarizationProxy, withMacOSNotarizationProxy } from '../scripts/macos-notarization-proxy.ts' +// System proxy transactions use POSIX flock, which is unavailable on Windows. +const it = test.skipIf(process.platform === 'win32') + async function fixture(action: (fixture: ReturnType, lock: string) => Promise): Promise { const root = await mkdtemp(join(tmpdir(), 'mac-proxy-test-')) try { await action(fakeSystem(), join(root, 'lock')) } finally { await rm(root, { recursive: true, force: true }) } @@ -75,6 +80,8 @@ it('does not steal a live lock and holds the proxy until concurrent work drains' try { await entered.promise await expect(withMacOSNotarizationProxy('http://localhost:8889', async () => {}, lock, ops)).rejects.toThrow('another run') + await expect(restoreMacOSNotarizationProxy(lock, ops)).rejects.toThrow('another run') + expect(ops.ownerAlive).not.toHaveBeenCalled() expect(states.http.port).toBe(8888) expect(existsSync(lock)).toBe(true) } finally { finish.resolve(undefined); await running } @@ -82,6 +89,47 @@ it('does not steal a live lock and holds the proxy until concurrent work drains' }) }) +it('excludes both operations across processes and releases ownership after forced exit', async () => { + await fixture(async ({ ops, states, original, control }, lock) => { + control.failSet = 5 + await expect(withMacOSNotarizationProxy('http://localhost:8888', async () => {}, lock, ops)).rejects.toThrow('restoration failed') + const saved = readFileSync(join(lock, 'original.json'), 'utf8') + const child = spawn(process.execPath, ['--input-type=module', '-e', ` + import { openSync } from 'node:fs'; + import { tryLockExclusive } from ${JSON.stringify(import.meta.resolve('@deepseek-ai/node-addon-system/flock'))}; + const fd = openSync(process.argv[1], 'a', 0o600); + await tryLockExclusive(fd); + process.send('locked'); + process.stdin.resume(); + `, `${lock}.flock`], { stdio: ['pipe', 'ignore', 'pipe', 'ipc'], env: { PATH: process.env.PATH } }) + const exited = once(child, 'exit') + const closed = once(child, 'close') + try { + await Promise.race([ + once(child, 'message').then(([message]) => { expect(message).toBe('locked') }), + exited.then(([code, signal]) => { throw new Error(`lock holder exited before readiness: ${code}/${signal}`) }), + ]) + ops.command.mockClear() + ops.ownerAlive.mockClear() + await expect(restoreMacOSNotarizationProxy(lock, ops)).rejects.toThrow('another run') + await expect(withMacOSNotarizationProxy('http://localhost:8889', async () => {}, lock, ops)).rejects.toThrow('another run') + expect(ops.command).not.toHaveBeenCalled() + expect(ops.ownerAlive).not.toHaveBeenCalled() + expect(readFileSync(join(lock, 'original.json'), 'utf8')).toBe(saved) + } finally { + child.kill('SIGKILL') + await closed + } + expect(await exited).toEqual([null, 'SIGKILL']) + await restoreMacOSNotarizationProxy(lock, ops) + expect(states).toEqual(original) + expect(existsSync(lock)).toBe(false) + expect(existsSync(`${lock}.flock`)).toBe(true) + await withMacOSNotarizationProxy('http://localhost:8889', async () => {}, lock, ops) + expect(states).toEqual(original) + }) +}) + it('restores partial setup and never calls Apple after a system write failure', async () => { await fixture(async ({ ops, states, original, control }, lock) => { control.failSet = 3 @@ -119,8 +167,8 @@ it('retains recovery data after failed restoration, rejects live owners, then re await expect(withMacOSNotarizationProxy('http://localhost:8888', async () => {}, lock, ops)).rejects.toThrow('restoration failed') expect(existsSync(join(lock, 'original.json'))).toBe(true) ops.ownerAlive.mockReturnValueOnce(true) - expect(() =>{ restoreMacOSNotarizationProxy(lock, ops) }).toThrow('still running') - restoreMacOSNotarizationProxy(lock, ops) + await expect(restoreMacOSNotarizationProxy(lock, ops)).rejects.toThrow('still running') + await restoreMacOSNotarizationProxy(lock, ops) expect(states).toEqual(original) expect(existsSync(lock)).toBe(false) }) @@ -132,7 +180,7 @@ it('rejects malformed saved state without invoking host commands', async () => { await expect(withMacOSNotarizationProxy('http://localhost:8888', async () => {}, lock, ops)).rejects.toThrow('restoration failed') await writeFile(join(lock, 'original.json'), '{"pid":-1}') ops.command.mockClear() - expect(() =>{ restoreMacOSNotarizationProxy(lock, ops) }).toThrow('invalid proxy recovery record') + await expect(restoreMacOSNotarizationProxy(lock, ops)).rejects.toThrow('invalid proxy recovery record') expect(ops.command).not.toHaveBeenCalled() }) }) @@ -165,6 +213,6 @@ it('retains both the Apple failure and restoration failure', async () => { const result = withMacOSNotarizationProxy('http://localhost:8888', async () => { throw appleError }, lock, ops) await expect(result).rejects.toMatchObject({ errors: [appleError, expect.any(AggregateError)] }) expect(existsSync(join(lock, 'original.json'))).toBe(true) - restoreMacOSNotarizationProxy(lock, ops) + await restoreMacOSNotarizationProxy(lock, ops) }) }) diff --git a/apps/desktop/tests/windows-sign.spec.ts b/apps/desktop/tests/windows-sign.spec.ts index 8af2036a5f..0ee9c19cd6 100644 --- a/apps/desktop/tests/windows-sign.spec.ts +++ b/apps/desktop/tests/windows-sign.spec.ts @@ -161,6 +161,7 @@ describe('Windows token signing', () => { validateDesktopPackageEnvironment({ DSH_DESKTOP_APP_ID: 'com.example.desktop', DOWNLOAD_TEST_ORIGIN: 'https://updates.example.com', DSH_DESKTOP_MANDATORY_UPDATE_TEST_ORIGIN: 'https://policy.example.com', + DSH_DESKTOP_MANDATORY_UPDATE_CONFIG: JSON.stringify({ allowedAuthOrigins: ['https://login.example.com'] }), DSH_DESKTOP_WINDOWS_CER_FILE: certificateFile, DSH_DESKTOP_WINDOWS_SIGNTOOL: signTool, DSH_DESKTOP_WINDOWS_TOKEN_PIN: 'fixture-pin', DSH_DESKTOP_WINDOWS_KEY_CONTAINER: 'fixture-container', }, { platform: 'win32', arch: 'x64' }) diff --git a/apps/web/tests/access-confirmation.e2e.ts b/apps/web/tests/access-confirmation.e2e.ts index 2b2ba0e135..b1c57c60e7 100644 --- a/apps/web/tests/access-confirmation.e2e.ts +++ b/apps/web/tests/access-confirmation.e2e.ts @@ -73,7 +73,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { const currentMenuBox = await currentMenu.boundingBox() expect(currentTriggerBox).not.toBeNull() expect(currentMenuBox).not.toBeNull() - expect(Math.abs(currentMenuBox!.width - 218)).toBeLessThan(1) + expect(Math.abs(currentMenuBox!.width - 144)).toBeLessThan(1) expect(currentMenuBox!.width).toBeGreaterThan(currentTriggerBox!.width) expect(Math.abs(currentTriggerBox!.y - currentMenuBox!.y - currentMenuBox!.height - 4)).toBeLessThan(1) @@ -183,7 +183,7 @@ describe('web e2e: experimental Auto and Full access confirmation', () => { await menu.waitFor() await captureAutoReviewState(page, 'experimental-narrow-menu') const menuBox = (await menu.boundingBox())! - expect(menuBox.width).toBeGreaterThanOrEqual(218) + expect(menuBox.width).toBeGreaterThanOrEqual(144) expect(menuBox.width).toBeLessThanOrEqual(360) expect(menuBox.x).toBeGreaterThanOrEqual(0) expect(menuBox.x + menuBox.width).toBeLessThanOrEqual(420) diff --git a/apps/web/tests/agent-team-panel.e2e.ts b/apps/web/tests/agent-team-panel.e2e.ts index b3192c3655..ac4e484db0 100644 --- a/apps/web/tests/agent-team-panel.e2e.ts +++ b/apps/web/tests/agent-team-panel.e2e.ts @@ -1,4 +1,4 @@ -// Keyless assembled-browser coverage for the opt-in Agent Teams Web profiles +// Keyless assembled-browser coverage for the opt-in Agent Teams bundle // over the real Host Typert Remote flow. import { fileURLToPath } from 'node:url' import { join } from 'node:path' @@ -18,11 +18,9 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/agent-team-panel', import.meta.url)) const PANEL_EXPECTED = join(SNAPSHOT_DIR, 'task.expected.md') const OVERLAY = fileURLToPath(new URL('./agent-team-panel.overlay.yml', import.meta.url)) -const HOST_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/cordis.patch.yml', import.meta.url)) -const WEB_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-web-profile/cordis.patch.yml', import.meta.url)) +const TEAM_PATCH = fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/cordis.patch.yml', import.meta.url)) const INSTALL_ANCHORS = [ fileURLToPath(new URL('../../../packages/experimental/agent-team-profile/package.json', import.meta.url)), - fileURLToPath(new URL('../../../packages/experimental/agent-team-web-profile/package.json', import.meta.url)), ] const MODE = webSnapshotMode() @@ -33,11 +31,8 @@ function profileEntries(path: string): unknown[] { } describe('Agent Teams panel overlay', () => { - it('matches the shipped Host and Web profile layers', () => { - expect(profileEntries(OVERLAY)).toEqual([ - ...profileEntries(HOST_PATCH), - ...profileEntries(WEB_PATCH), - ]) + it('matches the shipped Agent Teams bundle', () => { + expect(profileEntries(OVERLAY)).toEqual(profileEntries(TEAM_PATCH)) }) }) diff --git a/apps/web/tests/agent-team-panel.overlay.yml b/apps/web/tests/agent-team-panel.overlay.yml index cb6dd2decd..20c49c23a1 100644 --- a/apps/web/tests/agent-team-panel.overlay.yml +++ b/apps/web/tests/agent-team-panel.overlay.yml @@ -1,5 +1,4 @@ -# Equivalent to the two explicit Agent Teams profile layers over the scaffold's -# stable base and Web bundles. +# Equivalent to the Agent Teams bundle over the stable base and Web bundles. - id: tool-subagent-control disabled: true @@ -28,6 +27,5 @@ freshProvider: spawn forkProvider: fork -- insert: - id: ui-agent-team name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/apps/web/tests/assembled-remote.ts b/apps/web/tests/assembled-remote.ts index 69739dc7a7..36eab3cb42 100644 --- a/apps/web/tests/assembled-remote.ts +++ b/apps/web/tests/assembled-remote.ts @@ -71,7 +71,14 @@ interface ControlBaseline { interface CapturedFixture { readonly sessionList: { readonly ok: true; readonly value: { readonly items: readonly SessionSummary[] } } - readonly settingsDescribe: unknown + readonly settingsDescribe: { + readonly ok: true + readonly value: { + readonly writable: boolean + readonly hasDocument: boolean + readonly namespaces: readonly unknown[] + } + } readonly credentialsDescribe: unknown readonly modelCatalog: unknown readonly agentPresets: unknown @@ -90,6 +97,8 @@ interface CapturedFixture { } export interface AssembledRemoteOptions { + /** Supply an enabled Host preference for diagnostic View scenarios. */ + readonly developerTools?: boolean /** Return the fixture's image-dimension admission error from Session prompt. */ readonly rejectPrompt?: boolean } @@ -131,7 +140,16 @@ export function createAssembledRemote(options: AssembledRemoteOptions = {}): Ass const mock = RemoteMock.create().load(remoteDefaultResponses) mock.load({ unary: { - 'settings/describe': structuredClone(fixture.settingsDescribe), + 'settings/describe': options.developerTools === true + ? ok({ + ...fixture.settingsDescribe.value, + namespaces: [...fixture.settingsDescribe.value.namespaces, { + ns: 'ui-developer-tools', + schema: { type: 'object', dict: { enabled: { type: 'boolean' } } }, + value: { enabled: true }, applies: 'live', secrets: [], revision: 0, + }], + }) + : structuredClone(fixture.settingsDescribe), 'credentials/describe': structuredClone(fixture.credentialsDescribe), 'session/modelCatalog': structuredClone(fixture.modelCatalog), 'agentPresets/list': structuredClone(fixture.agentPresets), diff --git a/apps/web/tests/built-boot.expected.e2e.ts b/apps/web/tests/built-boot.expected.e2e.ts index 17a741dac4..46115eff44 100644 --- a/apps/web/tests/built-boot.expected.e2e.ts +++ b/apps/web/tests/built-boot.expected.e2e.ts @@ -91,6 +91,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn expect(waitingRow.querySelector('[data-state="warning"]')).not.toBeNull() expect(waitingRow.querySelector('[data-state="ongoing"]')).toBeNull() within(waitingRow).getByText('Waiting for answer') + within(waitingRow).getByText('Answer') // Opening a session reaches chat content through the RemoteMock transport. fireEvent.click(waitingTitle) @@ -103,7 +104,12 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn for (let index = 0; index < 3; index += 1) { fireEvent.click(await screen.findByRole('button', { name: 'Skip' })) } - fireEvent.click(await screen.findByRole('button', { name: 'Allow once' })) + const allowOnce = await screen.findByRole('button', { name: 'Allow once' }) + await waitFor(() => { + within(waitingRow).getByText('Waiting for approval') + within(waitingRow).getByText('Approval') + }) + fireEvent.click(allowOnce) // The fixture mirrors all three token-meter projections, so the assembled // ContextMeter reaches its composition panel instead of only the occupancy diff --git a/apps/web/tests/changed-files-turn.e2e.ts b/apps/web/tests/changed-files-turn.e2e.ts index 08045544fc..295c8d65fb 100644 --- a/apps/web/tests/changed-files-turn.e2e.ts +++ b/apps/web/tests/changed-files-turn.e2e.ts @@ -51,6 +51,7 @@ describe('web e2e: a git workspace turn ends with its changed files', () => { await writeFile(replayOverride, JSON.stringify(script).replaceAll('{{cwd}}', JSON.stringify(cwdToken).slice(1, -1))) } scaffold = await launchWebScaffold({ + developerTools: false, compareReplaySession: true, extraOverlayPath: fileURLToPath(new URL('./changed-files-turn.overlay.yml', import.meta.url)), ...(replayOverride === undefined ? {} : { replayFixture: FIXTURE, replayOverride }), @@ -106,6 +107,18 @@ describe('web e2e: a git workspace turn ends with its changed files', () => { expect(await readFile(join(cwd, 'notes.txt'), 'utf8')).toBe('start\ndone\n') const card = page.locator('[data-changed-files]') + expect(await card.count()).toBe(0) + const header = page.locator('header').filter({ has: page.locator('[data-conversation-header-leading]') }) + expect(await header.getByRole('tablist').count()).toBe(0) + const compactHeight = await header.evaluate(element => element.getBoundingClientRect().height) + expect(compactHeight).toBeLessThan(60) + await page.getByRole('button', { name: '设置', exact: true }).click() + const settings = page.getByRole('dialog', { name: '设置' }) + await settings.getByRole('switch', { name: '开发者工具' }).click() + await expect.poll(() => settings.getByRole('switch', { name: '开发者工具' }).getAttribute('aria-checked')).toBe('true') + await settings.getByRole('button', { name: '关闭', exact: true }).click() + await header.getByRole('tablist').waitFor({ state: 'visible' }) + expect(await header.evaluate(element => element.getBoundingClientRect().height)).toBeGreaterThan(compactHeight) await card.waitFor({ state: 'visible' }) expect(await card.getByText('已编辑 4 个文件', { exact: true }).count()).toBe(1) expect(await card.getByRole('listitem').count()).toBe(3) diff --git a/apps/web/tests/chat-long-interactions.e2e.ts b/apps/web/tests/chat-long-interactions.e2e.ts index 17e639814c..33c8046779 100644 --- a/apps/web/tests/chat-long-interactions.e2e.ts +++ b/apps/web/tests/chat-long-interactions.e2e.ts @@ -198,35 +198,37 @@ describe('web e2e: long Chat interaction contract', () => { const turnNavigation = page.getByRole('navigation', { name: 'Turn navigation' }) await turnNavigation.waitFor({ state: 'visible', timeout: 15_000 }) - // The whole-log outline offers every fixture turn before any paging, with - // the live tail mark current. const marks = turnNavigation.getByRole('button') - await expect.poll(() => marks.count(), { timeout: 15_000 }).toBe(FIXTURE_TURNS) - expect(await marks.last().getAttribute('aria-current')).toBe('true') + await expect.poll(() => marks.last().getAttribute('aria-current'), { timeout: 15_000 }).toBe('true') + expect(await marks.count()).toBeLessThan(FIXTURE_TURNS) // The oldest turn is an unloaded mark whose outline preview already // carries both the prompt and the settled response. const firstTurnButton = turnNavigation .getByRole('button', { name: 'Load and jump to turn 1', exact: true }) + const railScroller = turnNavigation.locator('[class*="scroller"]') + await railScroller.hover() + await page.mouse.wheel(0, -FIXTURE_TURNS * 10) + await expect.poll(() => railScroller.evaluate(element => element.scrollTop)).toBe(0) await firstTurnButton.focus() const preview = page.getByRole('tooltip') await preview.waitFor({ state: 'visible', timeout: 5_000 }) - expect(await preview.textContent()).toContain(FIXTURE.markers.user(1)) + await expect.poll(() => preview.textContent(), { timeout: 5_000 }).toContain(FIXTURE.markers.user(1)) expect(await preview.textContent()).toContain(FIXTURE.markers.assistant(1)) - const firstTurnPosition = await firstTurnButton.evaluate(button => ( - button.parentElement?.style.getPropertyValue('--turn-natural-position') ?? '' + const markPitch = () => turnNavigation.getByRole('button').evaluateAll(buttons => ( + buttons[1]!.getBoundingClientRect().top - buttons[0]!.getBoundingClientRect().top )) - expect(firstTurnPosition).toBe('0px') + expect(await markPitch()).toBe(10) const loadEarlier = page.getByRole('button', { name: 'Load earlier', exact: true }) - const loadedMarks = turnNavigation.getByRole('button', { name: /^Jump to turn / }) - const loadedBefore = await loadedMarks.count() + const loadedRows = page.locator('[data-chat-flow-key]') + const loadedBefore = await loadedRows.count() await loadEarlier.click() - // Paging converts marks to their loaded form without moving the - // fixed-pitch ladder. - await expect.poll(() => loadedMarks.count(), { timeout: 15_000 }).toBeGreaterThan(loadedBefore) - expect(await firstTurnButton.evaluate(button => ( - button.parentElement?.style.getPropertyValue('--turn-natural-position') ?? '' - ))).toBe(firstTurnPosition) + await expect.poll(() => loadedRows.count(), { timeout: 15_000 }).toBeGreaterThan(loadedBefore) + await railScroller.hover() + await page.mouse.wheel(0, -FIXTURE_TURNS * 10) + await expect.poll(() => railScroller.evaluate(element => element.scrollTop)).toBe(0) + await firstTurnButton.waitFor({ state: 'visible' }) + expect(await markPitch()).toBe(10) // Activating the still-unloaded oldest mark pages the rest in and lands // on the turn's own row. await firstTurnButton.focus() diff --git a/apps/web/tests/chat-scroll-contract.e2e.ts b/apps/web/tests/chat-scroll-contract.e2e.ts index 5c4eb17d93..af9ebc875f 100644 --- a/apps/web/tests/chat-scroll-contract.e2e.ts +++ b/apps/web/tests/chat-scroll-contract.e2e.ts @@ -587,7 +587,7 @@ describe('web e2e: long Chat scroll contract', () => { }) }, 180_000) - it.skipIf(MODE === 'record')('offers every outline turn on the rail and jumps to an unloaded one', async () => { + it.skipIf(MODE === 'record')('virtualizes the outline rail and jumps to an unloaded turn', async () => { await withScrollWorld({ failureShot: 'web-e2e-turn-rail-jump', seeds: [{ fixture: HISTORY_FIXTURE, id: RAIL_SESSION_ID }], @@ -595,31 +595,41 @@ describe('web e2e: long Chat scroll contract', () => { await openSeed(world.page, HISTORY_FIXTURE, HISTORY_FIXTURE.markers.assistant(HISTORY_FIXTURE.turns)) await expectBottom(world.page) - // The whole-log outline reaches the rail before any paging: one mark - // per fixture turn, the oldest still in its load-and-jump form. const rail = world.page.getByRole('navigation', { name: 'Turn navigation' }) - await expect.poll(() => rail.getByRole('button').count(), { timeout: 15_000 }) - .toBe(HISTORY_FIXTURE.turns) + const latest = rail.getByRole('button', { name: `Jump to turn ${String(HISTORY_FIXTURE.turns)}`, exact: true }) + await expect.poll(() => latest.getAttribute('aria-current'), { timeout: 15_000 }).toBe('true') + expect(await rail.getByRole('button').count()).toBeLessThan(HISTORY_FIXTURE.turns) const firstUnloaded = rail.getByRole('button', { name: 'Load and jump to turn 1', exact: true }) - expect(await firstUnloaded.count()).toBe(1) - // Fixed pitch: the ladder keeps its natural height, scrolls inside the - // frame, and (following the active tail mark) fades its upper end. - expect(await rail.evaluate(nav => nav.style.getPropertyValue('--turn-natural-height'))) - .toBe(`${String((HISTORY_FIXTURE.turns - 1) * 10 + 12)}px`) + expect(await firstUnloaded.count()).toBe(0) const railScroller = rail.locator('[class*="scroller"]') await expect.poll(() => railScroller.evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true) await expect.poll(() => rail.locator('[class*="fadeTop"]').count(), { timeout: 15_000 }).toBe(1) + const bodyBeforeRailScroll = await scrollGeometry(world.page) + await railScroller.hover() + await world.page.mouse.wheel(0, -HISTORY_FIXTURE.turns * 10) + await expect.poll(() => railScroller.evaluate(element => element.scrollTop)).toBe(0) + await firstUnloaded.waitFor({ state: 'visible' }) + expect((await scrollGeometry(world.page)).scrollTop).toBe(bodyBeforeRailScroll.scrollTop) - // Activate the unloaded mark by keyboard: pointer input belongs to the - // rail frame, while each mark is the keyboard/AT destination. Focus - // first shows the outline-backed preview: prompt and settled response - // both travel ahead of the events. const beforeRows = await loadedFlowRows(world.page) await firstUnloaded.focus() const tooltip = world.page.getByRole('tooltip') await expect.poll(() => tooltip.count(), { timeout: 15_000 }).toBe(1) - expect(await tooltip.textContent()).toContain(HISTORY_FIXTURE.markers.user(1)) + await expect.poll(() => tooltip.textContent(), { timeout: 5_000 }).toContain(HISTORY_FIXTURE.markers.user(1)) expect(await tooltip.textContent()).toContain(HISTORY_FIXTURE.markers.assistant(1)) + const previewId = await tooltip.getAttribute('id') + const hoveredMark = () => rail.evaluate(element => element.querySelector('button:hover')?.dataset.index ?? null) + await expect.poll(hoveredMark).not.toBeNull() + const hoveredBefore = await hoveredMark() + await world.page.mouse.wheel(0, 80) + await expect.poll(() => railScroller.evaluate(element => element.scrollTop)).toBe(80) + await expect.poll(hoveredMark).toBe(String(Number(hoveredBefore) + 8)) + await nextPaint(world.page) + expect(await firstUnloaded.evaluate(element => document.activeElement === element)).toBe(true) + expect(await firstUnloaded.getAttribute('aria-describedby')).toBe(previewId) + expect(await tooltip.textContent()).toContain(HISTORY_FIXTURE.markers.user(1)) + await world.page.mouse.wheel(0, -80) + await expect.poll(() => railScroller.evaluate(element => element.scrollTop)).toBe(0) const transcriptLayers = await tooltip.evaluate((preview) => { const railSlot = preview.closest('nav')?.parentElement const codeBanner = document.querySelector('.md-code-block > :first-child') diff --git a/apps/web/tests/clickable-links-gallery.e2e.ts b/apps/web/tests/clickable-links-gallery.e2e.ts index 5f210b0bab..21e4f78bae 100644 --- a/apps/web/tests/clickable-links-gallery.e2e.ts +++ b/apps/web/tests/clickable-links-gallery.e2e.ts @@ -436,8 +436,8 @@ describe('web e2e: clickable links gallery', () => { // globe in the same seat. const repoLink = markdown.locator(`a[href="${REPO_URL}"]`) expect(await repoLink.count()).toBe(1) - const repoMark = await repoLink.locator('svg path').getAttribute('d') - const globeMark = await guideLink.locator('svg path').getAttribute('d') + const repoMark = await repoLink.locator('svg path').first().getAttribute('d') + const globeMark = await guideLink.locator('svg path').first().getAttribute('d') expect(repoMark).not.toBe(globeMark) await guideLink.hover() expect(await styleOf(guideLink, 'text-decoration-line')).toBe('underline') diff --git a/apps/web/tests/developer-tools-settings.e2e.ts b/apps/web/tests/developer-tools-settings.e2e.ts new file mode 100644 index 0000000000..16c0e6500a --- /dev/null +++ b/apps/web/tests/developer-tools-settings.e2e.ts @@ -0,0 +1,29 @@ +/** Shared settings are off on a fresh Host and survive browser reload through the ordinary settings UI. */ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { chromium } from 'playwright' +import { expect, it, onTestFinished } from 'vitest' +import { launchWebScaffold } from './scaffold.ts' +import { newEnglishPage } from './support.ts' + +it('persists developer tools in the Host settings document and restores the accepted choice', async () => { + const scaffold = await launchWebScaffold({ developerTools: false }) + onTestFinished(() => scaffold.close()) + const browser = await chromium.launch() + onTestFinished(() => browser.close()) + const page = await newEnglishPage(browser) + await page.goto(scaffold.authenticatedUrl) + await page.getByRole('button', { name: 'Settings', exact: true }).click() + const toggle = page.getByRole('switch', { name: 'Developer tools' }) + expect(await toggle.getAttribute('aria-checked')).toBe('false') + expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).not.toContain('ui-developer-tools:') + await toggle.click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).toContain('ui-developer-tools:\n enabled: true') + await page.reload() + await page.getByRole('button', { name: 'Settings', exact: true }).click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true') + await toggle.click() + await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false') + expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).toContain('ui-developer-tools:\n enabled: false') +}) diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 24137b06ad..7e2d43331c 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -86,7 +86,9 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () beforeAll(async () => { outsideRoot = await mkdtemp(join(tmpdir(), 'dsh-preview-outside-')) - scaffold = await launchWebScaffold({ replayFixture: FIXTURE, paceMs: 5, compareReplaySession: false, extraOverlayPath: PAGING_PATCH }) + scaffold = await launchWebScaffold({ + developerTools: false, replayFixture: FIXTURE, paceMs: 5, compareReplaySession: false, extraOverlayPath: PAGING_PATCH, + }) browser = await chromium.launch() page = await newEnglishPage(browser) tripwire = watchConsole(page) @@ -111,17 +113,30 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () await mkdir(SHOT_DIR, { recursive: true }) await saveFailureShot(page, `screenshots/0908-document-preview/smoke-${process.pid}`) }) + expect(await page.locator('[data-slot="conversation.hero.agentPreset"] button').count()).toBe(0) + expect(scaffold.ctx.settings.get('ui-developer-tools')).toEqual({ enabled: false }) const settled = scaffold.whenTurnSettled() const input = page.locator('[data-composer-input]').first() await input.fill(PROMPT) await input.press('Enter') const sessionId = await settled await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) + expect(await page.getByRole('tablist').count()).toBe(0) const cwd = scaffold.ctx.agents.get(sessionId)?.session.header.cwd if (cwd === undefined) throw new Error('settled Session has no workspace cwd') if (outsideRoot === undefined) throw new Error('outside fixture directory is unavailable') const outsideScript = join(outsideRoot, 'outside.js') const outsideReference = relative(cwd, outsideScript).replace(/\\/g, '/') + let previewNetworkRequests = 0 + await page.route('https://preview.invalid/developer-tools.png', async (route) => { + previewNetworkRequests += 1 + await route.fulfill({ status: 200, contentType: 'image/png', body: TINY_PNG }) + }) + const blockedRequests: string[] = [] + await page.route('https://blocked-preview.invalid/**', async (route) => { + blockedRequests.push(route.request().url()) + await route.fulfill({ status: 200, contentType: 'text/html', body: 'ESCAPED' }) + }) const markdownText = [ '# Markdown smoke', '', 'Rendered from the workspace.', '', ...Array.from({ length: (PAGE_LINES - 4) / 2 }, (_, index) => [`Paragraph ${index + 1}: ${'visible prefix '.repeat(20)}`, '']).flat(), @@ -132,12 +147,24 @@ describe.skipIf(MODE === 'record')('web e2e: document preview through Files', () 'const tail = "CODE_TAIL";', ] await Promise.all([ + writeFile(join(cwd, 'hostile.html'), ` + + +

Static adversarial preview

+ Plain link +
+ SVG link + + x +
+

Static

+ + LinkSVG + `)) + const parsed = new DOMParser().parseFromString(html, 'text/html') + expect(parsed.head.firstElementChild?.getAttribute('http-equiv')).toBe('Content-Security-Policy') + expect(parsed.querySelector('meta')?.getAttribute('content')).toContain("connect-src 'none'") + expect(parsed.querySelectorAll('script, iframe, object, embed, base, a[href], a[xlink\\:href]')).toHaveLength(0) + expect(parsed.querySelectorAll('meta[http-equiv]')).toHaveLength(1) + expect(parsed.querySelector('p')?.textContent).toBe('Static') + expect(parsed.querySelector('style')?.textContent).toContain('color: red') + expect(parsed.querySelector('img')?.getAttribute('src')).toBe('data:image/png;base64,AA==') +}) + +it('preserves document styling and removes navigation inside nested templates and SVG animation', () => { + const html = createBasicHtmlDocument(new TextEncoder().encode(` + +
+ + `)) + const parsed = new DOMParser().parseFromString(html, 'text/html') + expect(parsed.documentElement.lang).toBe('zh') + expect(parsed.documentElement.className).toBe('dark') + expect(parsed.body.style.margin).toBe('0px') + expect(parsed.body.dir).toBe('rtl') + expect(parsed.querySelectorAll('noscript, link, set, animate')).toHaveLength(0) + const nested = parsed.querySelector('template')!.content.querySelector('template')!.content + expect(nested.querySelector('a')!.hasAttribute('href')).toBe(false) + expect(nested.querySelector('iframe')).toBeNull() +}) diff --git a/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx index 5c8f1c258c..d225f3cbd5 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx @@ -1,5 +1,6 @@ // @vitest-environment jsdom /** Document extension registration and dispatch through the production Sidebar and Slot renderer. */ +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, fireEvent, screen, waitFor } from '@testing-library/react' import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime' @@ -36,6 +37,7 @@ afterEach(async () => { async function boot() { const rt = await SlotTestRuntime.create() runtime = rt + rt.ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) } } as never) rt.ctx.provide('layout', { openRightbar: vi.fn(), closeRightbar: vi.fn() } as never) const locale = new LocaleRuntime(rt.ctx) rt.ctx.provide('locale', locale) diff --git a/packages/client/ui-sidebar-documentpreview/tests/fixtures.client.ts b/packages/client/ui-sidebar-documentpreview/tests/fixtures.client.ts index d0115bdf38..886b6856e8 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/fixtures.client.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/fixtures.client.ts @@ -30,7 +30,11 @@ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit' type BodySlot = PropsRenderSlots<'sidebar.right.tab.document'>['renderSlot'] /** Preserve the body-slot callback used by component fixtures. */ -export function documentSlots(body: BodySlot): TextPreviewProps['renderSlot'] { return body } +export function documentSlots(body: BodySlot): TextPreviewProps['renderSlot'] { + return (name, owner, options) => name === 'sidebar.right.tab.document' + ? body(name, owner as unknown as Parameters[1], options as Parameters[2]) + : null +} export const TAB_ID = 'tab-1' as TabId export const SESSION = 's-1' as SessionId diff --git a/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts index ccb8ec82c8..bcf14b5c6a 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/html-apply.client.spec.ts @@ -1,4 +1,5 @@ /** HTML metadata and keyed slot contributions share one identity and unwind with their fiber. */ +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path' @@ -30,6 +31,7 @@ describe('HTML registration', () => { it('registers its dictionary and matching keyed body, and removes all contributions on disposal', async () => { const ctx = new Context() + ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) } } as never) // No Session or Tab services are mounted; the global callback must use its file address. const registry = new DocumentPreviewRegistry() const dictionaries = new Map() @@ -46,7 +48,7 @@ describe('HTML registration', () => { bind: () => (key: keyof typeof en) => en[key], register: (name: string, value: unknown) => { dictionaries.set(name, value); return () => { dictionaries.delete(name) } }, } as never) - const fiber = ctx.plugin({ apply }) + const fiber = ctx.plugin({ inject: ['settingsScope'], apply }) dispose = async () => { await fiber.dispose() } await fiber.await() expect(registry.candidates('INDEX.HTM').map(entry => entry.id)).toEqual([HTML_BODY_ID]) diff --git a/packages/client/ui-sidebar-documentpreview/tests/html-body.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/html-body.client.spec.tsx index 78eca6a5f2..d5c36bbb4e 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/html-body.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/html-body.client.spec.tsx @@ -34,6 +34,7 @@ afterEach(() => { function props(text = '

hello

'): HtmlBodyProps { const signal = new AbortController().signal return { + useInteractivePreview: select => select(true), resourceAddress: 'dsh-resource://file/session/html/index.html', content: { kind: 'bytes', data: utf8(text) }, wrap: false, @@ -48,6 +49,28 @@ function props(text = '

hello

'): HtmlBodyProps { const utf8 = (text: string): Uint8Array => new TextEncoder().encode(text) describe('HtmlBody', () => { + it('renders static HTML without reading related files, running scripts or retaining an advanced frame', async () => { + const initial = props('

Preview

Static content

') + const basic = { ...initial, useInteractivePreview: ((select: (enabled: boolean) => unknown) => select(false)) as HtmlBodyProps['useInteractivePreview'] } + const view = render() + const frame = screen.getByTitle(en.frame) + expect(frame.getAttribute('sandbox')).toBe('') + expect(frame.getAttribute('srcdoc')).toContain("default-src 'none'") + expect(frame.getAttribute('srcdoc')).not.toContain('Advanced

') + view.rerender() + const advanced = await screen.findByTitle(en.frame) + expect(advanced).not.toBe(frame) + expect(advanced.getAttribute('sandbox')).toBe('allow-scripts') + view.rerender() + expect(advanced.isConnected).toBe(false) + expect(revoke).toHaveBeenCalledOnce() + view.rerender() + expect(screen.getByRole('alert').textContent).toBe(en.failed) + }) + it('renders a Blob iframe with only scripts allowed, keeping it mounted for unrelated props', async () => { const initial = props() const view = render() diff --git a/packages/client/ui-sidebar-documentpreview/tests/loading-indicator.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/loading-indicator.client.spec.tsx index 7ffc7aa826..aaf45a8a59 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/loading-indicator.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/loading-indicator.client.spec.tsx @@ -12,5 +12,6 @@ describe('LoadingIndicator', () => { const status = getByRole('status') expect(status.textContent).toBe('') expect(status.getAttribute('aria-label')).toBe('Reading…') + expect(status.querySelector('[data-state="ongoing"]')).not.toBeNull() }) }) diff --git a/packages/client/ui-sidebar-documentpreview/tests/office-font-notice.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/office-font-notice.client.spec.tsx index a66c536bec..169aaf0aa6 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/office-font-notice.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/office-font-notice.client.spec.tsx @@ -15,7 +15,6 @@ afterEach(() => { cleanup(); vi.unstubAllGlobals() }) function props(overrides: Partial = {}): FontNoticeProps { return { - resourceAddress: 'dsh-resource://file/session/s-1/report.docx', sourceVersion: 'v1', fonts: ['Consolas', 'Missing Serif'], t: makeTranslate(en), ...overrides, } @@ -23,7 +22,7 @@ function props(overrides: Partial = {}): FontNoticeProps { it('opens details, restores focus on Escape or close, and dismisses outside without stealing focus', () => { render() - const more = screen.getByRole('button', { name: en.showMore }) + const more = screen.getByRole('button', { name: makeTranslate(en)('viewMissingFonts', { count: 2 }) }) fireEvent.click(more) const dialog = screen.getByRole('dialog', { name: en.missingFontsTitle }) expect(dialog.textContent).toContain('Consolas') @@ -36,7 +35,8 @@ it('opens details, restores focus on Escape or close, and dismisses outside with expect(document.activeElement).toBe(more) fireEvent.click(more) fireEvent.click(screen.getByRole('button', { name: en.closeDetails })) - expect(screen.getByRole('status').textContent).toContain('Consolas') + expect(screen.getByRole('button', { name: makeTranslate(en)('viewMissingFonts', { count: 2 }) })).toBe(more) + expect(screen.queryByRole('status')).toBeNull() expect(document.activeElement).toBe(more) fireEvent.click(more) fireEvent.pointerDown(document.body) @@ -48,7 +48,7 @@ it('opens details, restores focus on Escape or close, and dismisses outside with it('keeps details open while focus moves inside them and closes when focus leaves the notice', () => { render(<>) - const more = screen.getByRole('button', { name: en.showMore }) + const more = screen.getByRole('button', { name: makeTranslate(en)('viewMissingFonts', { count: 2 }) }) fireEvent.click(more) const dialog = screen.getByRole('dialog') fireEvent.blur(dialog, { relatedTarget: screen.getByRole('button', { name: en.closeDetails }) }) @@ -59,24 +59,23 @@ it('keeps details open while focus moves inside them and closes when focus leave expect(screen.queryByRole('dialog')).toBeNull() }) -it('hides the entire notice until another source version is loaded', () => { - const input = props() - const view = render() - fireEvent.click(screen.getByRole('button', { name: en.showMore })) - fireEvent.click(screen.getByRole('button', { name: en.dismissNotice })) - expect(screen.queryByRole('status')).toBeNull() +it('shows a warning without automatically opening details or remembering that they were viewed', () => { + const view = render() + const warning = screen.getByRole('button') + expect(warning.getAttribute('aria-expanded')).toBe('false') expect(screen.queryByRole('dialog')).toBeNull() - expect(screen.queryByRole('button', { name: en.showMore })).toBeNull() - view.rerender() - expect(screen.queryByRole('status')).toBeNull() - view.rerender() - expect(screen.getByRole('status')).toBeDefined() + fireEvent.click(warning) + fireEvent.click(screen.getByRole('button', { name: en.closeDetails })) + expect(screen.getByRole('button', { name: makeTranslate(en)('viewMissingFonts', { count: 2 }) })).toBe(warning) + view.unmount() + render() + expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false') expect(screen.queryByRole('dialog')).toBeNull() }) it('has localized copy and does not reserve a notice for fonts that are available', () => { const view = render() - fireEvent.click(screen.getByRole('button', { name: zh.showMore })) + fireEvent.click(screen.getByRole('button', { name: makeTranslate(zh)('viewMissingFonts', { count: 2 }) })) expect(screen.getByRole('dialog', { name: zh.missingFontsTitle })).toBeDefined() view.rerender() expect(view.container.childElementCount).toBe(0) diff --git a/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts b/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts index f572cf219b..22bd4da75b 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts +++ b/packages/client/ui-sidebar-documentpreview/tests/office-registration.client.spec.ts @@ -99,7 +99,7 @@ it.each(['remote', 'render', 'files'] as const)('keeps Office registration and g expect(h.register).toHaveBeenCalledWith(expect.objectContaining({ name: 'sidebar.right.tab.document', key: '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office', locale: 'sidebarOffice', }), OfficeBody) - expect(h.removeNotice).toHaveBeenCalledTimes(2) + expect(h.removeNotice).toHaveBeenCalledTimes(3) }) it('requests a Host PDF with source identity and borrows the same binary cache result', async () => { diff --git a/packages/client/ui-sidebar-documentpreview/tests/pdf-body.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/pdf-body.client.spec.tsx index ee2e2a5c0d..a39f99a865 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/pdf-body.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/pdf-body.client.spec.tsx @@ -107,6 +107,7 @@ describe('PDF body', () => { const view = render() expect(screen.getByRole('status').getAttribute('aria-label')).toBe('Reading…') expect(screen.getByRole('status').hasAttribute('data-document-loading')).toBe(true) + expect(screen.getByRole('status').querySelector('[data-state="ongoing"]')).not.toBeNull() await act(async () => { loads[0]!.deferred.resolve(documentOf()) }) await act(async () => {}) expect(view.container.querySelector('[role="toolbar"]')).toBeNull() diff --git a/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx index 1a8836358e..892a0289e8 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx @@ -9,6 +9,7 @@ import type { OfficeToPdfGeneration } from '@deepseek-ai/dsh-office-to-pdf/types import type { DocumentPreviewDefinition } from '../src/client/document/registry.ts' import type { DocumentContent } from '../src/client/document/contract.ts' import { TextPreview, type TextPreviewProps } from '../src/client/TextPreview.tsx' +import { OfficeFontAction, type OfficeFontActionProps } from '../src/client/office/OfficeFontAction.tsx' import { OfficeBody, type OfficeBodyProps } from '../src/client/office/OfficeBody.tsx' import { createOfficeStore, type OfficeState } from '../src/client/office/store.ts' import type { ReadOfficeDocument } from '../src/client/office/cache.ts' @@ -45,7 +46,8 @@ it('lets a non-Office renderer load content, report its version, and reload thro useEffect(() => { if (displayed !== undefined) request?.loaded(displayed.version) }, [displayed, request?.loaded]) return

{displayed?.text ?? 'Loading custom content'}

} - const renderSlot: TextPreviewProps['renderSlot'] = (_name, input) => { + const renderSlot: TextPreviewProps['renderSlot'] = (name, input) => { + if (name !== 'sidebar.right.tab.document') return null const owner = input as unknown as OwnerOf<'sidebar.right.tab.document'> return } @@ -72,9 +74,9 @@ const definition: DocumentPreviewDefinition = { id: 'office', extensions: ['md'], binaryExtensions: ['md'], title: () => 'Office', loading: 'renderer', } type Result = Awaited> -const result = (version = 'v1'): Result => ({ ok: true, value: { +const result = (version = 'v1', missingFonts: readonly string[] = []): Result => ({ ok: true, value: { absolutePath: ABSOLUTE_PATH, version, data: new TextEncoder().encode(`PDF ${version}`), - offset: 0, eof: true, missingFonts: [], generation: 'engine' as OfficeToPdfGeneration, + offset: 0, eof: true, missingFonts, generation: 'engine' as OfficeToPdfGeneration, } }) function setup() { @@ -99,7 +101,11 @@ function setup() { } const describeFailure: OfficeBodyProps['describeFailure'] = error => error.message let request: Extract | undefined - const slots: TextPreviewProps['renderSlot'] = (_key, input, options) => { + const slots: TextPreviewProps['renderSlot'] = (key, input, options) => { + if (key === 'sidebar.right.tab.document.action') { + return + } const owner = input as unknown as OwnerOf<'sidebar.right.tab.document'> if (owner.content.kind !== 'renderer') return

Raw bytes

request = owner.content @@ -211,3 +217,26 @@ it('starts no conversion when a body receives ordinary shared content', () => { expect(view.container.childElementCount).toBe(0) expect(h.read).not.toHaveBeenCalled() }) + +it('shows the current revision’s missing fonts in the toolbar and clears stale details during reload', async () => { + const h = setup() + const view = render() + expect(screen.queryByRole('button', { name: /Missing fonts:/ })).toBeNull() + await act(async () => { h.pending[0]!.deferred.resolve(result('v1', ['Missing Serif'])) }) + const warning = screen.getByRole('button', { name: 'Missing fonts: 1. Click to view.' }) + fireEvent.click(warning) + expect(screen.getByRole('dialog').textContent).toContain('Missing Serif') + fireEvent.click(screen.getByRole('button', { name: 'reload' })) + expect(screen.queryByRole('button', { name: /Missing fonts:/ })).toBeNull() + expect(screen.queryByRole('dialog')).toBeNull() + await act(async () => { h.pending[1]!.deferred.resolve(result('v2', ['Missing Sans'])) }) + expect(screen.queryByRole('dialog')).toBeNull() + fireEvent.click(screen.getByRole('button', { name: 'Missing fonts: 1. Click to view.' })) + expect(screen.getByRole('dialog').textContent).toContain('Missing Sans') + expect(screen.getByRole('dialog').textContent).not.toContain('Missing Serif') + h.h.bytes.mockResolvedValue({ ok: true, value: { absolutePath: ABSOLUTE_PATH, version: 'v1', offset: 0, eof: true, data: new Uint8Array() } }) + view.rerender() + await settle() + expect(screen.queryByRole('button', { name: /Missing fonts:/ })).toBeNull() + expect(screen.queryByRole('dialog')).toBeNull() +}) diff --git a/packages/client/ui-sidebar-documentpreview/tsconfig.client.json b/packages/client/ui-sidebar-documentpreview/tsconfig.client.json index f1a8702766..c02235dfc2 100644 --- a/packages/client/ui-sidebar-documentpreview/tsconfig.client.json +++ b/packages/client/ui-sidebar-documentpreview/tsconfig.client.json @@ -64,6 +64,9 @@ }, { "path": "../../document/office-to-pdf" + }, + { + "path": "../ui-settings" } ] } diff --git a/packages/client/ui-sidebar-files/src/client/FilesBody.tsx b/packages/client/ui-sidebar-files/src/client/FilesBody.tsx index 7a8c70f501..9d3fb42f48 100644 --- a/packages/client/ui-sidebar-files/src/client/FilesBody.tsx +++ b/packages/client/ui-sidebar-files/src/client/FilesBody.tsx @@ -16,7 +16,7 @@ import clsx from 'clsx' import type { RemoteFailure } from '@deepseek-ai/dsh-api-remotes/client' import type { PropsLocale, PropsRuntime, PropsStore, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' import { - FileTypeIcon, IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, classifyFileType, + FileTypeIcon, IconFolderCloseRegular, IconFolderOpenRegular, IconRefreshOutlineRegular, classifyFileType, } from '@deepseek-ai/dsh-client-ui-primitives' import { fileAddressFor, pathPartsOf } from '@deepseek-ai/dsh-util-workspace-path' import type { WorkspaceDirectoryEntry } from '@deepseek-ai/dsh-api-workspace-files/types' @@ -116,7 +116,7 @@ function Entry({ parent, entry, tree }: { parent: string; entry: WorkspaceDirect return (
  • {expanded &&
    } @@ -243,7 +243,7 @@ export function FilesBody({ data-files-reload onClick={reload} > - + {/* jscpd:ignore-end */} diff --git a/packages/client/ui-sidebar-right/src/client/shell/ExpandButton.tsx b/packages/client/ui-sidebar-right/src/client/shell/ExpandButton.tsx index 7d9b2d276e..a89486d27c 100644 --- a/packages/client/ui-sidebar-right/src/client/shell/ExpandButton.tsx +++ b/packages/client/ui-sidebar-right/src/client/shell/ExpandButton.tsx @@ -14,7 +14,7 @@ * on the other edge. */ import type { ReactNode } from 'react' -import { IconPanelLeftOutline16, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconPanelLeftOutlineRegular, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import type { createSidebarRightStore } from '../stores.ts' @@ -41,7 +41,7 @@ export function ExpandButton({ sessionId, useStore, actions, t }: ExpandButtonPr data-sidebar-right-expand onClick={() => { actions.setExpanded(sessionId, true) }} > - + ) diff --git a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx index 476134c47d..1a517f4684 100644 --- a/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx +++ b/packages/client/ui-sidebar-right/src/client/shell/SidebarRight.tsx @@ -30,7 +30,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react' import type { ReactNode, RefObject } from 'react' import { createPortal } from 'react-dom' -import { IconPanelLeftOutline16, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconPanelLeftOutlineRegular, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, } from '@deepseek-ai/dsh-client-ui-slots' @@ -225,28 +225,21 @@ function titlesFor(panel: PanelProps): TabRenderer { return tab => } -/** Expand-to-viewport glyph: four frame corners (figma extract). */ +/** Expand-to-viewport glyph from the shared product artwork. */ function FullscreenGlyph(): ReactNode { return ( ) } -/** Restore-from-fullscreen glyph: two corners drawn inward (figma extract). */ +/** Restore-from-fullscreen glyph from the shared product artwork. */ function ExitFullscreenGlyph(): ReactNode { return ( ) } @@ -279,7 +272,7 @@ function PanelChrome({ sessionId, fullscreen, autoFullscreen, actions, t }: Pick data-sidebar-right-toggle onClick={() => { actions.toggleExpanded(sessionId) }} > - + diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css index eb4676a3e2..07f68cee8f 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.module.css @@ -56,7 +56,7 @@ font: inherit; text-align: left; background: var(--dsw-alias-bg-layer-1); - border: 0.5px solid var(--dsw-alias-border-l4); + border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 24px; cursor: pointer; } @@ -95,7 +95,7 @@ .entryTitle { overflow: hidden; - font-size: 15px; + font-size: 14px; line-height: 1.4; white-space: nowrap; text-overflow: ellipsis; diff --git a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx index 3c763ce95a..573c476f2d 100644 --- a/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx +++ b/packages/client/ui-sidebar-right/src/client/tabs/guide/GuideTitle.tsx @@ -18,8 +18,8 @@ import css from './GuideBody.module.css' export function CompassGlyph({ size = 16, className }: IconProps): ReactNode { return ( ) } @@ -35,19 +35,8 @@ export function CompassGlyph({ size = 16, className }: IconProps): ReactNode { export function CubeGlyph({ size = 16, className }: IconProps): ReactNode { return ( ) } diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.module.css b/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.module.css index 3380810d55..1926b98b1c 100644 --- a/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.module.css +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.module.css @@ -1,8 +1,9 @@ -.entry { box-sizing: border-box; display: flex; align-items: stretch; width: 100%; overflow: hidden; border: 0.5px solid var(--dsw-alias-border-l4); border-radius: 24px; background: var(--dsw-alias-bg-layer-1); } +.entry { box-sizing: border-box; display: flex; align-items: stretch; width: 100%; overflow: hidden; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 24px; background: var(--dsw-alias-bg-layer-1); } .main { flex: 1; justify-content: flex-start; gap: 14px; min-width: 0; height: auto; min-height: 56px; padding: 14px 20px; border-radius: 24px 0 0 24px; text-align: left; } .icon { flex: none; } .text { display: flex; flex-direction: column; gap: 3px; min-width: 0; } -.title { overflow: hidden; color: var(--dsw-alias-label-primary); font-size: 15px; line-height: 1.4; white-space: nowrap; text-overflow: ellipsis; } +.title { overflow: hidden; color: var(--dsw-alias-label-primary); font-size: 14px; line-height: 1.4; white-space: nowrap; text-overflow: ellipsis; } .description { overflow: hidden; color: var(--dsw-alias-label-caption); font-size: 13px; line-height: 1.4; white-space: nowrap; text-overflow: ellipsis; } .trigger { align-self: stretch; flex: none; height: auto; width: 44px; padding: 0; border-radius: 0 24px 24px 0; } +.trigger svg { width: 18px; height: 18px; color: var(--dsw-alias-label-tertiary); } .menu { display: flex; align-self: stretch; flex: none; } diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.tsx index 68fb351686..4011f17de7 100644 --- a/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.tsx +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.tsx @@ -1,6 +1,6 @@ /** Shell launch menu owned by the terminal provider's guide entry. */ import { useEffect, useState, type ReactNode } from 'react' -import { Button, IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives' +import { Button, IconChevronDownOutlineRegular, Menu } from '@deepseek-ai/dsh-client-ui-primitives' import type { TerminalLaunchShells } from '@deepseek-ai/dsh-api-terminal-controller/client' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' @@ -67,7 +67,7 @@ export function TerminalGuide({ title, description, kind, useTabInfo, loadShells }} anchor={} /> diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx index df5525a08d..4c0fa3d11c 100644 --- a/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx @@ -8,7 +8,8 @@ import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives' */ export function TerminalIcon(): ReactNode { return } @@ -19,7 +20,7 @@ export function TerminalIcon(): ReactNode { */ export function TerminalGuideIcon({ size = 26, className }: IconProps): ReactNode { return } diff --git a/packages/client/ui-sidebar-terminal/src/client/terminal.tsx b/packages/client/ui-sidebar-terminal/src/client/terminal.tsx index 9a64861bb3..1e3cd760d3 100644 --- a/packages/client/ui-sidebar-terminal/src/client/terminal.tsx +++ b/packages/client/ui-sidebar-terminal/src/client/terminal.tsx @@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useRef, type ReactNode } from 'react' import { Terminal } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' -import { Button, IconPlusOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { Button, IconPlusOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { TerminalViewState, TerminalView } from '@deepseek-ai/dsh-api-terminal-controller/client' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client' @@ -29,7 +29,7 @@ export function TerminalBody({ useTabInfo, useTerminal, useTheme, view, t }: Ter useEffect(() => model.mount(), [model]) if (state === undefined) return null const newTerminal = diff --git a/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx b/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx index 77a98caa63..677ed02fcf 100644 --- a/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx +++ b/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx @@ -1,6 +1,6 @@ /** macOS-desktop conversation-header controls for the fully hidden sidebar. */ import { - IconNewChatOutline16, IconPanelLeftOutline16, isDarwinDesktop, Tooltip, + IconNewChatOutlineMedium, IconPanelLeftOutlineRegular, isDarwinDesktop, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' // Type-only: pulls the conversation header slot declarations. @@ -35,7 +35,7 @@ export function HeaderLeadingControls({ toggleSidebar, startSession, t }: Header aria-label={t('toggle.open')} onClick={() => { toggleSidebar() }} > - + @@ -45,7 +45,7 @@ export function HeaderLeadingControls({ toggleSidebar, startSession, t }: Header aria-label={t('session.new.label')} onClick={() => { startSession() }} > - + diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx index 6ed94f3b0d..ea362a51f2 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx @@ -19,7 +19,7 @@ import { useEffect, useRef, useState } from 'react' import clsx from 'clsx' import { - FishLogo, IconNewChatOutline16, IconPanelLeftOutline16, isDarwinDesktop, Tooltip, + FishLogo, IconNewChatOutlineMedium, IconPanelLeftOutlineRegular, isDarwinDesktop, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type { @@ -182,7 +182,7 @@ export function SidebarRoot({ )} {/* Rail icons render at 18 (figma rail spec); expanded keeps the glyph-native sizes. */} - + {!wide && renderSlot('sidebar.toggle.badge', {})} @@ -245,7 +245,7 @@ export function SidebarRoot({ aria-label={t('session.new.label')} onClick={() => { startSession() }} > - + {wide && {t('session.new')}} diff --git a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap index d0833b1a6b..ce6cdec05b 100644 --- a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap +++ b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap @@ -18,10 +18,12 @@ exports[`sidebar shell snapshots > renders Windows caption controls in expanded type="button" > - + ) } diff --git a/packages/client/ui-skill/README.i18n.yaml b/packages/client/ui-skill/README.i18n.yaml index b5491aded0..83f8c75b1c 100644 --- a/packages/client/ui-skill/README.i18n.yaml +++ b/packages/client/ui-skill/README.i18n.yaml @@ -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 packages/client/ui-skill/README.md -README.md: a1e9e7047f102f2610160905727450833b540082 -README.zh.md: d7669b11fc7249c857e20f3c52f93b8bfd214281 +README.md: b2687d59fc6566d9ea8a0c0d4d5b16eea38282b5 +README.zh.md: 7e6c8a7ecee2fe10d5826b91c108733bb4d3746f diff --git a/packages/client/ui-skill/README.md b/packages/client/ui-skill/README.md index a1e9e7047f..b2687d59fc 100644 --- a/packages/client/ui-skill/README.md +++ b/packages/client/ui-skill/README.md @@ -33,7 +33,7 @@ Ordinary-session candidates come from the `skills/list` Remote; the host serves ### The skill tool row -A collapsed row renders the skill glyph, `Skill` title, and requested skill name; running calls carry the transcript shimmer, failures replace the name with the first error line, and interrupted calls use the warning state. A settled row expands into a bounded `Instructions` card containing the exact durable tool output, with the standard trajectory `Inspect` affordance when available. The row derives its name, lifecycle, and body only from the frozen call/result slice supplied by ui-tool, never from the current catalog, so replay stays stable when installed skills or their descriptions change. +A collapsed row renders the skill glyph, `Skill` title, and requested skill name on the same secondary font-size axis, line height, row height, and glyph scaling as ordinary Tool rows. Every lifecycle state retains the ordinary skill glyph; running calls carry the transcript shimmer, while failures replace the name with the first error line and interruptions retain their explicit status text. A settled row expands into a bounded `Instructions` card containing the exact durable tool output, with the standard trajectory `Inspect` affordance when available; this expanded card keeps its own typography. The row derives its name, lifecycle, and body only from the frozen call/result slice supplied by ui-tool, never from the current catalog, so replay stays stable when installed skills or their descriptions change. Hovering over `/name` highlights the entire reference. Clicking a known skill opens its provider-supplied `SKILL.md` path in the right Sidebar while keeping the token editable. An uncached click shares the per-Session catalog fetch and opens when it completes, retaining the clicked Session address. Preset changes, connection resets, and plugin disposal cancel pending previews; a later click fetches the current catalog again. Skills without a file path remain invocable but have no file preview. diff --git a/packages/client/ui-skill/README.zh.md b/packages/client/ui-skill/README.zh.md index d7669b11fc..7e6c8a7ece 100644 --- a/packages/client/ui-skill/README.zh.md +++ b/packages/client/ui-skill/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" ### skill 工具行 -收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称;运行中的调用带有 transcript(文本记录)的扫光效果,失败时用错误首行替换名称,中断的调用使用警告状态。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。 +收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。所有生命周期状态都保留普通 skill 图标;运行中的调用带有 transcript(文本记录)的扫光效果,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。 鼠标移入 `/name` 时,背景覆盖整个引用。点击已知 skill 会在右侧栏打开提供方给出的 `SKILL.md` 路径,同时保留文本的可编辑性。缓存未就绪时,点击复用该 Session 的目录请求,在完成后按点击时的 Session 地址打开预览。切换预设、重置连接和插件释放会取消待处理的预览;后续点击重新获取当前目录。没有文件路径的 skill 仍可调用,但没有文件预览。 diff --git a/packages/client/ui-skill/src/client/SkillRow.module.css b/packages/client/ui-skill/src/client/SkillRow.module.css index 4dd75f8498..34f255483d 100644 --- a/packages/client/ui-skill/src/client/SkillRow.module.css +++ b/packages/client/ui-skill/src/client/SkillRow.module.css @@ -1,4 +1,4 @@ -/* Skill toolview: Bash-matched summary row plus a bounded instructions disclosure. */ +/* Skill toolview: ToolRow-matched summary line plus a bounded instructions disclosure. */ .card { display: flex; @@ -10,7 +10,7 @@ overflow: hidden; display: flex; align-items: center; - height: 24px; + height: calc(24px + var(--dsh-content-font-delta, 0px)); min-width: 0; } @@ -41,8 +41,8 @@ .leading { position: relative; flex: none; - width: 16px; - height: 16px; + width: calc(16px + var(--dsh-content-font-delta, 0px)); + height: calc(16px + var(--dsh-content-font-delta, 0px)); display: inline-flex; align-items: center; justify-content: center; @@ -50,6 +50,11 @@ color: var(--dsw-alias-label-tertiary); } +.leading svg { + width: calc(14px + var(--dsh-content-font-delta, 0px)); + height: calc(14px + var(--dsh-content-font-delta, 0px)); +} + .chevron { color: var(--dsw-alias-label-secondary); } @@ -78,8 +83,8 @@ .title { flex: none; - font-size: 14px; - line-height: 24px; + font-size: var(--dsh-content-font-size-secondary, 13px); + line-height: calc(24px + var(--dsh-content-font-delta, 0px)); color: var(--dsw-alias-label-secondary); } @@ -98,8 +103,8 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: 14px; - line-height: 24px; + font-size: var(--dsh-content-font-size-secondary, 13px); + line-height: calc(24px + var(--dsh-content-font-delta, 0px)); color: var(--dsw-alias-label-tertiary); } @@ -107,6 +112,10 @@ color: var(--dsw-alias-state-error-primary); } +.stoppedSummary { + color: var(--dsw-alias-state-warn-label); +} + .bodyWrap { display: flex; flex-direction: column; diff --git a/packages/client/ui-skill/src/client/SkillRow.tsx b/packages/client/ui-skill/src/client/SkillRow.tsx index bee776d93c..6f0d3b5c54 100644 --- a/packages/client/ui-skill/src/client/SkillRow.tsx +++ b/packages/client/ui-skill/src/client/SkillRow.tsx @@ -1,6 +1,6 @@ import { useState, type KeyboardEvent, type ReactNode } from 'react' import { - IconChevronDownOutline14, IconInspectOutline12, IconSkillOutline16, StateDot, + IconChevronDownOutlineRegular, IconInspectOutlineRegular, IconSkillOutlineRegular, } from '@deepseek-ai/dsh-client-ui-primitives' import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' @@ -72,34 +72,24 @@ function skillRowModel(block: ToolCallViewProps['block']): SkillRowModel { } } -/** State substitution for the collapsed leading slot. */ -function leadingFor(state: SkillRowState): ReactNode { - switch (state) { - case 'error': return - case 'stopped': return - default: return - } -} - /** Leading disclosure slot: state icon at rest, chevron on hover or while open. */ -function disclosureLeading(state: SkillRowState, open: boolean, expandable: boolean): ReactNode { - if (open) return - const icon = leadingFor(state) +function disclosureLeading(open: boolean, expandable: boolean): ReactNode { + if (open) return + const icon = if (!expandable) return icon return ( <> {icon} - + ) } -/** Visually hidden state copy for the colour-only lifecycle cues. */ +/** Visually hidden state copy for the color-only running sweep and error tone. */ function stateStatus(state: SkillRowState, t: SkillRowProps['t']): string | null { switch (state) { case 'running': return t('row.running') case 'error': return t('row.failed') - case 'stopped': return t('row.stopped') default: return null } } @@ -115,7 +105,7 @@ export function SkillRow({ block, inspect, t }: SkillRowProps) { const expandable = model.output !== null const open = expanded && expandable const status = stateStatus(model.state, t) - const summary = model.errorSummary ?? model.name + const summary = model.state === 'stopped' ? t('row.stopped') : model.errorSummary ?? model.name const toggleExpand = (): void => { setExpanded(value => !value) } @@ -131,7 +121,7 @@ export function SkillRow({ block, inspect, t }: SkillRowProps) { onClick: toggleExpand, onKeyDown: toggleFromKeyboard, } : {} - const leading = disclosureLeading(model.state, open, expandable) + const leading = disclosureLeading(open, expandable) return (
    {status} : null} {t('row.title')} - + {summary}
    @@ -155,7 +148,7 @@ export function SkillRow({ block, inspect, t }: SkillRowProps) { {inspect !== undefined ? ( ) : null} diff --git a/packages/client/ui-skill/tests/skill-row-styles.client.spec.ts b/packages/client/ui-skill/tests/skill-row-styles.client.spec.ts new file mode 100644 index 0000000000..f2bb2b4d3d --- /dev/null +++ b/packages/client/ui-skill/tests/skill-row-styles.client.spec.ts @@ -0,0 +1,38 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +const css = readFileSync(fileURLToPath(new URL('../src/client/SkillRow.module.css', import.meta.url)), 'utf8') +const declarationText = css.replace(/\/\*[\s\S]*?\*\//g, ' ') + +function declarations(selector: string): string[] { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const rule = new RegExp(`(?:^|\\})\\s*${escaped}\\s*\\{([^{}]*)\\}`).exec(declarationText) + if (rule === null) throw new Error(`SkillRow.module.css has no \`${selector}\` rule`) + return (rule[1] ?? '').split(';').map(part => part.trim()).filter(Boolean) +} + +describe('SkillRow.module.css summary line', () => { + it('uses the shared ToolCall secondary font-size axis', () => { + for (const selector of ['.title', '.summary']) { + expect(declarations(selector)).toEqual(expect.arrayContaining([ + 'font-size: var(--dsh-content-font-size-secondary, 13px)', + 'line-height: calc(24px + var(--dsh-content-font-delta, 0px))', + ])) + } + }) + + it('scales the collapsed row and business glyph with the global font delta', () => { + expect(declarations('.row')).toEqual(expect.arrayContaining([ + 'height: calc(24px + var(--dsh-content-font-delta, 0px))', + ])) + expect(declarations('.leading')).toEqual(expect.arrayContaining([ + 'width: calc(16px + var(--dsh-content-font-delta, 0px))', + 'height: calc(16px + var(--dsh-content-font-delta, 0px))', + ])) + expect(declarations('.leading svg')).toEqual(expect.arrayContaining([ + 'width: calc(14px + var(--dsh-content-font-delta, 0px))', + 'height: calc(14px + var(--dsh-content-font-delta, 0px))', + ])) + }) +}) diff --git a/packages/client/ui-skill/tests/skill-row.client.spec.tsx b/packages/client/ui-skill/tests/skill-row.client.spec.tsx index daac0ed0de..da55917f84 100644 --- a/packages/client/ui-skill/tests/skill-row.client.spec.tsx +++ b/packages/client/ui-skill/tests/skill-row.client.spec.tsx @@ -97,6 +97,8 @@ describe('SkillRow', () => { }))} />) const row = screen.getByRole('button', { name: 'skill 加载失败SkillSkillError: missing resource' }) expect(view.container.querySelector('[data-tool="skill"]')?.getAttribute('data-state')).toBe('error') + expect(view.container.querySelector('[data-tool="skill"] > div > span:first-child svg')).not.toBeNull() + expect(view.container.querySelector('[data-tool="skill"] [data-state]')).toBeNull() expect(row.textContent).not.toContain('Check SKILL.md.') fireEvent.click(row) const output = view.container.querySelector('pre')! @@ -108,8 +110,10 @@ describe('SkillRow', () => { const stoppedView = render() - expect(stoppedView.container.textContent).toContain('skill 加载已中止') - expect(stoppedView.container.querySelector('[data-state="warning"]')).not.toBeNull() + const stoppedSummary = stoppedView.getByText('skill 加载已中止') + expect(stoppedSummary.className).toContain('stoppedSummary') + expect(stoppedView.container.querySelector('[data-tool="skill"] > div > span:first-child svg')).not.toBeNull() + expect(stoppedView.container.querySelector('[data-tool="skill"] [data-state]')).toBeNull() cleanup() const structuredView = render(?parent=< ### Browsing the tree -Rows display mode plus `running`/`inactive` activity and an optional log-backed title; the trailing column stacks total durable provider usage above active-turn duration. Keyboard navigation works with ArrowRight/ArrowLeft to expand and collapse branches and ArrowUp/ArrowDown, Home, End, and Escape to navigate or close the tree. An unlabeled one-shot row falls back to its session id; corrupt, unsupported, or unavailable rows remain readable but disabled. +Rows display mode plus `running`/`inactive` activity and an optional log-backed title; running uses the shared ongoing loader, inactive uses the shared idle dot, and diagnostic rows use the shared error dot. The trailing column stacks total durable provider usage above active-turn duration. Keyboard navigation works with ArrowRight/ArrowLeft to expand and collapse branches and ArrowUp/ArrowDown, Home, End, and Escape to navigate or close the tree. An unlabeled one-shot row falls back to its session id; corrupt, unsupported, or unavailable rows remain readable but disabled. ### Continuing a conversation diff --git a/packages/client/ui-subagent/README.zh.md b/packages/client/ui-subagent/README.zh.md index 295206ace1..3da2c3dc27 100644 --- a/packages/client/ui-subagent/README.zh.md +++ b/packages/client/ui-subagent/README.zh.md @@ -31,7 +31,7 @@ kind: "package-reference" ### 浏览目录 -行显示 mode、`running`/`inactive` 活动状态与由日志支撑的可选 title;尾随列在上行显示提供方的持久化 token 用量总计,在下行显示活跃轮次耗时。键盘导航:ArrowRight/ArrowLeft 展开和折叠分支;ArrowUp/ArrowDown、Home、End 与 Escape 用于导航或关闭树。没有 label 的 one-shot 行回退到其会话 id;损坏、不受支持或不可用的行仍保持可读但禁用。 +行显示 mode、`running`/`inactive` 活动状态与由日志支撑的可选 title;running 使用共享 ongoing loading,inactive 使用共享 idle 灰点,诊断行使用共享 error 红点。尾随列在上行显示提供方的持久化 token 用量总计,在下行显示活跃轮次耗时。键盘导航:ArrowRight/ArrowLeft 展开和折叠分支;ArrowUp/ArrowDown、Home、End 与 Escape 用于导航或关闭树。没有 label 的 one-shot 行回退到其会话 id;损坏、不受支持或不可用的行仍保持可读但禁用。 ### 续接对话 diff --git a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css index 713ad62166..87f23eda6a 100644 --- a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css +++ b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.module.css @@ -101,10 +101,11 @@ width: 336px; max-width: min(400px, calc(100vw - 32px)); max-height: min(560px, calc(100vh - 140px)); - padding: 4px; + padding: 3px; overflow: auto; - border-radius: 20px; + border-radius: 16px; background: var(--dsw-specific-menu); + backdrop-filter: var(--dsw-menu-backdrop-filter); --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); @@ -116,24 +117,24 @@ min-width: 0; } .menu > .node { - margin-left: -3px; + margin-left: -2px; } .row { position: relative; display: flex; align-items: flex-start; - gap: 8px; + gap: 6px; box-sizing: border-box; width: 100%; - min-height: 50px; - padding: 7px 8px 7px 11px; + min-height: 44px; + padding: 6px 7px 6px 9px; border: 0; - border-radius: 8px; + border-radius: 7px; background: transparent; color: var(--dsw-alias-label-primary); - font-size: 13px; - line-height: 18px; + font-size: 12px; + line-height: 17px; text-align: left; cursor: pointer; outline: none; @@ -150,16 +151,16 @@ flex: 1; align-self: stretch; align-items: flex-start; - gap: 8px; + gap: 6px; min-width: 0; - margin: -7px -8px -7px; - padding: 7px 8px; - border-radius: 8px; + margin: -6px -7px -6px; + padding: 6px 7px; + border-radius: 7px; } .row > :global([data-state]), .clickarea > :global([data-state]) { - margin-top: 4px; + margin-top: 3px; } .disabled { @@ -179,7 +180,7 @@ .disclosureSpace { flex: none; width: 14px; - height: 18px; + height: 17px; } .disclosure { @@ -194,6 +195,11 @@ transition: transform 120ms ease; } +.disclosure svg { + width: 12px; + height: 12px; +} + .disclosure:hover { color: var(--dsw-alias-label-primary); } @@ -228,13 +234,13 @@ .summary, .metrics { color: var(--dsw-alias-label-tertiary); - font-size: 11px; - line-height: 16px; + font-size: 10px; + line-height: 15px; } .metrics { display: grid; - grid-template-rows: 18px 16px; + grid-template-rows: 17px 15px; flex: none; font-variant-numeric: tabular-nums; text-align: right; @@ -243,7 +249,7 @@ .metricToken { grid-row: 1; - line-height: 18px; + line-height: 17px; } .metricDuration { @@ -275,8 +281,8 @@ .children { position: relative; - margin-left: 18px; - padding-left: 4px; + margin-left: 16px; + padding-left: 3px; } .children::before, @@ -288,8 +294,8 @@ } .children::before { - top: -26px; - height: 26px; + top: -23px; + height: 23px; } .children[aria-busy='true']::before { @@ -299,36 +305,36 @@ .children > .node::before { top: 0; bottom: 0; - left: -4px; + left: -3px; } .children > .node:last-child::before { bottom: auto; - height: 17px; + height: 15px; } .children > .node > .row::before { content: ''; position: absolute; - top: 16px; - left: -4px; - width: 14px; + top: 14px; + left: -3px; + width: 12px; border-top: 0.5px solid var(--dsw-alias-border-l2); } .notice, .error { - padding: 10px 12px; + padding: 8px 10px; color: var(--dsw-alias-label-tertiary); - font-size: 12px; - line-height: 18px; + font-size: 11px; + line-height: 16px; } .error { display: flex; align-items: center; justify-content: space-between; - gap: 12px; + gap: 10px; color: var(--dsw-alias-state-error-primary); } @@ -336,15 +342,20 @@ display: inline-flex; flex: none; align-items: center; - gap: 4px; - padding: 4px 6px; + gap: 3px; + padding: 3px 5px; border: 0; - border-radius: 6px; + border-radius: 5px; background: transparent; color: inherit; cursor: pointer; } +.refresh svg { + width: 12px; + height: 12px; +} + .refresh:hover { background: var(--dsw-alias-interactive-bg-hover); } diff --git a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx index 27fda1d281..81da63ec0f 100644 --- a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx +++ b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx @@ -9,8 +9,7 @@ import { import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { - IconChevronDownOutline14, IconChevronRightOutline14, IconRefreshOutline14, - StateDot, + IconChevronDownOutlineRegular, IconChevronRightOutlineRegular, IconRefreshOutlineRegular, StateDot, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' import { NS } from './locales.ts' @@ -229,7 +228,7 @@ function CatalogLoadingRows({ className={`${css.row} ${css.disabled} ${css.loadingRow}`} > - + {t('loading.label')} @@ -265,7 +264,7 @@ function CatalogRows({ className={css.refresh} onClick={() => { refresh(parentSessionId) }} > - + {t('retry')}
    @@ -379,11 +378,11 @@ function CatalogRows({ aria-label={t(isExpanded ? 'branch.collapse' : 'branch.expand', { label })} onClick={toggle} > - + )}
    - + {label} {secondary} @@ -410,7 +409,7 @@ function CatalogRows({ onClick={openAside} onKeyDown={(event) => { event.stopPropagation() }} > - + )}
    @@ -784,7 +783,7 @@ function CatalogDropdown({ )} {variant === 'switcher' ? - : } + : } {open && createPortal((
    () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) }, bind: () => stubSettingsScope().scope } as never) await provideSlotFaces(ctx) await ctx.plugin({ inject: localeInject, apply: applyLocale }).await() await ctx.plugin({ inject: [...inject], apply }).await() diff --git a/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx b/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx index 4b828bca13..1311aae860 100644 --- a/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx +++ b/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx @@ -524,8 +524,8 @@ describe('SubagentHeaderLineage', () => { }, summaries) view.rerender() expect(screen.getByRole('group').getAttribute('aria-busy')).toBeNull() - expect(screen.getByRole('treeitem', { name: /indexer/ })).toBeTruthy() - expect(screen.getByRole('treeitem', { name: /critic/ })).toBeTruthy() + expect(screen.getByRole('treeitem', { name: /indexer/ }).querySelector('[data-state="idle"]')).not.toBeNull() + expect(screen.getByRole('treeitem', { name: /critic/ }).querySelector('[data-state="ongoing"]')).not.toBeNull() expect(screen.queryByRole('treeitem', { name: '正在加载子代理' })).toBeNull() }) diff --git a/packages/client/ui-theme/README.i18n.yaml b/packages/client/ui-theme/README.i18n.yaml index bbb71eb326..dfcdd66ea3 100644 --- a/packages/client/ui-theme/README.i18n.yaml +++ b/packages/client/ui-theme/README.i18n.yaml @@ -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 packages/client/ui-theme/README.md -README.md: a25f0202d278d52448d52a22f451612295325f87 -README.zh.md: 08a0c37dc3c80b4385173e6ca74bb9254b4870ea +README.md: bc8f0402eadbe30971db265f330ceb6af19095ce +README.zh.md: dc3a3672fee40903c9c6a4de4922db6988187de1 diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index a25f0202d2..bc8f0402ea 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -51,15 +51,15 @@ The service owns theme and font-size state and publishes snapshots. The ui-layou ### Stylesheets -`src/styles/` holds six sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` is the sole consumer of the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. +`src/styles/` holds six sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` consumes the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. Status marks use their own semantic state tokens. `corner-shape.css` smooths every rounded corner: inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape` and applies it to all elements and their `::before`/`::after` through the universal selector, so engines without `corner-shape` keep circular corners. Full-round shapes — `border-radius: 50%` circles and pill radii — pair `corner-shape: round` with their radius in the owning component sheet because a superellipse deforms them; the corner-shape stylesheet spec enforces that pairing across every package stylesheet. -`gradient-shadow-text.css` derives `--dsh-content-font-delta` from `--dsh-content-font-size` and shifts the Markdown heading and base-text ladder by that increment. It also derives the secondary tier `--dsh-content-font-size-secondary` (setting −1 at ≤14, setting −2 above; 13px at the default) with its own `--dsh-content-font-delta-secondary` for the table variants and the flow rows one step under the body. Dense small and code variants stay fixed. Outside the ladder, the user bubble and composer draft read the body pair directly, and flow-row titles and summaries read the secondary pair. The sheet also owns the shadow scale (`--dsw-shadow-lv*`) and the elevation tokens: `--dsw-elevation-stroke` draws a 0.5px hairline through the rebindable `--dsw-elevation-stroke-color`, and `--dsw-elevation-panel`/`--dsw-elevation-prominent`/`--dsw-elevation-soft` (the composer's larger-blur, lower-alpha tier) layer two faint soft shadows over that stroke, so elevated surfaces set `border: 0` and carry no layout-consuming outline; the derived tokens are re-declared per element so a surface's stroke-color rebind takes effect. +`gradient-shadow-text.css` derives `--dsh-content-font-delta` from `--dsh-content-font-size` and shifts the Markdown heading and base-text ladder by that increment. It also derives the secondary tier `--dsh-content-font-size-secondary` (setting −1 at ≤14, setting −2 above; 13px at the default) with its own `--dsh-content-font-delta-secondary` for the table variants and the flow rows one step under the body. Dense small and code variants stay fixed. Outside the ladder, the user bubble and composer draft read the body pair directly, and flow-row titles and summaries read the secondary pair. The sheet also owns the shadow scale (`--dsw-shadow-lv*`), the translucent-menu `--dsw-menu-backdrop-filter`, and the elevation tokens: `--dsw-elevation-stroke` draws a 0.5px hairline through the rebindable `--dsw-elevation-stroke-color`, and `--dsw-elevation-panel`/`--dsw-elevation-prominent`/`--dsw-elevation-soft` (the composer's larger-blur, lower-alpha tier) layer two faint soft shadows over that stroke, so elevated surfaces set `border: 0` and carry no layout-consuming outline; the derived tokens are re-declared per element so a surface's stroke-color rebind takes effect. An elevated surface that paints `--dsw-specific-menu` also applies `backdrop-filter: var(--dsw-menu-backdrop-filter)` ([decision](../../../.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.md)). ### Scrollbar rebinding -`scrollbar.css` binds `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover` on `body` to the l1 base-surface tokens; an elevated surface (menu, popover, dialog) rebinds them to the l2 tokens on its own container, and the pair's other legal target is `transparent` (ui-sidebar rebinds its column that way while the pointer is elsewhere). WebKit-based browsers also read `--dsh-scrollbar-width`, `--dsh-scrollbar-thumb-border`, and `--dsh-scrollbar-track-margin`; a scroll surface may rebind them to keep a wide draggable rail around a narrower visible thumb or to inset the track from rounded ends. The two rendering paths are mutually exclusive by construction: Firefox takes the standard thin scrollbar inside `@supports not selector(::-webkit-scrollbar)`, and WebKit-based engines take the pseudo-elements, so geometry and hover customization apply only through the pseudo-element path. +`scrollbar.css` binds `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover` on `body` to the l1 base-surface tokens; an elevated surface (menu, popover, dialog) rebinds them to the l2 tokens on its own container, and the pair's other legal target is `transparent` (ui-sidebar rebinds its column that way while the pointer is elsewhere). WebKit-based browsers use a 5px default `--dsh-scrollbar-width` and also read `--dsh-scrollbar-thumb-border` and `--dsh-scrollbar-track-margin`; a scroll surface may rebind them to keep a wide draggable rail around a narrower visible thumb or to inset the track from rounded ends. The two rendering paths are mutually exclusive by construction: Firefox takes the standard thin scrollbar inside `@supports not selector(::-webkit-scrollbar)`, and WebKit-based engines take the pseudo-elements, so geometry and hover customization apply only through the pseudo-element path. ### Preference persistence diff --git a/packages/client/ui-theme/README.zh.md b/packages/client/ui-theme/README.zh.md index 08a0c37dc3..dc3a3672fe 100644 --- a/packages/client/ui-theme/README.zh.md +++ b/packages/client/ui-theme/README.zh.md @@ -51,15 +51,15 @@ kind: "package-reference" ### 样式表 -`src/styles/` 下有六张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR(热模块替换)会随 ui-theme 一同移除。`scrollbar.css` 是 `--dsw-alias-scrollbar-*` token 的唯一消费方,必须排在声明这些 token 的 `design-platform.css` 之后。 +`src/styles/` 下有六张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR(热模块替换)会随 ui-theme 一同移除。`scrollbar.css` 消费 `--dsw-alias-scrollbar-*` token,必须排在声明这些 token 的 `design-platform.css` 之后。状态标记使用各自的语义状态 token。 `corner-shape.css` 平滑所有圆角:在 `@supports (corner-shape: superellipse(1.5))` 内定义 `--dsw-corner-shape`,并通过通配选择器应用到所有元素及其 `::before`/`::after`,因此不支持 `corner-shape` 的引擎保持普通圆弧。正圆形状——`border-radius: 50%` 的圆与胶囊半径——因超级椭圆会使其变形,须在所属组件样式表中把 `corner-shape: round` 与半径声明配对;corner-shape 样式表 spec 跨全部包样式表强制这一配对。 -`gradient-shadow-text.css` 从 `--dsh-content-font-size` 派生 `--dsh-content-font-delta`,并以该增量移动 Markdown 标题与基础文本阶梯。它同时派生低一档变量 `--dsh-content-font-size-secondary`(设置 ≤14 时为设置值 −1,>14 时为设置值 −2;默认设置下为 13 px)及配套的 `--dsh-content-font-delta-secondary`,供表格变体与比正文低一档的流内行使用。紧凑的小号文本与代码变体保持固定字号。阶梯之外,用户气泡与 composer 草稿直接读取正文字号变量对,流内行的标题及摘要读取低一档变量对。该表还持有阴影阶(`--dsw-shadow-lv*`)与 elevation token:`--dsw-elevation-stroke` 经可重绑的 `--dsw-elevation-stroke-color` 画 0.5 px 发丝描边,`--dsw-elevation-panel`/`--dsw-elevation-prominent`/`--dsw-elevation-soft`(composer 专用的更大模糊、更低透明度档)在描边之上叠两层极淡柔光,因此高层级表面设 `border: 0`,不再有占布局的轮廓;派生 token 逐元素重声明,使表面对描边色的重绑真实生效。 +`gradient-shadow-text.css` 从 `--dsh-content-font-size` 派生 `--dsh-content-font-delta`,并以该增量移动 Markdown 标题与基础文本阶梯。它同时派生低一档变量 `--dsh-content-font-size-secondary`(设置 ≤14 时为设置值 −1,>14 时为设置值 −2;默认设置下为 13 px)及配套的 `--dsh-content-font-delta-secondary`,供表格变体与比正文低一档的流内行使用。紧凑的小号文本与代码变体保持固定字号。阶梯之外,用户气泡与 composer 草稿直接读取正文字号变量对,流内行的标题及摘要读取低一档变量对。该表还持有阴影阶(`--dsw-shadow-lv*`)、半透明菜单使用的 `--dsw-menu-backdrop-filter` 与 elevation token:`--dsw-elevation-stroke` 经可重绑的 `--dsw-elevation-stroke-color` 画 0.5 px 发丝描边,`--dsw-elevation-panel`/`--dsw-elevation-prominent`/`--dsw-elevation-soft`(composer 专用的更大模糊、更低透明度档)在描边之上叠两层极淡柔光,因此高层级表面设 `border: 0`,不会产生占布局的轮廓;派生 token 逐元素重声明,使表面对描边色的重绑真实生效。绘制 `--dsw-specific-menu` 的高层级表面还会应用 `backdrop-filter: var(--dsw-menu-backdrop-filter)`([决定](../../../.agents/notes/implemented/feature/2026-09-17-compact-translucent-menu-surfaces.zh.md))。 ### 滚动条重新绑定 -`scrollbar.css` 在 `body` 上把 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover` 绑定到 l1 基础表面 token;高层级表面(菜单、浮层、对话框)在自己的容器上把它们重新绑定为 l2 token;这组变量的另一个合法目标是 `transparent`(ui-sidebar 在指针不在栏内时就这样重新绑定自己的列)。WebKit 系浏览器还会读取 `--dsh-scrollbar-width`、`--dsh-scrollbar-thumb-border` 与 `--dsh-scrollbar-track-margin`;滚动表面可重新绑定它们,在较窄的可见滑块外保留较宽的拖动区域,或让轨道避开圆角两端。两条渲染路径在构造上互斥:Firefox 走 `@supports not selector(::-webkit-scrollbar)` 内的标准细滚动条,WebKit 系引擎走伪元素,因此几何与 hover 定制只经由伪元素路径生效。 +`scrollbar.css` 在 `body` 上把 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover` 绑定到 l1 基础表面 token;高层级表面(菜单、浮层、对话框)在自己的容器上把它们重新绑定为 l2 token;这组变量的另一个合法目标是 `transparent`(ui-sidebar 在指针不在栏内时就这样重新绑定自己的列)。WebKit 系浏览器默认使用 5px 的 `--dsh-scrollbar-width`,并读取 `--dsh-scrollbar-thumb-border` 与 `--dsh-scrollbar-track-margin`;滚动表面可重新绑定它们,在较窄的可见滑块外保留较宽的拖动区域,或让轨道避开圆角两端。两条渲染路径在构造上互斥:Firefox 走 `@supports not selector(::-webkit-scrollbar)` 内的标准细滚动条,WebKit 系引擎走伪元素,因此几何与 hover 定制只经由伪元素路径生效。 ### 偏好持久化 diff --git a/packages/client/ui-theme/src/client/AppearanceRow.tsx b/packages/client/ui-theme/src/client/AppearanceRow.tsx index b1db97d009..d234bd81a5 100644 --- a/packages/client/ui-theme/src/client/AppearanceRow.tsx +++ b/packages/client/ui-theme/src/client/AppearanceRow.tsx @@ -7,7 +7,7 @@ */ import clsx from 'clsx' import { - IconDarkOutline16, IconFollowsystemOutline16, IconLightOutline16, + IconDarkOutlineMedium, IconFollowsystemOutlineMedium, IconLightOutlineMedium, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' import type { ThemePreference } from '../theme-settings.ts' @@ -28,10 +28,10 @@ export type AppearanceRowComponentProps = & PropsLocale<'settings.theme'> & AppearanceRowInjected /** Cube order and icons (figma 501:30015-30017: Light, Dark, System). */ -const CUBES: readonly { id: ThemePreference; labelKey: ThemeKey; Icon: typeof IconLightOutline16 }[] = [ - { id: 'light', labelKey: 'appearance.light', Icon: IconLightOutline16 }, - { id: 'dark', labelKey: 'appearance.dark', Icon: IconDarkOutline16 }, - { id: 'system', labelKey: 'appearance.system', Icon: IconFollowsystemOutline16 }, +const CUBES: readonly { id: ThemePreference; labelKey: ThemeKey; Icon: typeof IconLightOutlineMedium }[] = [ + { id: 'light', labelKey: 'appearance.light', Icon: IconLightOutlineMedium }, + { id: 'dark', labelKey: 'appearance.dark', Icon: IconDarkOutlineMedium }, + { id: 'system', labelKey: 'appearance.system', Icon: IconFollowsystemOutlineMedium }, ] /** diff --git a/packages/client/ui-theme/src/client/FontSizeRow.tsx b/packages/client/ui-theme/src/client/FontSizeRow.tsx index 2783f3a0fc..5754ae7ad9 100644 --- a/packages/client/ui-theme/src/client/FontSizeRow.tsx +++ b/packages/client/ui-theme/src/client/FontSizeRow.tsx @@ -8,7 +8,7 @@ * click echo. */ import { - IconChevronDownOutline14, IconChevronUpOutline14, + IconChevronDownOutlineRegular, IconChevronUpOutlineRegular, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' import { FONT_SIZE_MAX, FONT_SIZE_MIN } from '../theme-settings.ts' @@ -51,7 +51,7 @@ export function FontSizeRow({ t, setFontSize, useStore }: FontSizeRowComponentPr disabled={fontSize >= FONT_SIZE_MAX} onClick={() => { setFontSize(fontSize + 1) }} > - +
    diff --git a/packages/client/ui-theme/src/client/index.ts b/packages/client/ui-theme/src/client/index.ts index 9997d3f291..83f1a22082 100644 --- a/packages/client/ui-theme/src/client/index.ts +++ b/packages/client/ui-theme/src/client/index.ts @@ -139,6 +139,7 @@ const BUILTIN_INSPECT_TOKENS: readonly ThemeTokenInspection[] = Object.freeze([ { name: '--dsw-alias-label-primary', description: 'Primary text color.', valueType: 'CSS color', requiresLightAndDark: true, cssVariable: '--dsw-alias-label-primary' }, { name: '--dsw-alias-label-secondary', description: 'Secondary text color.', valueType: 'CSS color', requiresLightAndDark: true, cssVariable: '--dsw-alias-label-secondary' }, { name: '--dsw-alias-state-error-primary', description: 'Primary error state color.', valueType: 'CSS color', requiresLightAndDark: true, cssVariable: '--dsw-alias-state-error-primary' }, + { name: '--dsw-alias-state-idle-primary', description: 'Primary inactive state color.', valueType: 'CSS color', requiresLightAndDark: true, cssVariable: '--dsw-alias-state-idle-primary' }, { name: '--dsw-alias-state-success-primary', description: 'Primary success state color.', valueType: 'CSS color', requiresLightAndDark: true, cssVariable: '--dsw-alias-state-success-primary' }, { name: '--dsw-alias-state-warn-primary', description: 'Primary warning state color.', valueType: 'CSS color', requiresLightAndDark: true, cssVariable: '--dsw-alias-state-warn-primary' }, { name: '--dsw-specific-sidebar-fill', description: 'Sidebar column and title-row background.', valueType: 'CSS color', requiresLightAndDark: true, cssVariable: '--dsw-specific-sidebar-fill' }, diff --git a/packages/client/ui-theme/src/styles/design-platform.css b/packages/client/ui-theme/src/styles/design-platform.css index 8b32be8feb..9ff8e82684 100644 --- a/packages/client/ui-theme/src/styles/design-platform.css +++ b/packages/client/ui-theme/src/styles/design-platform.css @@ -226,6 +226,7 @@ body { --dsw-alias-state-business-tertiary: var(--dsw-static-deepseek-100); --dsw-alias-state-error-primary: var(--dsw-static-red-600); --dsw-alias-state-error-secondary: var(--dsw-static-red-400); + --dsw-alias-state-idle-primary: var(--dsw-static-neutral-300); --dsw-alias-state-success-primary: var(--dsw-static-green-500); --dsw-alias-state-success-secondary: var(--dsw-static-green-400); --dsw-alias-state-success-tertiary: var(--dsw-static-green-100); @@ -239,7 +240,7 @@ body { --dsw-specific-bubble: var(--dsw-static-deepseek-50); --dsw-specific-input-major: var(--dsw-static-neutral-bluish-00); --dsw-specific-login-input: var(--dsw-static-neutral-bluish-50); - --dsw-specific-menu: var(--dsw-alias-bg-layer-3); + --dsw-specific-menu: rgba(248, 249, 250, 0.58); --dsw-specific-selector: var(--dsw-static-neutral-bluish-60); --dsw-specific-sidebar-fill: var(--dsw-static-neutral-bluish-50); --dsw-specific-sidebar-nav-item-active-accent: var(--dsw-static-deepseek-100); @@ -323,6 +324,7 @@ body[data-ds-dark-theme] { --dsw-alias-state-business-tertiary: var(--dsw-static-deepseek-800); --dsw-alias-state-error-primary: var(--dsw-static-red-400); --dsw-alias-state-error-secondary: var(--dsw-static-red-400); + --dsw-alias-state-idle-primary: var(--dsw-static-neutral-600); --dsw-alias-state-success-primary: var(--dsw-static-green-500); --dsw-alias-state-success-secondary: var(--dsw-static-green-400); --dsw-alias-state-success-tertiary: var(--dsw-static-green-900); @@ -336,7 +338,7 @@ body[data-ds-dark-theme] { --dsw-specific-bubble: var(--dsw-static-neutral-bluish-850); --dsw-specific-input-major: var(--dsw-static-neutral-bluish-850); --dsw-specific-login-input: var(--dsw-static-neutral-bluish-900); - --dsw-specific-menu: var(--dsw-alias-bg-layer-3); + --dsw-specific-menu: rgba(48, 49, 54, 0.5); --dsw-specific-selector: var(--dsw-static-neutral-bluish-800); --dsw-specific-sidebar-fill: var(--dsw-static-neutral-bluish-900); --dsw-specific-sidebar-nav-item-active-accent: var(--dsw-static-neutral-bluish-800); diff --git a/packages/client/ui-theme/src/styles/gradient-shadow-text.css b/packages/client/ui-theme/src/styles/gradient-shadow-text.css index 36fee49415..b6dc4db464 100644 --- a/packages/client/ui-theme/src/styles/gradient-shadow-text.css +++ b/packages/client/ui-theme/src/styles/gradient-shadow-text.css @@ -15,8 +15,9 @@ body { 模糊、更低透明度的柔光。默认色只声明在 body 上,让表面的重绑沿继承 传给真正消费投影的后代。 */ --dsw-elevation-stroke-color: var(--dsw-alias-border-l4); - /* blur filter */ + /* Shared blur treatments for blocking masks and translucent menus. */ --dsw-mask-blur: blur(2px); + --dsw-menu-backdrop-filter: blur(40px) saturate(150%); } /* 派生 elevation 值逐元素声明而非从 body 继承:继承传下来的是在 body 上就 diff --git a/packages/client/ui-theme/src/styles/scrollbar.css b/packages/client/ui-theme/src/styles/scrollbar.css index d61bcbcedb..035d315130 100644 --- a/packages/client/ui-theme/src/styles/scrollbar.css +++ b/packages/client/ui-theme/src/styles/scrollbar.css @@ -22,14 +22,14 @@ body { below. A surface that must align itself beside a space-consuming bar (ConversationRoot's overlay composer seat) reads this instead of hardcoding the number. */ - --dsh-scrollbar-width: 8px; + --dsh-scrollbar-width: 5px; } /* The two paths are mutually exclusive, and the gate is load-bearing rather than defensive. A non-`auto` `scrollbar-width` or `scrollbar-color` makes Chromium and Safari drop every `::-webkit-scrollbar*` rule for that element, including `::-webkit-scrollbar-thumb:hover` — measured in chromium - as an 8px `::-webkit-scrollbar` width taking effect on its own and being + as a `::-webkit-scrollbar` width taking effect on its own and being ignored as soon as `scrollbar-width: thin` is added. Declaring both unconditionally therefore leaves the hover tokens with no rendering at all, because the engines that implement the hover pseudo-element are exactly the diff --git a/packages/client/ui-theme/tests/elevation-styles.client.spec.ts b/packages/client/ui-theme/tests/elevation-styles.client.spec.ts index bf246f2d10..ade17f1f06 100644 --- a/packages/client/ui-theme/tests/elevation-styles.client.spec.ts +++ b/packages/client/ui-theme/tests/elevation-styles.client.spec.ts @@ -22,6 +22,8 @@ const NEUTRAL_BORDER = /--dsw-alias-border-/ const sheetCss = readFileSync( fileURLToPath(new URL('../src/styles/gradient-shadow-text.css', import.meta.url)), 'utf8') +const platformCss = readFileSync( + fileURLToPath(new URL('../src/styles/design-platform.css', import.meta.url)), 'utf8') describe('elevation tokens', () => { const rules = parseRules(sheetCss) @@ -53,6 +55,76 @@ describe('elevation tokens', () => { expect(bodyOnly.has(name), name).toBe(false) } }) + + it('defines the translucent menu material for both palettes', () => { + expect(bodyOnly.get('--dsw-menu-backdrop-filter')).toBe('blur(40px) saturate(150%)') + const platformRules = parseRules(platformCss) + const value = (selector: string): string | undefined => platformRules + .filter(rule => rule.selectors.includes(selector)) + .flatMap(rule => rule.declarations) + .findLast(([property]) => property === '--dsw-specific-menu')?.[1] + expect(value('body')).toBe('rgba(248, 249, 250, 0.58)') + expect(value('body[data-ds-dark-theme]')).toBe('rgba(48, 49, 54, 0.5)') + }) +}) + +/** Elevated or isolated-background menu-fill rules that omit the shared backdrop filter. */ +function translucentMenusWithoutBackdrop(css: string): string[] { + return parseRules(css) + .filter(rule => rule.declarations.some(([property, value]) => + (property === 'background' || property === 'background-color') + && value === 'var(--dsw-specific-menu)')) + .filter(rule => rule.declarations.some(([property, value]) => + property === 'box-shadow' && ELEVATED_SHADOW.test(value)) + || rule.selectors.some(selector => /::(?:before|after)$/.test(selector))) + .filter(rule => !rule.declarations.some(([property, value]) => + property === 'backdrop-filter' && value === 'var(--dsw-menu-backdrop-filter)')) + .map(rule => rule.selectors.join(', ')) +} + +describe('translucent menu surfaces pair fill and filter', () => { + it('rejects a menu fill without the shared backdrop filter', () => { + expect(translucentMenusWithoutBackdrop( + '.a { background: var(--dsw-specific-menu); box-shadow: var(--dsw-elevation-panel); }', + )).toEqual(['.a']) + expect(translucentMenusWithoutBackdrop( + '.a { background: var(--dsw-specific-menu); box-shadow: var(--dsw-elevation-panel); backdrop-filter: var(--dsw-menu-backdrop-filter); }', + )).toEqual([]) + expect(translucentMenusWithoutBackdrop( + '.a::before { background: var(--dsw-specific-menu); }', + )).toEqual(['.a::before']) + expect(translucentMenusWithoutBackdrop( + '.a::before { background: var(--dsw-specific-menu); backdrop-filter: var(--dsw-menu-backdrop-filter); }', + )).toEqual([]) + }) + + it('covers every package menu-fill consumer', () => { + const missing = packageStylesheets().flatMap(file => + translucentMenusWithoutBackdrop(readFileSync(file, 'utf8')) + .map(selectors => `${file} ${selectors}`)) + expect(missing).toEqual([]) + }) + + it('keeps backdrop filtering on background layers when descendants use fixed positioning', () => { + const surfaces = [ + ['packages/client/ui-goal/src/client/GoalBar.module.css', '.bar', '.bar::before'], + ['packages/client/ui-conversation/src/client/queue/QueueDock.module.css', '.panel', '.panel::before'], + ['packages/extensions/ui-cordis/src/client/CordisPanel.module.css', '.panel', '.panel::before'], + ] as const + const files = packageStylesheets() + for (const [suffix, container, background] of surfaces) { + const file = files.find(candidate => candidate.replaceAll('\\', '/').endsWith(suffix)) + expect(file, suffix).toBeDefined() + const rules = parseRules(readFileSync(file!, 'utf8')) + const declarations = (selector: string) => new Map(rules + .filter(rule => rule.selectors.length === 1 && rule.selectors[0] === selector) + .flatMap(rule => rule.declarations)) + expect(declarations(container).has('backdrop-filter'), container).toBe(false) + expect(declarations(background).get('background'), background).toBe('var(--dsw-specific-menu)') + expect(declarations(background).get('backdrop-filter'), background) + .toBe('var(--dsw-menu-backdrop-filter)') + } + }) }) /** @@ -144,7 +216,6 @@ describe('neutral solid borders are hairlines', () => { */ const RING_TRACKS = new Set([ 'boot-page.module.css .spinner', - 'TrajectoryTable.module.css .historyLoadingSpinner', ]) it('rejects a wide neutral border and a wide filled divider', () => { diff --git a/packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts b/packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts index 9682bdeb3c..48a49786cc 100644 --- a/packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts +++ b/packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts @@ -142,6 +142,8 @@ const SURFACE_PROPERTIES = ['background', 'background-color'] * carries a radius, a shadow, and a fixed size, so shape cannot separate them. */ const SURFACE_TOKEN_PATTERN = /^--dsw-(?:alias-bg-|specific-)/ +/** Translucent elevated surfaces whose colors do not resolve to a palette rung. */ +const TRANSLUCENT_ELEVATED_SURFACES = new Set(['--dsw-specific-menu']) /** * The palette's own dark elevation ladder, resolved from `design-platform.css`: @@ -178,7 +180,7 @@ function elevatedRungs(): Set { return tokens } -const elevatedSurfaces = elevatedRungs() +const elevatedSurfaces = new Set([...elevatedRungs(), ...TRANSLUCENT_ELEVATED_SURFACES]) for (const file of packageStylesheets()) { const rules = parseRules(readFileSync(file, 'utf8')) @@ -471,15 +473,16 @@ describe('elevated surface rebinds', () => { } }) - it('resolves the elevated surface set from the palette ladder', () => { + it('resolves opaque elevated surfaces from the palette ladder and includes translucent elevated surfaces', () => { // The set has to come from the palette, not from the sheets that happen to // rebind: derived from rebinds it can only confirm what someone already // remembered, and a surface nobody has rebound yet — the case the check // exists for — would define itself as unelevated. Anchoring it here means a // new palette token on an elevated rung is in scope the moment it is // defined. `--dsw-specific-tip` is the regression that proved the point: it - // resolves to the same dark rung as the menu surface, and the Todo panel - // scrolled on it unrebound while a rebind-derived set stayed green. + // resolves to the same dark rung as the input surface, and the Todo panel + // scrolled on it unrebound while a rebind-derived set stayed green. The + // translucent menu remains explicit because its alpha color has no rung. expect(elevatedSurfaces).toContain('--dsw-alias-bg-layer-2') expect(elevatedSurfaces).toContain('--dsw-alias-bg-layer-3') expect(elevatedSurfaces).toContain('--dsw-specific-menu') diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index 7a92445368..04b554ad36 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/README.i18n.yaml @@ -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 packages/client/ui-tool/README.md -README.md: 15dffa634660a5cf94081452481399821d09b25b -README.zh.md: 6f01658e5fea4fefca9eded114971d3e88a13148 +README.md: 067fa16bad9f6a34b26da3402de5e0a574323500 +README.zh.md: 9376493b780511c6e412cff4c5b976126c230693 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index 15dffa6346..067fa16bad 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -25,7 +25,7 @@ English | [中文](README.zh.md) ## Use this package -Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Users see running, successful, failed, and interrupted states that come only from the frozen call/result slice, and can open files or inspect calls through the Host callbacks. +Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. ### Registering a business tool view @@ -62,7 +62,7 @@ The package realizes one dispatch rule: atomic Tool views are keyed by wire Tool ### Cards -Every card is read in place in the call tree; there is no second, full-height presentation of a selected call. Row renderers share one pure card model for each terminal, read, diff, search, and web card, and the image card's gallery renders through the tool-owned `tool.call.images` slot. These models validate raw call arguments, result content, failure state, persisted metadata, PTC dispatch `parentCallId`, and Session path facts. Unsupported or malformed inputs use flattened Tool result text. A file-path summary opens the file through the owner's `openFile`, which the chat view routes to the right Sidebar's text preview; `inspect` opens the trajectory view. Card-specific limits and fallback rules for the terminal, diff, read, search, and web cards remain in [the ui-primitives README](../ui-primitives/README.md); the image card's model in this package carries its own fallback rules. +Every card is read in place in the call tree; there is no second, full-height presentation of a selected call. Row renderers share one pure card model for each terminal, read, diff, search, and web card, and the image card's gallery renders through the tool-owned `tool.call.images` slot. These models validate raw call arguments, result content, failure state, persisted metadata, PTC dispatch `parentCallId`, and Session path facts. Unsupported or malformed inputs use flattened Tool result text. A file-path summary opens the file through the owner's `openFile`, which the chat view routes to the right Sidebar's text preview; `inspect` opens the trajectory view and is absent when that View is unavailable; cards then omit Inspect. Card-specific limits and fallback rules for the terminal, diff, read, search, and web cards remain in [the ui-primitives README](../ui-primitives/README.md); the image card's model in this package carries its own fallback rules. Chat diff cards keep nine rows before folding, enough for a file header, one removed/added pair, and three context lines on either side. The collapsed row and expanded footer share the primitive's exact or coarse-replacement counts. diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 6f01658e5f..9376493b78 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。用户看到运行中、成功、失败与中断状态,这些状态只来自冻结的调用/结果切片,并可通过宿主回调打开文件或检查调用。 +工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。 ### 注册业务工具视图 @@ -62,7 +62,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block` ### 卡片 -每张卡片都直接在调用树中查看;选中调用后不会再显示第二个全高视图。行 renderer 为 terminal、read、diff、search 和 web 卡片各复用同一个纯 card model,image 卡片的图库经由工具自有 `tool.call.images` slot 渲染。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、PTC dispatch 的 `parentCallId` 与会话路径信息。不受支持或格式错误的输入使用压平的工具结果文本。文件路径摘要经属主的 `openFile` 打开文件,chat 视图把它路由到右侧 Sidebar 的文本预览;`inspect` 打开轨迹视图。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。 +每张卡片都直接在调用树中查看;选中调用后不会再显示第二个全高视图。行 renderer 为 terminal、read、diff、search 和 web 卡片各复用同一个纯 card model,image 卡片的图库经由工具自有 `tool.call.images` slot 渲染。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、PTC dispatch 的 `parentCallId` 与会话路径信息。不受支持或格式错误的输入使用压平的工具结果文本。文件路径摘要经属主的 `openFile` 打开文件,chat 视图把它路由到右侧 Sidebar 的文本预览;`inspect` 打开轨迹视图;该视图不可用时不提供此回调,卡片随之隐藏 Inspect。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。 Chat diff 卡片在折叠前保留九行,足以容纳文件标题、一对删除与新增行及两侧各三行上下文。折叠工具行与展开卡片底部采用原语一致的精确或粗粒度替换统计。 diff --git a/packages/client/ui-tool/src/client/tool/ToolCallTree.tsx b/packages/client/ui-tool/src/client/tool/ToolCallTree.tsx index f4d62697f7..6338f89f1c 100644 --- a/packages/client/ui-tool/src/client/tool/ToolCallTree.tsx +++ b/packages/client/ui-tool/src/client/tool/ToolCallTree.tsx @@ -29,7 +29,7 @@ const ToolCall = memo(function ToolCall({ cwd, home, loadImage, - inspect: () => { inspectCall(callId) }, + inspect: inspectCall === undefined ? undefined : () => { inspectCall(callId) }, }), [callId, toolName, block, openFile, cwd, home, loadImage, inspectCall]) const autoReviewDenied = useMemo( () => toolRowModel(toolName, block).autoReviewDenial !== null, diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css b/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css index f1933dc068..71bd2d41e1 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css @@ -155,6 +155,11 @@ color: var(--dsw-alias-state-error-primary); } +/* Interrupted rows retain their business glyph and replace the summary with a visible warning. */ +.stoppedSummary { + color: var(--dsw-alias-state-warn-label); +} + /* Expanded body + Inspect pill wrapper (sibling of .row: clicks never toggle). */ .bodyWrap { display: flex; @@ -344,9 +349,8 @@ border: 0.5px solid var(--dsw-alias-border-l1); } -/* Visually hidden run-state label for assistive technology: the StateDot and - the running sweep are aria-hidden / colour-only, so the text carries the - running/failed/interrupted state to a screen reader. */ +/* Visually hidden run-state label for assistive technology: the running sweep + and settlement summary tones are color-only. */ .visuallyHidden { position: absolute; width: 1px; diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx index 0472875209..fb8f42949f 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx @@ -1,7 +1,8 @@ import { useMemo, useState, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react' import clsx from 'clsx' import { - CodeBlock, DiffBlock, DisclosureRow, IconInspectOutline12, ReadBlock, SearchBlock, StateDot, TerminalBlock, WebBlock, + CodeBlock, DiffBlock, DisclosureRow, IconInspectOutlineRegular, ReadBlock, SearchBlock, + TerminalBlock, WebBlock, diffTotals, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsRenderSlots, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' @@ -87,18 +88,7 @@ export interface ToolRowProps { inspect?: (() => void) | undefined } -function leadingFor(state: ToolRowState, icon: ReactNode): ReactNode { - switch (state) { - case 'error': return - case 'stopped': return - default: return icon - } -} - -/** Visually hidden run-state label: the StateDot and the CSS sweep are both - * aria-hidden / colour-only, so assistive technology needs this text to know a - * row is running, failed, or interrupted. null in the ok state (the icon and - * summary already describe a settled row). */ +/** Visually hidden run-state label for color-only running and settlement cues. */ function stateStatus(state: ToolRowState, t: TranslateNS<'conversation'>): string | null { switch (state) { case 'running': return t('row.running') @@ -161,9 +151,12 @@ export function ToolRow({ [card, inputRaw, open, variant], ) const status = stateStatus(state, t) - // A failure must replace, not supplement, the normal summary. - const failureLine = state === 'error' ? errorSummary ?? null : null - const summaryText = failureLine ?? terminalBody?.description ?? summary + const normalSummary = terminalBody?.description ?? summary + // A failure keeps its first result line when available and otherwise turns + // the ordinary summary red. An interruption turns the tool-owned summary + // amber while retaining the business icon and hidden state announcement. + const failureLine = state === 'error' ? errorSummary ?? normalSummary : null + const summaryText = failureLine ?? normalSummary // A diff row's collapsed line carries the card's +/- totals (the same // numbers the expanded footer prints) so the change size reads without // expanding; an explicit summarySuffix (none today on diff rows) wins. @@ -172,11 +165,12 @@ export function ToolRow({ const { added, removed } = diffTotals(diffBody.card.diffs) return `+${added} -${removed}` }, [diffBody]) - const suffix = failureLine === null ? summarySuffix ?? diffStat : null + const settledWithCue = state === 'error' || state === 'stopped' + const suffix = settledWithCue ? null : summarySuffix ?? diffStat const toggleExpand = () => { setExpanded(v => !v) } - const openFile = filePath !== undefined && onOpenFile !== undefined && failureLine === null + const openFile = filePath !== undefined && onOpenFile !== undefined && !settledWithCue ? (event: MouseEvent) => { event.stopPropagation() if (filePathLine === undefined) onOpenFile(filePath) @@ -201,7 +195,7 @@ export function ToolRow({ leadingClassName={css.leading} titleClassName={css.title} chevronClassName={css.chevron} - icon={leadingFor(state, icon)} + icon={icon} title={title} open={open} expandable={expandable} @@ -224,7 +218,11 @@ export function ToolRow({ ) : ( {summaryText} @@ -325,7 +323,7 @@ export function ToolRow({ className={css.inspectButton} onClick={inspect} > - + {t('row.inspect')} )} diff --git a/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts b/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts index 0b6f386623..79e211a028 100644 --- a/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts @@ -17,7 +17,7 @@ export type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-chat/client' /** Tool-call row variants selected by the generic atomic renderer. */ export type ToolRowVariant = 'search' | 'read' | 'bash' | 'write' | 'edit' | 'code' | 'others' -/** Row state semantic; colors self-supplied via StateDot (design gives none). */ +/** Row lifecycle state used by summary styling and accessible status text. */ export type ToolRowState = 'running' | 'ok' | 'error' | 'stopped' /** Locale-neutral structured fact consumed only by the user-facing Tool row. */ diff --git a/packages/client/ui-tool/src/client/tool/toolviews/GenericToolCard.tsx b/packages/client/ui-tool/src/client/tool/toolviews/GenericToolCard.tsx index 4765faeb3d..e79e88b1df 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/GenericToolCard.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/GenericToolCard.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react' import { - IconApiOutline14, IconBrowseOutline16, IconCodeOutline16, IconEditOutline16, IconSearchOutline16, IconSparkle16, + IconApiOutlineRegular, IconBrowseOutlineRegular, IconCodeOutlineRegular, IconEditOutlineRegular, IconSearchOutlineRegular, + IconSparkleRegular, } from '@deepseek-ai/dsh-client-ui-primitives' import type { ToolCallOwnerProps, ToolTreeProps } from '../../contract/slots.ts' import { readCardModel } from '../models/read-card-model.ts' @@ -14,13 +15,13 @@ import { ToolRow } from '../components/ToolRow.tsx' /** Variant leading icons (figma table); all glyphs render at 14 inside the 16px leading box. */ const VARIANT_ICONS: Record = { - search: , - read: , - bash: , - write: , - edit: , - code: , - others: , + search: , + read: , + bash: , + write: , + edit: , + code: , + others: , } /** Card props: the owner payload plus the render site's locale seat (plain prop). */ @@ -39,7 +40,7 @@ export function GenericToolCard({ toolName, block, cwd, home, openFile, inspect, const search = searchCardModel(block) const web = webCardModel(block) // A failing exit status is the terminal card's own error signal (the call - // itself settles isError:false), surfaced as the row's red state dot. + // itself settles isError:false), surfaced through the row's error summary. const state = model.state === 'ok' && terminal !== null && terminalFailed(terminal) ? 'error' : model.state diff --git a/packages/client/ui-tool/src/client/tool/toolviews/ask-question-row.tsx b/packages/client/ui-tool/src/client/tool/toolviews/ask-question-row.tsx index 2a9178eb12..4f1ab77cdb 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/ask-question-row.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/ask-question-row.tsx @@ -1,4 +1,4 @@ -import { IconQuestionOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconQuestionOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { Context } from '@deepseek-ai/cordis' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' @@ -183,7 +183,7 @@ export function AskQuestionRow({ toolName, block, inspect, t }: AskQuestionRowPr t={t} variant={model.variant} toolName={toolName} - icon={} + icon={} title={t('ask.rowTitle')} summary={summary} bodyRaw={transcript === null ? model.bodyRaw : null} diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css index ab93cbbaeb..ab8acd692c 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css +++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css @@ -129,8 +129,7 @@ } /* Same glyph-scaling rule as DisclosureRow's leading: the CSS edge overrides - the svg attributes so the 14px glyph follows the font-size delta; StateDot - (data-state) keeps its fixed figma size. + the svg attributes so the 14px business glyph follows the font-size delta. TODO: this bash header duplicates the DisclosureRow structure (and this is the fourth copy of the glyph rule, with DisclosureRow, MessageItem .compactionLeading, and MessageIconActions); migrating the bash row onto @@ -200,6 +199,10 @@ color: var(--dsw-alias-state-error-primary); } +.stoppedSummary { + color: var(--dsw-alias-state-warn-label); +} + /* Hover-revealed Inspect pill under the expanded terminal's bottom-left — ToolRow's .bodyWrap/.inspectButton treatment, replicated per the registrant posture: real flow (it reserves its line), revealed by hovering anywhere on diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx index e5380979b9..7b1f8653c5 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx @@ -2,7 +2,7 @@ import { useMemo, useState, type KeyboardEvent } from 'react' import type { Context } from '@deepseek-ai/cordis' import clsx from 'clsx' import { - IconApiOutline14, IconChevronDownOutline14, IconInspectOutline12, StateDot, TerminalBlock, + IconApiOutlineRegular, IconChevronDownOutlineRegular, IconInspectOutlineRegular, TerminalBlock, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' @@ -20,21 +20,11 @@ import css from './bash-sample.module.css' type BashRowProps = ToolCallViewProps & PropsLocale<'conversation'> -function leadingFor(state: ToolRowState) { - switch (state) { - case 'error': return - case 'stopped': return - // Running keeps the icon — the row sweep carries the in-flight signal. - default: return - } -} - -/** Visually hidden status — StateDot is aria-hidden; AT needs a text label. */ +/** Visually hidden status for the color-only running sweep and error tone. */ function stateStatus(state: ToolRowState, t: BashRowProps['t']): string | null { switch (state) { case 'running': return t('bash.running') case 'error': return t('bash.failed') - case 'stopped': return t('bash.stopped') default: return null } } @@ -48,7 +38,7 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }: const terminalModel = terminalCardModel(block, cwd) const terminal = terminalModel === null ? null : localizeTerminalCardModel(terminalModel, t) // A failing exit status is the terminal card's own error signal (the call - // itself settles isError:false), surfaced as the row's red state dot. + // itself settles isError:false), surfaced through the row's error summary. const state = model.state === 'ok' && terminalModel !== null && terminalFailed(terminalModel) ? 'error' : model.state @@ -67,7 +57,10 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }: : null, [genericBody, model.bodyRaw, model.variant, open], ) - const failureLine = model.state === 'error' ? model.errorSummary : null + const normalSummary = terminal?.description ?? model.summary + const settlementLine = state === 'error' + ? model.errorSummary ?? normalSummary + : state === 'stopped' ? t('bash.stopped') : null const toggleExpand = () => { setExpanded(v => !v) } @@ -76,16 +69,17 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }: event.preventDefault() toggleExpand() } + const businessIcon = const leading = open - ? + ? : expandable ? ( <> - {leadingFor(state)} - + {businessIcon} + ) - : leadingFor(state) + : businessIcon return (
    {status}} {t(model.titleKey)} - - {failureLine ?? terminal?.description ?? model.summary} + + {settlementLine ?? normalSummary}
    {open && ( @@ -142,7 +140,7 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }: )} {inspect !== undefined && ( )} diff --git a/packages/client/ui-tool/src/client/tool/toolviews/file-mutation-row.tsx b/packages/client/ui-tool/src/client/tool/toolviews/file-mutation-row.tsx index dafc8c0697..480fdc5efe 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/file-mutation-row.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/file-mutation-row.tsx @@ -1,5 +1,5 @@ import type { Context } from '@deepseek-ai/cordis' -import { IconEditOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconEditOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' import { diffCardModel } from '../models/diff-card-model.ts' @@ -20,7 +20,7 @@ export function FileMutationRow({ toolName, block, cwd, home, openFile, inspect, t={t} variant={model.variant} toolName={toolName} - icon={} + icon={} title={t(model.titleKey)} summary={model.summary} output={model.output} diff --git a/packages/client/ui-tool/src/client/tool/toolviews/read-family-row.tsx b/packages/client/ui-tool/src/client/tool/toolviews/read-family-row.tsx index 40553313d2..efb1847346 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/read-family-row.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/read-family-row.tsx @@ -7,7 +7,7 @@ // row assembly lives here once instead of being copied per tool. import type { ReactNode } from 'react' -import { IconBrowseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconBrowseOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' import { toolRowModel } from '../models/tool-call-model.ts' @@ -44,7 +44,7 @@ export function readFamilyRow( t={t} variant={model.variant} toolName={toolName} - icon={} + icon={} title={t(model.titleKey)} summary={model.summary} bodyRaw={null} diff --git a/packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx b/packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx index d3ce18e976..ccb32c8ad4 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx @@ -1,5 +1,5 @@ import type { Context } from '@deepseek-ai/cordis' -import { IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconSearchOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' import { searchCardModel } from '../models/search-card-model.ts' @@ -23,7 +23,7 @@ export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) { t={t} variant={model.variant} toolName={toolName} - icon={} + icon={} title={t(toolName === 'grep' ? SEARCH_TITLE_KEYS.grep : toolName === 'glob' ? SEARCH_TITLE_KEYS.glob : model.titleKey)} diff --git a/packages/client/ui-tool/src/client/tool/toolviews/todo-row.tsx b/packages/client/ui-tool/src/client/tool/toolviews/todo-row.tsx index 7493066722..26984ab937 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/todo-row.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/todo-row.tsx @@ -1,4 +1,4 @@ -import { IconChecklistOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconChecklistOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { Context } from '@deepseek-ai/cordis' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' @@ -54,7 +54,7 @@ export function TodoRow({ toolName, block, inspect, t }: TodoRowProps) { t={t} variant={model.variant} toolName={toolName} - icon={} + icon={} title={t('todo.rowTitle')} summary={summary.text} summarySuffix={summary.extra > 0 ? `+${summary.extra}` : null} diff --git a/packages/client/ui-tool/src/client/tool/toolviews/web-row.tsx b/packages/client/ui-tool/src/client/tool/toolviews/web-row.tsx index 6a739b7540..e51ae05695 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/web-row.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/web-row.tsx @@ -1,5 +1,5 @@ import type { Context } from '@deepseek-ai/cordis' -import { IconBrowseOutline16, IconGlobeOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconBrowseOutlineRegular, IconGlobeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' import { webCardModel } from '../models/web-card-model.ts' @@ -18,7 +18,7 @@ const WEB_TITLE_KEYS = { export function WebRow({ toolName, block, inspect, t }: WebRowProps) { const model = toolRowModel(toolName, block) const web = webCardModel(block) - const icon = toolName === 'web_fetch' ? : + const icon = toolName === 'web_fetch' ? : return ( { const view = render() expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull() + expect(view.container.querySelector('[data-state="stopped"] svg')).not.toBeNull() expect(screen.queryByText('已取消')).toBeNull() expect(screen.getByText(`ask_user_question · ${ARGS}`)).toBeTruthy() }) @@ -218,6 +219,7 @@ describe('AskQuestionRow', () => { it('other tool errors keep the generic summary with the error state', () => { const view = render() expect(view.container.querySelector('[data-state="error"]')).not.toBeNull() + expect(view.container.querySelector('[data-state="error"] svg')).not.toBeNull() expect(screen.getByText(`ask_user_question · ${ARGS}`)).toBeTruthy() }) diff --git a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx index 4b48f88c0b..738fb2c02b 100644 --- a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx +++ b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx @@ -1,3 +1,4 @@ +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import { toolSessionEvents } from './tool-fixtures.client.ts' // @vitest-environment jsdom /** Tool assembly acceptance through the real ui-conversation host. */ @@ -76,7 +77,7 @@ async function bench(nodes: ToolResultNode[]) { openWorkspacePath: vi.fn(async () => ({ ok: true, value: { opened: true } })), }, }) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) }, bind: () => stubSettingsScope().scope } as never) runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { diff --git a/packages/client/ui-tool/tests/chat-ptc-subcalls.client.spec.tsx b/packages/client/ui-tool/tests/chat-ptc-subcalls.client.spec.tsx index 8c888dd667..54212fa981 100644 --- a/packages/client/ui-tool/tests/chat-ptc-subcalls.client.spec.tsx +++ b/packages/client/ui-tool/tests/chat-ptc-subcalls.client.spec.tsx @@ -80,6 +80,7 @@ function snapshotWith( /** Test-owned AppFrame role: declares and renders the Chat view list. */ type AppRootProps = PropsRenderSlots<'conversation.view'> const VIEW_OWNER: ConvViewOwnerProps = { + inspectCall: undefined, viewRequest: null, openView: () => {}, completeViewRequest: () => {}, @@ -103,7 +104,7 @@ async function bench(snapshot: ChatSnapshot) { const chat = createSnapshotStore(snapshot) const events = new ConversationEventRegistry(ctx) const views = new ConversationViewRegistry(ctx) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) }, bind: () => stubSettingsScope().scope } as never) ctx.provide('uiConversation', { events, views, @@ -206,7 +207,7 @@ describe('run_code sub-calls through the real chat machinery', () => { expect(pre!.querySelectorAll('span[style]').length).toBeGreaterThan(3) }) - it('an isError sub-call renders the error state dot exactly like a failed native row', async () => { + it('an isError sub-call renders the same error row state as a failed native row', async () => { const parent = 'call-64' const subCalls = [ subCall(11, parent, 1, 'mystery', { n: 1 }, 'Error: boom', true), diff --git a/packages/client/ui-tool/tests/coverage-tails.client.spec.tsx b/packages/client/ui-tool/tests/coverage-tails.client.spec.tsx index 7c2a3bf265..2c0fe097a2 100644 --- a/packages/client/ui-tool/tests/coverage-tails.client.spec.tsx +++ b/packages/client/ui-tool/tests/coverage-tails.client.spec.tsx @@ -41,12 +41,14 @@ function bashProps(block: RunningToolCall | ToolResultNode): BashRowProps { } describe('Tool presentation tails', () => { - it('ToolRow stopped state renders the warning dot in the leading slot', () => { + it('ToolRow stopped state retains the business icon and shows a warning summary', () => { const view = render( - } title="Bash" summary="s" state="stopped" />, + } + title="Bash" summary="s" state="stopped" />, ) - expect(view.queryByTestId('icon')).toBeNull() - expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull() + expect(view.queryByTestId('icon')).not.toBeNull() + const summary = view.getByText('s') + expect(summary.className).toContain('stoppedSummary') }) it('a settled others-variant row renders the sparkle icon in the leading slot', () => { @@ -79,7 +81,7 @@ describe('Tool presentation tails', () => { expect(row.getAttribute('data-clickable')).toBeNull() }) - it('BashRow carries data-state for running and StateDots for error/stopped', () => { + it('BashRow retains its business icon for failed and stopped states', () => { const running: RunningToolCall = { callId: 'c1', name: 'bash', argsRaw: '{"command":"ls","description":"List"}', turn: 1, step: 1, time: 1_000, subCalls: [], @@ -104,11 +106,15 @@ describe('Tool presentation tails', () => { const errorView = render() expect(errorView.container.querySelector('[data-sample="bash"]')).not.toBeNull() expect(errorView.container.querySelector('[data-state="error"]')).not.toBeNull() - expect(errorView.getByText('失败')).toBeTruthy() + expect(errorView.container.querySelector('[data-state="error"] svg')).not.toBeNull() + expect(errorView.getByText('Bash')).toBeTruthy() + expect(errorView.container.querySelector('[class*="_errorSummary_"]')).not.toBeNull() errorView.unmount() const stoppedView = render() expect(stoppedView.container.querySelector('[data-state="stopped"]')).not.toBeNull() - expect(stoppedView.getByText('已停止')).toBeTruthy() + expect(stoppedView.container.querySelector('[data-state="stopped"] svg')).not.toBeNull() + const stoppedSummary = stoppedView.getByText('已停止') + expect(stoppedSummary.className).toContain('stoppedSummary') }) }) diff --git a/packages/client/ui-tool/tests/diff-card.client.spec.tsx b/packages/client/ui-tool/tests/diff-card.client.spec.tsx index 9f5fabeede..2c50e9f6bb 100644 --- a/packages/client/ui-tool/tests/diff-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/diff-card.client.spec.tsx @@ -256,6 +256,7 @@ describe('FileMutationRow diff card', () => { cleanup() const errorView = render() expect(errorView.container.querySelector('[data-state="error"]')).not.toBeNull() + expect(errorView.container.querySelector('[data-state="error"] svg')).not.toBeNull() }) it('a mutation result with no metadata renders the summary row alone', () => { @@ -299,9 +300,9 @@ describe('FileMutationRow diff card', () => { error: { name: 'ToolError', code: 'interrupted' }, }))} />) expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull() - // The amber StateDot is aria-hidden, so ToolRow carries the state to AT as - // visually-hidden text; without it a stopped row is a colour-only signal. - expect(view.getByText('已停止')).toBeTruthy() + expect(view.container.querySelector('[data-state="stopped"] svg')).not.toBeNull() + expect(view.getByText('已停止').className).toContain('visuallyHidden') + expect(view.container.querySelector('[class*="_stoppedSummary_"]')?.textContent).toBe('notes/demo.txt') }) it('renders a plain summary span when the call carries no file path', () => { diff --git a/packages/client/ui-tool/tests/read-card.client.spec.tsx b/packages/client/ui-tool/tests/read-card.client.spec.tsx index 91d25b8740..a268273e8e 100644 --- a/packages/client/ui-tool/tests/read-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/read-card.client.spec.tsx @@ -261,6 +261,7 @@ describe('ReadRow keyed toolview', () => { content: [{ type: 'text', text: 'ENOENT' }], }))} />) expect(view.container.querySelector('[data-variant="read"]')?.getAttribute('data-state')).toBe('error') + expect(view.container.querySelector('[data-state="error"] svg')).not.toBeNull() expect(view.container.querySelector('[data-read]')).toBeNull() }) @@ -269,6 +270,7 @@ describe('ReadRow keyed toolview', () => { isError: true, error: { name: 'ToolError', code: 'interrupted' }, }))} />) expect(view.container.querySelector('[data-variant="read"]')?.getAttribute('data-state')).toBe('stopped') + expect(view.container.querySelector('[data-state="stopped"] svg')).not.toBeNull() }) it('registers under the read key of the keyed toolview slot', () => { diff --git a/packages/client/ui-tool/tests/search-card.client.spec.tsx b/packages/client/ui-tool/tests/search-card.client.spec.tsx index b189b6f6a8..ca8e8bde20 100644 --- a/packages/client/ui-tool/tests/search-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/search-card.client.spec.tsx @@ -268,6 +268,7 @@ describe('SearchRow keyed card', () => { isError: true, }), 'grep')} />) expect(errorView.container.querySelector('[data-variant="search"]')?.getAttribute('data-state')).toBe('error') + expect(errorView.container.querySelector('[data-variant="search"] svg')).not.toBeNull() }) it('surfaces the result text through the Output section when an errored search has no card', () => { diff --git a/packages/client/ui-tool/tests/terminal-card.client.spec.tsx b/packages/client/ui-tool/tests/terminal-card.client.spec.tsx index 1df6ac1d3e..cb95b6fcf6 100644 --- a/packages/client/ui-tool/tests/terminal-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/terminal-card.client.spec.tsx @@ -429,9 +429,8 @@ describe('BashRow terminal card', () => { expect(view.getByText('List files')).toBeTruthy() }) - // The row's leading StateDot and the card's run-state dot describe the same - // command, so a running row whose card claimed 'done' would be a contradiction - // the reader sees on one line. + // The row's running state and the card's run-state dot describe the same + // command, so a running row whose card claimed 'done' would contradict itself. it('agrees with the summary row about the run state', () => { const runningView = render() expect(runningView.container.querySelector('[data-variant="bash"]')?.getAttribute('data-state')).toBe('running') @@ -449,6 +448,7 @@ describe('BashRow terminal card', () => { content: [{ type: 'text', text: 'boom\n[exit code: 2]' }], }))} />) expect(view.container.querySelector('[data-variant="bash"]')?.getAttribute('data-state')).toBe('error') + expect(view.container.querySelector('[class*="_errorSummary_"]')?.textContent).toBe('List files') }) it('shows the call description as the terminal summary', () => { diff --git a/packages/client/ui-tool/tests/todo-row.client.spec.tsx b/packages/client/ui-tool/tests/todo-row.client.spec.tsx index 0012e65263..819e1aee2b 100644 --- a/packages/client/ui-tool/tests/todo-row.client.spec.tsx +++ b/packages/client/ui-tool/tests/todo-row.client.spec.tsx @@ -118,6 +118,7 @@ describe('TodoRow', () => { it('falls back to the generic summary on malformed args and marks the error state', () => { const view = render() expect(view.container.querySelector('[data-state="error"]')).not.toBeNull() + expect(view.container.querySelector('[data-state="error"] svg')).not.toBeNull() expect(screen.getByText('todo_write · not json')).toBeTruthy() }) diff --git a/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx b/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx index 093295b407..f8215ed61c 100644 --- a/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx @@ -93,6 +93,12 @@ describe('ToolCallTree', () => { ]) }) + it('omits Inspect from tool owners when the target view is unavailable', () => { + const owners: ToolCallOwnerProps[] = [] + render() + expect(owners[0]?.inspect).toBeUndefined() + }) + it('dispatches a running call by its wire name and forwards inspect', () => { const owners: ToolCallOwnerProps[] = [] const block: ToolCallBlock = { diff --git a/packages/client/ui-tool/tests/tool-row.client.spec.tsx b/packages/client/ui-tool/tests/tool-row.client.spec.tsx index 9a616aaecc..6ef6cd713a 100644 --- a/packages/client/ui-tool/tests/tool-row.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-row.client.spec.tsx @@ -340,14 +340,12 @@ describe('ToolRow', () => { expect(view.queryByText(/"a": 1/)).toBeNull() }) - it('running keeps the icon (row sweep carries the signal); error swaps in a StateDot', () => { + it('keeps the business icon across running and error states', () => { const runningView = render() expect(runningView.queryByTestId('tool-icon')).not.toBeNull() expect(runningView.container.querySelector('[data-state="running"]')).not.toBeNull() const errorView = render() - expect(errorView.container.querySelector('[data-testid="tool-icon"]')).toBeNull() - // The dot rides the idle slot, so an expandable error row keeps the - // icon→chevron hover preview instead of losing it with the icon. + expect(errorView.container.querySelector('[data-testid="tool-icon"]')).not.toBeNull() expect(errorView.container.querySelector('[class*="chevronHover"]')).not.toBeNull() }) @@ -421,7 +419,8 @@ describe('ToolRow', () => { it('an error row without an error summary keeps the args summary', () => { const view = render() - expect(view.getByText('List files')).toBeTruthy() + const summary = view.getByText('List files') + expect(summary.className).toContain('errorSummary') }) it('renders summarySuffix outside the ellipsized summary span, and drops it on a failure line', () => { @@ -505,6 +504,20 @@ describe('GenericToolCard', () => { expect(view.container.querySelector('[data-variant="bash"]')).not.toBeNull() }) + it.each([ + 'bash', 'read', 'grep', 'write', 'run_code', 'unknown_tool', + ] as const)('keeps the %s business family artwork on failure', (toolName) => { + const failed = result({ + call: { name: toolName, argsRaw: '{}' }, + content: [{ type: 'text', text: 'failed' }], + isError: true, + }) + const view = render() + const root = view.container.querySelector(`[data-tool="${toolName}"]`)! + expect(root.querySelector('[data-disclosure-row] > :first-child svg')).not.toBeNull() + expect(root.querySelector('[data-state]')).toBeNull() + }) + it('unknown tools land on the others variant titled Tool call', () => { const view = render( , diff --git a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx index c13ac0965a..8a854cd8fe 100644 --- a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx +++ b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx @@ -1,5 +1,6 @@ // @vitest-environment jsdom +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent } from '@testing-library/react' import type { ISession } from '@deepseek-ai/dsh-api-session-controller/client' @@ -67,7 +68,7 @@ async function bench(nodes: ToolResultNode[]) { const runtime = await SlotTestRuntime.create() const openWorkspacePath = vi.fn(async () => ({ ok: true, value: { opened: true } })) runtime.remote.provideNamespaces({ session: { openWorkspacePath } }) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) }, bind: () => stubSettingsScope().scope } as never) const layout = { openDetails: vi.fn(), closeDetails: vi.fn() } runtime.ctx.provide('layout', layout) const sidebarRight = { openResource: vi.fn<(address: string) => void>() } @@ -288,7 +289,7 @@ describe('registrant declaration injection', () => { openWorkspacePath: vi.fn(async () => ({ ok: true, value: { opened: true } })), }, }) - runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + runtime.ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) }, bind: () => stubSettingsScope().scope } as never) runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { diff --git a/packages/client/ui-tool/tests/web-card.client.spec.tsx b/packages/client/ui-tool/tests/web-card.client.spec.tsx index 28f5c8c2d3..96f66d8cbd 100644 --- a/packages/client/ui-tool/tests/web-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/web-card.client.spec.tsx @@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render } from '@testing-library/react' import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-ui-chat/client' import type { ToolCallOwnerProps } from '@deepseek-ai/dsh-client-ui-tool/client' -import { IconGlobeOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconGlobeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import { webCardModel } from '../src/client/tool/models/web-card-model.ts' import { GenericToolCard } from '../src/client/tool/toolviews/GenericToolCard.tsx' import { WebRow, webToolview } from '../src/client/tool/toolviews/web-row.tsx' @@ -138,7 +138,7 @@ describe('chat row web body', () => { } it('the WebRow collapses to the summary row, expanding to the full search card', () => { - const globe = render().container.querySelector('svg')!.outerHTML + const globe = render().container.querySelector('svg')!.outerHTML const view = render() // Collapsed: the summary row alone, no card in the DOM. expect(view.getByText('网页搜索')).toBeTruthy() @@ -181,6 +181,7 @@ describe('chat row web body', () => { expect(view.container.querySelector('[data-web]')).toBeNull() // The row reflects the error state so the summary line still reads as failed. expect(view.container.querySelector('[data-state="error"]')).not.toBeNull() + expect(view.container.querySelector('[data-state="error"] svg')).not.toBeNull() }) it('the GenericToolCard fallback does not promote an unknown tool from metadata alone', () => { diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml index 8ab91c8e9f..e865300cf5 100644 --- a/packages/client/ui-trajectory/README.i18n.yaml +++ b/packages/client/ui-trajectory/README.i18n.yaml @@ -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 packages/client/ui-trajectory/README.md -README.md: b58466f0c38efef89ff3aa2b5d6da49b6c85ac86 -README.zh.md: ab9ecbfd26478c30c9078a25f92bd121245b7bf9 +README.md: 35135c726d59669ee6368d33ba35882897df7b15 +README.zh.md: ddde852f56924599b46a7f813619dcfc7d099714 diff --git a/packages/client/ui-trajectory/README.md b/packages/client/ui-trajectory/README.md index b58466f0c3..35135c726d 100644 --- a/packages/client/ui-trajectory/README.md +++ b/packages/client/ui-trajectory/README.md @@ -25,7 +25,7 @@ The Trajectory tab lets you inspect agent activity as a turn-aware ledger and in ## Use this package -Open the Trajectory tab in the conversation's view ring to inspect agent activity as an event ledger and timeline. The ledger covers records with an explicit loading row until the initial tail is positioned; while an older prefix remains unloaded, a first-row control loads one earlier page on click and shows a disabled loading status while that page is pending. +Open the Trajectory tab in the conversation's view ring to inspect agent activity as an event ledger and timeline. The ledger covers records with an explicit loading row until the initial tail is positioned; while an older prefix remains unloaded, a first-row control loads one earlier page on click and shows the shared ongoing loader while that page is pending. ### Inspecting records diff --git a/packages/client/ui-trajectory/README.zh.md b/packages/client/ui-trajectory/README.zh.md index ab9ecbfd26..ddde852f56 100644 --- a/packages/client/ui-trajectory/README.zh.md +++ b/packages/client/ui-trajectory/README.zh.md @@ -25,7 +25,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间 ## 使用本包 -在对话视图环中打开 Trajectory 标签页,把 agent 活动作为事件记录表与时间线查看。初始尾部完成定位前,记录表会用明确的加载行遮住真实记录;更早的前缀仍未加载时,首行控件会在点击时加载一页更早的历史,并在该页加载期间显示禁用的加载状态。 +在对话视图环中打开 Trajectory 标签页,把 agent 活动作为事件记录表与时间线查看。初始尾部完成定位前,记录表会用明确的加载行遮住真实记录;更早的前缀仍未加载时,首行控件会在点击时加载一页更早的历史,并在该页加载期间显示共享 ongoing loading。 ### 检查记录 diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css index 520b71c4da..b07fbe48a2 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.module.css @@ -45,17 +45,6 @@ font: var(--dsw-font-xxs-12); } -.historyLoadingSpinner { - width: 10px; - height: 10px; - box-sizing: border-box; - border: 1.5px solid var(--dsw-alias-border-l2); - border-top-color: var(--dsw-alias-state-business-primary); - border-radius: 50%; - corner-shape: round; - animation: history-loading-spin 700ms linear infinite; -} - .table tbody .historyLoadRow td { height: 30px; padding: 0; @@ -107,16 +96,6 @@ visibility: hidden; } -@keyframes history-loading-spin { - to { transform: rotate(360deg); } -} - -@media (prefers-reduced-motion: reduce) { - .historyLoadingSpinner { - animation: none; - } -} - .table { --trajectory-turn-accent: color-mix( in srgb, diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index 630d0bebfe..fb765dca97 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -8,16 +8,17 @@ import { FileTypeIcon, fileExtension, fileSizeText, - IconCheckOutline16, - IconChevronRightOutline14, - IconCodeOutline16, - IconWrapLinesOutline16, - IconCopyOutline16, - IconSettingsOutline16, - IconSparkle16, - IconUserOutline16, + IconCheckOutlineRegular, + IconChevronRightOutlineRegular, + IconCodeOutlineRegular, + IconWrapLinesOutlineRegular, + IconCopyOutlineRegular, + IconSettingsOutlineRegular, + IconSparkleRegular, + IconUserOutlineRegular, JsonTree, MarkdownText, + StateDot, Tooltip, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives' @@ -120,11 +121,11 @@ function CompactedIcon(): ReactNode { } const KIND_ICON: Record = { - system: , - user: , + system: , + user: , context: , compacted: , - message: , + message: , tool: , subtool: , } @@ -1116,7 +1117,7 @@ function RecordListText({ return ( <> - {toolCallText.program && } + {toolCallText.program && } {toolCallText.name || '—'} {toolCallText.args !== undefined && ( @@ -1199,7 +1200,7 @@ function SourceBlocks({ {t('block.label', { index: index + 1, type: block.type })} - + ) : ( @@ -1371,7 +1372,7 @@ function ToolCatalog({ {tools.map((tool, index) => (
    - + {tool.name} {tool.description} @@ -1562,7 +1563,7 @@ function MarkdownRecordContent({ onClick={() => { onThinkingExpandedChange(!thinkingExpanded) }} > {t('record.thinking')} - + {thinkingExpanded && ( { void writeClipboard(text).then((ok) => { setState(ok ? 'copied' : 'failed') }) }} > - {state === 'copied' ? : } + {state === 'copied' ? : } ) } @@ -1922,7 +1923,7 @@ function ProgramInput({ program, initialWrapped, stringWrapping, onOpen, t }: { stringWrapping?.setDefault(next) }} > - + )} {onOpen === undefined && ( @@ -2044,7 +2045,7 @@ function OverviewSection({ onClick={onOpen} > {label} - + {actions} @@ -2603,7 +2604,7 @@ export function TrajectoryTable({ {showInitialLoading && (
    -
    @@ -2638,7 +2639,7 @@ export function TrajectoryTable({ }} > {olderBusy && ( -
    - + = { target: 'trajectory', + toolCallFocus: callId => callId, create: () => new TrajectorySnapshotBuilder(), } diff --git a/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts b/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts index e1ca41bfb2..5f2fbe8569 100644 --- a/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts +++ b/packages/client/ui-trajectory/tests/client-bundle.client.spec.ts @@ -6,6 +6,7 @@ * registers the view tab into a real SlotRegistry ring. Skips when dist/ is * not built (`pnpm --filter @deepseek-ai/dsh-client-ui-trajectory bundle`). */ +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { Context } from '@deepseek-ai/cordis' @@ -89,7 +90,7 @@ describe('tsdown client artifact', () => { const { events, views } = uiConversation ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) ctx.provide('remote', { $on: () => () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) }, bind: () => stubSettingsScope().scope } as never) const locale = await import('@deepseek-ai/dsh-client-locale/client') ctx.plugin({ inject: [...locale.inject], apply: locale.apply }) const fiber = ctx.plugin(exports as { apply: (ctx: Context) => void }) diff --git a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts index 1f15cc202a..0a4db8aeb9 100644 --- a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts +++ b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts @@ -172,6 +172,10 @@ function systemMessage(text: string) { } describe('Trajectory conversation Definitions', () => { + it('addresses tool-call inspection with the persisted call identity', () => { + expect(trajectoryViewDefinition.toolCallFocus?.('call-1')).toBe('call-1') + }) + it('assembles streaming usage, preserves retry facts, and materializes interruption', () => { const value = assembler([ at(1, 'turn/start', { turn: 1 }), diff --git a/packages/client/ui-trajectory/tests/table.client.spec.tsx b/packages/client/ui-trajectory/tests/table.client.spec.tsx index fd9042729a..7d054adc9b 100644 --- a/packages/client/ui-trajectory/tests/table.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/table.client.spec.tsx @@ -662,6 +662,7 @@ describe('TrajectoryTable', () => { expect(onLoadOlder).toHaveBeenCalledOnce() expect(loadButton.hasAttribute('disabled')).toBe(true) expect(screen.getByRole('status').textContent).toBe('Loading earlier history…') + expect(screen.getByRole('button', { name: 'Loading earlier history…' }).querySelector('[data-state="ongoing"]')).not.toBeNull() resolveOlder?.(false) await waitFor(() => { @@ -698,6 +699,7 @@ describe('TrajectoryTable', () => { ) expect(screen.getByRole('status').textContent).toContain('Loading trajectory…') + expect(screen.getByRole('status').querySelector('[data-state="ongoing"]')).not.toBeNull() expect(screen.getByRole('table').getAttribute('data-scroll-ready')).toBeNull() view.rerender() diff --git a/packages/client/ui-trajectory/tests/views.client.spec.tsx b/packages/client/ui-trajectory/tests/views.client.spec.tsx index e1707c6aee..660445959e 100644 --- a/packages/client/ui-trajectory/tests/views.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.client.spec.tsx @@ -234,6 +234,7 @@ function standaloneProps( useInput: bindSnapshotSelector(input), inputActions, useProjection, + inspectCall: undefined, viewRequest: null, openView: () => {}, completeViewRequest: () => {}, @@ -290,7 +291,7 @@ async function bench(snapshot = historySnapshot(NODES)) { // The locale plugin backs the locale-aware view tab label ('locale' in // inject); its settings scope needs a connection handle. ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) }, bind: () => stubSettingsScope().scope } as never) await runtime.mount({ inject: [...localeInject], apply: localeApply }) const provide = vi.spyOn(ctx.uiSession, 'provide') const feature = await runtime.mount({ inject: [...inject], apply }) @@ -308,10 +309,10 @@ function tabsOf(slots: SlotRegistry): ViewTab[] { .map(e => ({ id: e.options.id!, label: resolveSlotLabel(e.options.label) ?? e.options.id! })) } -type ConvViewOwner = Pick +type ConvViewOwner = Pick function isConvViewOwner(owner: object): owner is ConvViewOwner { - return 'viewRequest' in owner + return 'inspectCall' in owner && 'viewRequest' in owner && 'openView' in owner && typeof owner.openView === 'function' && 'completeViewRequest' in owner && typeof owner.completeViewRequest === 'function' } @@ -411,6 +412,7 @@ function mount(fixture: Awaited>) { renderSlot={renderSlot} bindDraftMirror={() => () => {}} openView={conversation.actions.openView} + useInspectCall={selector => selector(undefined)} /> , ) diff --git a/packages/client/ui-user-questions/README.i18n.yaml b/packages/client/ui-user-questions/README.i18n.yaml index ae9408ee1d..626839020e 100644 --- a/packages/client/ui-user-questions/README.i18n.yaml +++ b/packages/client/ui-user-questions/README.i18n.yaml @@ -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 packages/client/ui-user-questions/README.md -README.md: abbf855706b478857540b12712c5999082b0ab22 -README.zh.md: 995035cbb08ff6c64e2b91b61ab49b2bd473b9b7 +README.md: 929494485b5ca1c0afb6fa3d24f59627136cc915 +README.zh.md: 0269998b744e020bd9d58b8f919626297cd94f7f diff --git a/packages/client/ui-user-questions/README.md b/packages/client/ui-user-questions/README.md index abbf855706..929494485b 100644 --- a/packages/client/ui-user-questions/README.md +++ b/packages/client/ui-user-questions/README.md @@ -33,7 +33,7 @@ A multi-select draft keeps its selected labels while the user opens or edits the ### The plan-review card -A `plan-review` intent — set by `dsh-plan-mode` on the `exit_plan_mode` review — renders a compact approval card: a `Plan review` strip with a `View full plan` link, the plan title and a two-line plain-text summary, and `Request changes` / `Approve` actions. The complete plan opens in the sidebar through the link or its permanent Chat card. Approve answers with the asker's approval label; `Request changes` rejects the wait as `ASK_CANCELLED`, returning the composer for the user's feedback without submitting an approval. The card has no separate refusal button. +A `plan-review` intent — set by `dsh-plan-mode` on the `exit_plan_mode` review — renders a compact approval card: a `Plan review` strip with a `View full plan` link, the plan title and a two-line plain-text summary, and `Request changes` / `Approve` actions. The strip uses the shared warning dot and changes it to the ongoing loader while either action settles. The complete plan opens in the sidebar through the link or its permanent Chat card. Approve answers with the asker's approval label; `Request changes` rejects the wait as `ASK_CANCELLED`, returning the composer for the user's feedback without submitting an approval. The card has no separate refusal button. ### Failure and recovery diff --git a/packages/client/ui-user-questions/README.zh.md b/packages/client/ui-user-questions/README.zh.md index 995035cbb0..0269998b74 100644 --- a/packages/client/ui-user-questions/README.zh.md +++ b/packages/client/ui-user-questions/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" ### plan-review 卡片 -`plan-review` 意图——由 `dsh-plan-mode` 在 `exit_plan_mode` 审阅上设置——渲染紧凑审批卡片:顶部为带“查看全文”链接的“计划待审”条带,正文显示计划标题和两行纯文本摘要,底部提供“要求修改”和“同意执行”。通过链接或聊天历史中的常驻卡片可在侧边栏阅读完整计划。“同意执行”用提问方的批准标签回答;“要求修改”以 `ASK_CANCELLED` 拒绝等待,让编辑器归位供用户输入反馈,不会提交批准。卡片不再单列拒绝按钮。 +`plan-review` 意图——由 `dsh-plan-mode` 在 `exit_plan_mode` 审阅上设置——渲染紧凑审批卡片:顶部为带“查看全文”链接的“计划待审”条带,正文显示计划标题和两行纯文本摘要,底部提供“要求修改”和“同意执行”。条带使用共享 warning 状态点,并在任一操作结算期间切换为 ongoing loading。通过链接或聊天历史中的常驻卡片可在侧边栏阅读完整计划。“同意执行”用提问方的批准标签回答;“要求修改”以 `ASK_CANCELLED` 拒绝等待,让编辑器归位供用户输入反馈,不会提交批准。卡片不单列拒绝按钮。 ### 失败与恢复 diff --git a/packages/client/ui-user-questions/src/client/PlanReviewPanel.module.css b/packages/client/ui-user-questions/src/client/PlanReviewPanel.module.css index cb4c4808e3..ed9a8cae5f 100644 --- a/packages/client/ui-user-questions/src/client/PlanReviewPanel.module.css +++ b/packages/client/ui-user-questions/src/client/PlanReviewPanel.module.css @@ -38,14 +38,6 @@ line-height: 20px; } -.dot { - width: 8px; - height: 8px; - border-radius: 50%; - corner-shape: round; - background: var(--dsw-alias-state-warn-primary); -} - .footer { display: flex; align-items: center; diff --git a/packages/client/ui-user-questions/src/client/PlanReviewPanel.tsx b/packages/client/ui-user-questions/src/client/PlanReviewPanel.tsx index 388a3d4eaa..b0f6946f96 100644 --- a/packages/client/ui-user-questions/src/client/PlanReviewPanel.tsx +++ b/packages/client/ui-user-questions/src/client/PlanReviewPanel.tsx @@ -1,5 +1,7 @@ import { useMemo, useState } from 'react' -import { Button, extractMarkdownPlainText, IconEditOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { + Button, extractMarkdownPlainText, IconEditOutlineRegular, StateDot, +} from '@deepseek-ai/dsh-client-ui-primitives' import type { PendingQuestion, PlanReview, QuestionComposerProps } from './contract/slots.ts' import css from './PlanReviewPanel.module.css' @@ -48,9 +50,9 @@ export function PlanReviewPanel({ pending, review, t, renderSlot }: PlanReviewPa return (
    -
    +
    - + {t('plan.header')}
    {renderSlot('conversation.plan-review.actions', { review, requestKey: pending.key })} @@ -64,7 +66,7 @@ export function PlanReviewPanel({ pending, review, t, renderSlot }: PlanReviewPa
    {error}
    @@ -337,7 +336,7 @@ function QuestionFlow({ pending, t, useStore, actions }: QuestionFlowProps) { {question.multiSelect === true ? ( ) : {optionIndex + 1}} @@ -365,12 +364,12 @@ function QuestionFlow({ pending, t, useStore, actions }: QuestionFlowProps) { className={clsx(css.checkbox, draft.custom !== '' && css.checkboxChecked)} aria-hidden="true" > - {draft.custom !== '' && } + {draft.custom !== '' && } ) : ( )} { replaceProgress(index - 1, drafts); setError(null) }} > - + {index + 1} / {questions.length}
    diff --git a/packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx b/packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx index 094b056780..29770236b8 100644 --- a/packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx +++ b/packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx @@ -239,6 +239,7 @@ describe('PlanReviewPanel', () => { expect(document.querySelector('[data-plan-review-key]')?.getAttribute('data-plan-review-key')).toBe(carrier.key) expect(screen.getByText(zh['plan.header'])).toBeTruthy() + expect(document.querySelector('[data-plan-review-key] [data-state="warning"]')).not.toBeNull() expect(screen.getByRole('heading', { name: 'Ship the picker' })).toBeTruthy() expect(screen.getByText('read the store')).toBeTruthy() expect(screen.getAllByRole('button')).toHaveLength(2) @@ -279,6 +280,8 @@ describe('PlanReviewPanel', () => { expect(approve.getAttribute('title')).toBe('Leave plan mode; the plan is carried out from the next step.') fireEvent.click(approve) expect(answer).toHaveBeenCalledWith(decision('Approve')) + expect(document.querySelector('[data-plan-review-key] [data-state="ongoing"]')).not.toBeNull() + expect(document.querySelector('[data-plan-review-key] section')?.getAttribute('aria-busy')).toBe('true') // One-shot: every action locks until the host's resolved frame lands. expect(approve.hasAttribute('disabled')).toBe(true) expect(screen.getByRole('button', { name: zh['plan.discuss'] }).hasAttribute('disabled')).toBe(true) diff --git a/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx b/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx index 19d62b6af5..8cd4d768e6 100644 --- a/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx +++ b/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx @@ -3,7 +3,7 @@ import { type FocusEvent, type MouseEvent, type ReactNode, } from 'react' import { - DisclosureRow, IconChevronRightOutline14, StateDot, + DisclosureRow, IconChevronRightOutlineRegular, StateDot, type DisclosureRowProps, type StateDotState, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' @@ -212,7 +212,7 @@ function RunHeader({ children, count, name, onToggle, open, status, t }: { }) { return ( } + icon={} title={t('run.title', { name })} open={open} onToggle={onToggle} @@ -305,7 +305,7 @@ function PhaseSection({ onMouseDownCapture={pendingCleanCollapse ? preventPendingHeaderFocus : undefined} > } + icon={} title={readablePhase(phase.phase, t)} open={open} onToggle={onToggle} diff --git a/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx b/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx index 6c999db2b6..84b589c36e 100644 --- a/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx +++ b/packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx @@ -1,4 +1,5 @@ // @vitest-environment jsdom +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots' import { Context } from '@deepseek-ai/cordis' import { cleanup, fireEvent, render, screen } from '@testing-library/react' @@ -884,7 +885,7 @@ describe('plugin lifecycle', () => { await ctx.plugin(SlotRegistry).await() ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) ctx.provide('remote', { $on: () => () => {} } as never) - ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) + ctx.provide('settingsScope', { developerTools: { enabled: createSnapshotStore(true) }, bind: () => stubSettingsScope().scope } as never) const sessions = new TestSessions(async (action) => { await action() }, ctx) ctx.provide('sessions', sessions) const openSession = vi.fn(async () => {}) diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index 0bdecf2b08..db4e1f8684 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -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 packages/client/ui-workspace/README.md -README.md: 3fbbf52fd5fabca3416b7fe566f579f4a74dd3f2 -README.zh.md: 04b0565f8fc17f183fd9c1b1b48a7edbd1ba32ba +README.md: 92c29b2758817c639268d77d504fec3423d0f05b +README.zh.md: f61007aca1e3631f26144a864b6911d5da20646e diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index 3fbbf52fd5..92c29b2758 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -49,11 +49,11 @@ A Session title wider than its row is clipped with an ellipsis at rest. Hovering ### Pending interactions -Session rows render the runtime's live `pendingInteraction` classification: approvals report **Waiting for approval**, plan reviews report **Plan awaiting review**, and ordinary questions report **Waiting for answer**. Every pending interaction uses an amber warning dot that takes precedence over the running indicator. +Session rows render the runtime's live `pendingInteraction` classification: approvals report **Waiting for approval**, plan reviews report **Plan awaiting review**, and ordinary questions report **Waiting for answer**. While an interaction is pending, the row uses the shared warning dot and replaces its trailing update time with **Approval**, **Plan review**, or **Answer**; the full status and relative time remain in hover details. Pending interaction takes precedence over the shared ongoing loader; a finished-but-unviewed Session uses done, while idle remains dot-free in the row and uses the shared idle dot in its hover details. ### Active Schedule markers -Grouped and flat Session rows, plus search results, show an outline alarm when `SessionSummary.projectionValues.schedule` is a non-empty array. The marker sits after the title; an ordinary row keeps its update time after the marker, while a search result has no update time. It is not a button, has no independent pointer action or tab stop, and clicking its area still opens the row. The localized tooltip and matching screen-reader label say **Has active scheduled task**. +Grouped and flat Session rows, plus search results, show an outline alarm when `SessionSummary.projectionValues.schedule` is a non-empty array. The marker sits after the title; an ordinary row keeps its update time or compact pending-interaction label after the marker, while a search result has neither trailing value. It is not a button, has no independent pointer action or tab stop, and clicking its area still opens the row. The localized tooltip and matching screen-reader label say **Has active scheduled task**. The value is intentionally best effort for cold Sessions. An identity-matching usable projection-cache row can prewarm the alarm without opening the Session; a missing or stale cache may briefly omit or retain it. The marker means only that the current list value contains an undispatched or undeleted Schedule record. It does not report whether a Schedule runtime is live or able to wake the Session. @@ -75,7 +75,7 @@ Each registration declares a **directory-flow child hole** (`single` kind: `conv ### View state -Once the Workspace list baseline is ready, browser-persisted expansion and manual Session-order records retain only current Workspace ids plus Ungrouped and the flat-list account. `WorkspaceView.sessionIds` supplies real-Workspace membership, not Session display order. View actions require the current account orders explicitly. Flat-list membership and ordering use Session ids; row rendering adds status indicators once. Entering Manual snapshots every active account from the current display; reconciliation retains saved members that still belong to the account, removes departed members, and appends newly known members by recency. A new membership entry without a Session summary is omitted until that summary arrives, while an already saved slot survives a temporarily missing summary. During Workspace reconnection, Manual records an observed blank Session at the front of its saved flat and known group orders without removing other saved members; full membership reconciliation waits for the Workspace baseline. This reconciliation remains mounted while the sidebar is a rail or search replaces its body. Last updated derives directly from each current list snapshot without reading or writing saved positions; equal timestamps use Session ids as a stable tie-break. The shared sidebar projection hides rows whose durable Session summary has `origin: 'subagent'`, and each visible ordinary row inherits the blue activity indicator while any descendant reached through uninterrupted subagent-origin lineage is running. The same pure derivation reads the Schedule key from list projection values for grouped, flat, and search nodes; the package uses only the type-only `@deepseek-ai/dsh-schedule/client` dependency and does not import the Schedule runtime or `ui-schedule`. +Once the Workspace list baseline is ready, browser-persisted expansion and manual Session-order records retain only current Workspace ids plus Ungrouped and the flat-list account. `WorkspaceView.sessionIds` supplies real-Workspace membership, not Session display order. View actions require the current account orders explicitly. Flat-list membership and ordering use Session ids; row rendering adds status indicators once. Entering Manual snapshots every active account from the current display; reconciliation retains saved members that still belong to the account, removes departed members, and appends newly known members by recency. A new membership entry without a Session summary is omitted until that summary arrives, while an already saved slot survives a temporarily missing summary. During Workspace reconnection, Manual records an observed blank Session at the front of its saved flat and known group orders without removing other saved members; full membership reconciliation waits for the Workspace baseline. This reconciliation remains mounted while the sidebar is a rail or search replaces its body. Last updated derives directly from each current list snapshot without reading or writing saved positions; equal timestamps use Session ids as a stable tie-break. The shared sidebar projection hides rows whose durable Session summary has `origin: 'subagent'`, and each visible ordinary row inherits the shared ongoing loader while any descendant reached through uninterrupted subagent-origin lineage is running. The same pure derivation reads the Schedule key from list projection values for grouped, flat, and search nodes; the package uses only the type-only `@deepseek-ai/dsh-schedule/client` dependency and does not import the Schedule runtime or `ui-schedule`. ### Hover cards diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index 04b0565f8f..f61007aca1 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -49,11 +49,11 @@ Session 行内的 Rename 操作打开一个以该行显示标题预填的对话 ### 待处理交互 -Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**等待审批**,计划审阅显示**计划待审**,普通问题显示**等待回答**。每个待处理交互都使用一枚琥珀色警告点,优先级高于运行指示器。 +Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**等待审批**,计划审阅显示**计划待审**,普通问题显示**等待回答**。交互待处理期间,该行使用共享 warning 橙点,并以**待批准**、**计划待审**或**待回答**替换尾部更新时间;悬停详情仍保留完整状态和相对时间。待处理交互的优先级高于共享 ongoing loading;已完成但未查看的 Session 使用 done,idle 在行内不显示点,在悬停详情中使用共享 idle 灰点。 ### 活动 Schedule 标识 -分组与平铺 Session 行以及搜索结果会在 `SessionSummary.projectionValues.schedule` 为非空数组时显示一枚轮廓闹钟。标识位于标题之后;普通行的更新时间仍位于标识之后,搜索结果则没有更新时间。它不是按钮,没有独立 pointer 行为或 Tab stop,点击所在区域仍会打开整行。本地化 tooltip 与文本相同的读屏标签均为**有活动定时任务**。 +分组与平铺 Session 行以及搜索结果会在 `SessionSummary.projectionValues.schedule` 为非空数组时显示一枚轮廓闹钟。标识位于标题之后;普通行的更新时间或紧凑待处理文案仍位于标识之后,搜索结果则不显示尾部信息。它不是按钮,没有独立 pointer 行为或 Tab stop,点击所在区域仍会打开整行。本地化 tooltip 与文本相同的读屏标签均为**有活动定时任务**。 对于 cold Session,该值有意采用尽力而为语义。身份匹配且可用的 projection-cache 行可以在不打开 Session 的情况下预热闹钟;cache 缺失或陈旧可能造成短暂漏显或残留。标识只表示当前列表值包含尚未 dispatch 或 delete 的 Schedule 记录,不表示 Schedule 运行时当前 live 或能够唤醒该 Session。 @@ -75,7 +75,7 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示** ### 视图状态 -Workspace 列表基线就绪后,浏览器持久化的展开状态和手动 Session 顺序记录只保留当前 Workspace id、Ungrouped 和单列表记账。`WorkspaceView.sessionIds` 提供真实 Workspace 的成员关系,而不提供 Session 显示顺序。视图操作要求显式传入当前各记账的顺序。单列表的成员筛选和排序使用 Session id,行渲染只计算一次状态指示。进入手动排序会从当前显示结果一次性记录每个有效记账;对账会保留仍属于该记账的已保存成员、移除已经离开的成员,并按最近更新时间追加新发现的成员。尚无 Session 摘要的新成员会等摘要到达后再加入,而已经保存的位置在摘要暂时缺失时仍会保留。Workspace 重连期间,手动排序会将已观察到的空白 Session 记录到已保存的单列表及已知分组顺序首位,不移除其他已保存成员;完整成员对账等待 Workspace 基线到齐。即使侧边栏收成窄栏或搜索替代列表主体,这项对账也保持挂载。最近更新直接从每份当前列表快照派生,不读取或写入已保存位置;时间相同时按 Session id 稳定排序。共享侧边栏投影会隐藏持久化 Session 摘要中带有 `origin: 'subagent'` 的行;每个可见普通行都会在经不间断的 subagent 谱系可达的任一后代运行时继承蓝色活动指示器。同一项纯派生逻辑还会为分组、平铺与搜索节点读取列表 projection value 中的 Schedule key;本包只使用纯类型依赖 `@deepseek-ai/dsh-schedule/client`,不会导入 Schedule 运行时或 `ui-schedule`。 +Workspace 列表基线就绪后,浏览器持久化的展开状态和手动 Session 顺序记录只保留当前 Workspace id、Ungrouped 和单列表记账。`WorkspaceView.sessionIds` 提供真实 Workspace 的成员关系,而不提供 Session 显示顺序。视图操作要求显式传入当前各记账的顺序。单列表的成员筛选和排序使用 Session id,行渲染只计算一次状态指示。进入手动排序会从当前显示结果一次性记录每个有效记账;对账会保留仍属于该记账的已保存成员、移除已经离开的成员,并按最近更新时间追加新发现的成员。尚无 Session 摘要的新成员会等摘要到达后再加入,而已经保存的位置在摘要暂时缺失时仍会保留。Workspace 重连期间,手动排序会将已观察到的空白 Session 记录到已保存的单列表及已知分组顺序首位,不移除其他已保存成员;完整成员对账等待 Workspace 基线到齐。即使侧边栏收成窄栏或搜索替代列表主体,这项对账也保持挂载。最近更新直接从每份当前列表快照派生,不读取或写入已保存位置;时间相同时按 Session id 稳定排序。共享侧边栏投影会隐藏持久化 Session 摘要中带有 `origin: 'subagent'` 的行;每个可见普通行都会在经不间断的 subagent 谱系可达的任一后代运行时继承共享 ongoing loading。同一项纯派生逻辑还会为分组、平铺与搜索节点读取列表 projection value 中的 Schedule key;本包只使用纯类型依赖 `@deepseek-ai/dsh-schedule/client`,不会导入 Schedule 运行时或 `ui-schedule`。 ### 悬浮卡片 diff --git a/packages/client/ui-workspace/src/client/WorkspacePicker.tsx b/packages/client/ui-workspace/src/client/WorkspacePicker.tsx index b1801cc3d9..b74e3689dc 100644 --- a/packages/client/ui-workspace/src/client/WorkspacePicker.tsx +++ b/packages/client/ui-workspace/src/client/WorkspacePicker.tsx @@ -11,7 +11,7 @@ import type { ReactNode, RefObject } from 'react' import { useCallback, useEffect, useState } from 'react' import { - Button, IconFolderClose16, IconPlusOutline16, Menu, Modal, type MenuEntry, + Button, IconFolderCloseRegular, IconPlusOutlineRegular, Menu, Modal, type MenuEntry, } from '@deepseek-ai/dsh-client-ui-primitives' import type { WorkspaceId, WorkspaceSnapshot, WorkspaceView, @@ -99,7 +99,7 @@ export function WorkspacePickFlow({ if (flowOpen && !flowAvailable) setFlowOpen(false) }, [flowOpen, flowAvailable]) const addEntries: MenuEntry[] = flowAvailable - ? [{ id: ADD_WORKSPACE, label: t('menu.addWorkspace'), icon: , disabled: flowBusy }] + ? [{ id: ADD_WORKSPACE, label: t('menu.addWorkspace'), icon: , disabled: flowBusy }] : [] // With workspaces listed, the add action pins below the scroll region // (divider + always visible); otherwise it IS the menu. @@ -108,7 +108,7 @@ export function WorkspacePickFlow({ ? workspaces.map(workspace => ({ id: workspace.workspaceId, label: workspace.title, - icon: , + icon: , disabled: flowBusy, })) : addEntries diff --git a/packages/client/ui-workspace/src/client/locales.ts b/packages/client/ui-workspace/src/client/locales.ts index 9160d9194a..95c4d184eb 100644 --- a/packages/client/ui-workspace/src/client/locales.ts +++ b/packages/client/ui-workspace/src/client/locales.ts @@ -58,6 +58,9 @@ export const zh = { 'status.waitingApproval': '等待审批', 'status.planReview': '计划待审', 'status.waitingAnswer': '等待回答', + 'status.compact.approval': '待审批', + 'status.compact.planReview': '计划待审', + 'status.compact.answer': '待回答', 'status.completed': '已完成', 'schedule.active': '有活动定时任务', 'hover.created': '创建于 {time}', @@ -129,6 +132,9 @@ export const en = { 'status.waitingApproval': 'Waiting for approval', 'status.planReview': 'Plan awaiting review', 'status.waitingAnswer': 'Waiting for answer', + 'status.compact.approval': 'Approval', + 'status.compact.planReview': 'Plan review', + 'status.compact.answer': 'Answer', 'status.completed': 'Completed', 'schedule.active': 'Has active scheduled task', 'hover.created': 'Created {time}', diff --git a/packages/client/ui-workspace/src/client/rows/Rows.module.css b/packages/client/ui-workspace/src/client/rows/Rows.module.css index a222d05da9..0f5792b4c2 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.module.css +++ b/packages/client/ui-workspace/src/client/rows/Rows.module.css @@ -219,9 +219,9 @@ .time { flex: none; - font-size: 12px; - line-height: 20px; - color: var(--dsw-alias-label-tertiary); + font-size: 10px; + line-height: 16px; + color: var(--dsw-alias-label-caption); } .scheduleIndicator { diff --git a/packages/client/ui-workspace/src/client/rows/Rows.tsx b/packages/client/ui-workspace/src/client/rows/Rows.tsx index 3d015f3c57..20cc9a4852 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.tsx +++ b/packages/client/ui-workspace/src/client/rows/Rows.tsx @@ -9,9 +9,9 @@ import { useEffect, useRef, useState } from 'react' import clsx from 'clsx' import { - HoverCard, IconAlarmClockOutline16, IconArchiveOutline20, IconBranchOutline16, - IconEditOutline16, IconEllipsisOutline16, IconFolderClose16, IconFolderOpen16, - IconPlusOutline16, IconTrashOutline16, IconTriangleRightFill14, Menu, relativeTime, + HoverCard, IconAlarmClockOutlineRegular, IconArchiveOutlineRegular, IconBranchOutlineRegular, + IconEditOutlineRegular, IconEllipsisOutlineRegular, IconFolderCloseRegular, IconFolderOpenRegular, + IconPlusOutlineMedium, IconTrashOutlineRegular, IconTriangleRightFillRegular, Menu, relativeTime, StateDot, } from '@deepseek-ai/dsh-client-ui-primitives' import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives' @@ -154,8 +154,8 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog const active = containsCurrentDescendant || (group.expanded && group.containsCurrent) const [menuOpen, setMenuOpen] = useState(false) const workspaceMenuItems = [ - { id: 'rename', label: t('rename'), icon: }, - { id: 'delete', label: t('delete.workspace'), icon: , danger: true }, + { id: 'rename', label: t('rename'), icon: }, + { id: 'delete', label: t('delete.workspace'), icon: , danger: true }, ] const ownRow = (
    - {row.expanded ? : } + {row.expanded ? : } - + {label} @@ -206,7 +206,7 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog aria-label={t('actions.workspace.aria', { name: label })} onClick={(e) => { e.stopPropagation(); setMenuOpen(v => !v) }} > - + )} /> @@ -217,7 +217,7 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog aria-label={t('actions.newSession.aria', { name: label })} onClick={(e) => { e.stopPropagation(); onCreate() }} > - +
    @@ -249,6 +249,8 @@ function assertNever(value: never): never { interface SessionStatus { state: StateDotState label: string + /** Compact text that replaces the Session row's update time. */ + trailingLabel?: string } /** @@ -273,13 +275,25 @@ function sessionStatuses( let pending: SessionStatus | undefined switch (node.pendingInteraction) { case 'approval': - pending = { state: 'warning', label: t('status.waitingApproval') } + pending = { + state: 'warning', + label: t('status.waitingApproval'), + trailingLabel: t('status.compact.approval'), + } break case 'plan-review': - pending = { state: 'warning', label: t('status.planReview') } + pending = { + state: 'warning', + label: t('status.planReview'), + trailingLabel: t('status.compact.planReview'), + } break case 'question': - pending = { state: 'warning', label: t('status.waitingAnswer') } + pending = { + state: 'warning', + label: t('status.waitingAnswer'), + trailingLabel: t('status.compact.answer'), + } break case undefined: break /* v8 ignore next -- closed PendingInteractionStatus union */ @@ -292,7 +306,7 @@ function sessionStatuses( } if (subagents !== undefined) return [subagents] if (node.completed) return [{ state: 'done', label: t('status.completed') }] - return [{ state: 'done', label: t('status.idle') }] + return [{ state: 'idle', label: t('status.idle') }] } /** Primary status dot plus every status's screen-reader label, shared by the search and session rows. */ @@ -317,7 +331,7 @@ function ActiveScheduleIndicator({ t, search = false }: { t: RowTranslate; searc aria-label={label} title={label} > - + ) } @@ -370,7 +384,7 @@ export function SearchResultItem({ result, currentId, onOpen, t }: { > - {(primaryStatus.state !== 'done' || result.completed) && ( + {primaryStatus.state !== 'idle' && ( )} @@ -389,7 +403,8 @@ export function SearchResultItem({ result, currentId, onOpen, t }: { /** * One top-level 34px session row: status dot (pending user interaction outranks - * own or descendant activity), title, relative time, and the row actions menu. + * own or descendant activity), title, relative time or compact pending label, + * and the row actions menu. * @param props.node - derived session node. * @param props.currentId - selected session id (row highlight). * @param props.now - epoch ms for relative-time formatting. @@ -429,7 +444,7 @@ export function SessionNodeItem({ const selected = node.id === currentId const statuses = sessionStatuses(node, t) const primaryStatus = statuses[0] - const showStatus = primaryStatus.state !== 'done' || row.completed + const showStatus = primaryStatus.state !== 'idle' const draggable = drag !== undefined && !row.blank const [menuOpen, setMenuOpen] = useState(false) const rowRef = useRef(null) @@ -443,10 +458,9 @@ export function SessionNodeItem({ // touches the session log, so it is not styled as destructive and needs no // confirmation dialog. const sessionMenuItems = [ - { id: 'rename', label: t('rename'), icon: }, - { id: 'fork', label: t('menu.fork'), icon: }, - // 20-native glyph in the menu's 16px icon slot (Menu.module.css .itemIcon). - { id: 'archive', label: t('menu.archiveSession'), icon: }, + { id: 'rename', label: t('rename'), icon: }, + { id: 'fork', label: t('menu.fork'), icon: }, + { id: 'archive', label: t('menu.archiveSession'), icon: }, ] // Figma session cell: pad 8, status slot 16, then a 4px title gap. const ownRow = ( @@ -501,7 +515,14 @@ export function SessionNodeItem({ happened in it yet, so a "now" timestamp and the row verbs (rename/fork/archive) would all act on content that does not exist — both trailing cells stay off until the first prompt. */} - {!row.blank && {timeLabel(row.updatedAt, now, t)}} + {!row.blank && ( + + {primaryStatus.trailingLabel ?? timeLabel(row.updatedAt, now, t)} + + )} {!row.blank && ( { e.stopPropagation(); setMenuOpen(v => !v) }} > - + )} /> diff --git a/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.module.css b/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.module.css index 6616da56e8..91ad5eb779 100644 --- a/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.module.css +++ b/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.module.css @@ -1,6 +1,6 @@ .root { --dsh-session-list-edge-inset: var(--dsh-sidebar-inline-padding); - --dsh-session-list-scrollbar-width: 8px; + --dsh-session-list-scrollbar-width: 5px; --dsh-session-list-scrollbar-offset: 2px; flex: 1; min-height: 0; @@ -341,7 +341,7 @@ } /* List: the only scrolling region. Block children keep their design heights - under content overflow. The 2px edge offset, stable 8px themed scrollbar, + under content overflow. The 2px edge offset, stable 5px themed scrollbar, and remaining padding equal the shell's right inset, with or without overflow, so moving the bar does not move the rows. */ .list { diff --git a/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx b/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx index e41a695b0d..784391e45d 100644 --- a/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx +++ b/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx @@ -12,8 +12,8 @@ import { type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { - Button, IconCloseFill14, IconPersonalizationOutline16, - IconProjectAddOutline16, IconSearchOutline16, Menu, Modal, Tooltip, + Button, IconCloseFillRegular, IconPersonalizationOutlineRegular, + IconProjectAddOutlineRegular, IconSearchOutlineRegular, Menu, Modal, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' import type { SessionListState, SessionSearchResultItem, @@ -138,7 +138,7 @@ function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }: { aria-label={t('viewOptions.label')} onClick={() => { setOpen(v => !v) }} > - + )} @@ -1139,7 +1139,7 @@ export function WorkspaceBrowser({ setSearchExpanded(true) }} > - + - + )}
    @@ -1198,7 +1198,7 @@ export function WorkspaceBrowser({ setWsPickerOpen(v => !v) }} > - + )} @@ -1235,7 +1235,7 @@ export function WorkspaceBrowser({ expandSidebar() }} > - +
    } diff --git a/packages/client/ui-workspace/tests/browser-styles.client.spec.ts b/packages/client/ui-workspace/tests/browser-styles.client.spec.ts index 3b25db0644..38d4ee36a3 100644 --- a/packages/client/ui-workspace/tests/browser-styles.client.spec.ts +++ b/packages/client/ui-workspace/tests/browser-styles.client.spec.ts @@ -45,7 +45,7 @@ describe('WorkspaceBrowser.module.css list', () => { it('counts the themed scrollbar inside the shell trailing inset', () => { expect(root?.get('--dsh-session-list-edge-inset')).toBe('var(--dsh-sidebar-inline-padding)') - expect(root?.get('--dsh-session-list-scrollbar-width')).toBe('8px') + expect(root?.get('--dsh-session-list-scrollbar-width')).toBe('5px') expect(root?.get('--dsh-session-list-scrollbar-offset')).toBe('2px') expect(root?.get('padding-right')).toBe('var(--dsh-session-list-edge-inset)') expect(listArea?.get('margin-left')).toBe('-4px') diff --git a/packages/client/ui-workspace/tests/rows.client.spec.tsx b/packages/client/ui-workspace/tests/rows.client.spec.tsx index 644cb50a8f..57308820e2 100644 --- a/packages/client/ui-workspace/tests/rows.client.spec.tsx +++ b/packages/client/ui-workspace/tests/rows.client.spec.tsx @@ -4,15 +4,17 @@ import { act, cleanup, createEvent, fireEvent, render, screen } from '@testing-l import type { WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import type { RowDragProps } from '../src/client/rows/Rows.tsx' import { ProjectRowItem, SearchResultItem, SessionNodeItem } from '../src/client/rows/Rows.tsx' import type { GroupNode, SearchResultNode, SessionNode } from '../src/client/tree.ts' -import { zh } from '../src/client/locales.ts' +import { en, zh } from '../src/client/locales.ts' afterEach(cleanup) const t = makeTranslate(zh, commonZh) as never +const tEn = makeTranslate(en, commonEn) as never const sid = (id: string) => id as SessionId const wid = (id: string) => id as WorkspaceId @@ -562,10 +564,10 @@ describe('workspace browser rows', () => { }) it.each([ - ['approval', '等待审批'], - ['plan-review', '计划待审'], - ['question', '等待回答'], - ] as const)('shows %s as warning ahead of the running state', (pendingInteraction, label) => { + ['approval', '等待审批', '待审批'], + ['plan-review', '计划待审', '计划待审'], + ['question', '等待回答', '待回答'], + ] as const)('shows %s as warning and replaces the row time', (pendingInteraction, label, compactLabel) => { vi.useFakeTimers() try { const node: SessionNode = { @@ -576,9 +578,13 @@ describe('workspace browser rows', () => { const view = render() const row = screen.getByRole('treeitem') + const title = screen.getByText('Needs input') expect(row.querySelector('[data-state="warning"]')).toBeTruthy() expect(row.querySelector('[data-state="ongoing"]')).toBeNull() - expect(screen.getByText(label)).toBeTruthy() + expect(row.textContent).toContain(label) + expect(title.nextElementSibling?.textContent).toBe(compactLabel) + expect(title.nextElementSibling?.getAttribute('aria-hidden')).toBe('true') + expect(row.textContent).not.toContain('刚刚') view.rerender() @@ -586,13 +592,32 @@ describe('workspace browser rows', () => { fireEvent.pointerEnter(screen.getByRole('treeitem').parentElement as HTMLElement) act(() => { vi.advanceTimersByTime(500) }) - expect(screen.getAllByText(label)).toHaveLength(2) + expect(screen.getAllByText(label).length).toBeGreaterThanOrEqual(2) + expect(screen.getByText('刚刚')).toBeTruthy() expect(document.querySelectorAll('[data-state="warning"]')).toHaveLength(2) } finally { vi.useRealTimers() } }) + it.each([ + ['approval', 'Approval'], + ['plan-review', 'Plan review'], + ['question', 'Answer'], + ] as const)('uses the compact English %s row label', (pendingInteraction, compactLabel) => { + const node: SessionNode = { + id: sid(pendingInteraction), title: 'Needs input', blank: false, + pendingInteraction, running: false, runningSubagentCount: 0, completed: false, + hasActiveSchedule: false, updatedAt: 0, + } + render() + const row = screen.getByRole('treeitem') + expect(screen.getByText('Needs input').nextElementSibling?.textContent).toBe(compactLabel) + expect(screen.getByText('Needs input').nextElementSibling?.getAttribute('aria-hidden')).toBe('true') + expect(row.textContent).not.toContain('now') + }) + it('idle hover card shows the Idle status line', () => { vi.useFakeTimers() try { @@ -604,7 +629,7 @@ describe('workspace browser rows', () => { onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />) fireEvent.pointerEnter(screen.getByRole('treeitem').parentElement as HTMLElement) act(() => { vi.advanceTimersByTime(500) }) - expect(screen.getByText('空闲')).toBeTruthy() + expect(screen.getByText('空闲').parentElement?.querySelector('[data-state="idle"]')).not.toBeNull() expect(screen.getAllByText('刚刚')).toHaveLength(2) } finally { vi.useRealTimers() diff --git a/packages/experimental/README.i18n.yaml b/packages/experimental/README.i18n.yaml index fba483caa6..af98f96967 100644 --- a/packages/experimental/README.i18n.yaml +++ b/packages/experimental/README.i18n.yaml @@ -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 packages/experimental/README.md -README.md: 6bb562cf21703fb153c04d9fc839610563bba6cf -README.zh.md: c586a3a89611bfc78587c2b9a48245d79e67515f +README.md: f9fa7be744ff84993373b0cb15451e1a5d5641a9 +README.zh.md: 58e7d1614f8ab200e0dad8ebb53122b371b5430d diff --git a/packages/experimental/README.md b/packages/experimental/README.md index 6bb562cf21..f9fa7be744 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -24,9 +24,8 @@ The experimental group contains prototype capabilities whose contracts can chang | Package | Role | ctx key | |---|---|---| -| [`agent-team-profile`](agent-team-profile/README.md) | Published opt-in profile layer for Agent Teams | — | +| [`agent-team-profile`](agent-team-profile/README.md) | Agent Teams collaboration, tools, and Web UI bundle | — | | [`agent-team`](agent-team/README.md) | Named teammates with durable messages and a shared task board | `ctx.agentTeams` | -| [`agent-team-web-profile`](agent-team-web-profile/README.md) | Published opt-in Web layer for Agent Teams | — | | [`client-ui-agent-team`](client-ui-agent-team/README.md) | Team roster, task board, and teammate navigation for Web | — | | [`auto-review`](auto-review/README.md) | Explicit Web layer for same-model review before each native or PTC inner tool call | — | | [`ptc-runtime-python`](ptc-runtime-python/README.md) | CPython subprocess backend for the PTC execution seam | `ctx.ptcRuntime` | diff --git a/packages/experimental/README.zh.md b/packages/experimental/README.zh.md index c586a3a896..58e7d1614f 100644 --- a/packages/experimental/README.zh.md +++ b/packages/experimental/README.zh.md @@ -24,9 +24,8 @@ kind: "package-group" | 包 | 职责 | ctx 键 | |---|---|---| -| [`agent-team-profile`](agent-team-profile/README.zh.md) | Agent Teams 的公开 opt-in profile 层 | — | +| [`agent-team-profile`](agent-team-profile/README.zh.md) | Agent Teams 协作、工具与 Web UI 组合包 | — | | [`agent-team`](agent-team/README.zh.md) | 具名 teammate,成员之间持久消息与共享任务板 | `ctx.agentTeams` | -| [`agent-team-web-profile`](agent-team-web-profile/README.zh.md) | Agent Teams 的公开 opt-in Web 层 | — | | [`client-ui-agent-team`](client-ui-agent-team/README.zh.md) | Web Team roster、任务板与 teammate 导航 | — | | [`auto-review`](auto-review/README.zh.md) | 显式 Web 层,在每个原生或 PTC inner 工具调用前使用同一模型审查 | — | | [`ptc-runtime-python`](ptc-runtime-python/README.zh.md) | PTC 执行 seam 的 CPython 子进程后端 | `ctx.ptcRuntime` | diff --git a/packages/experimental/agent-team-profile/README.i18n.yaml b/packages/experimental/agent-team-profile/README.i18n.yaml index 362d87d9f8..12faf98360 100644 --- a/packages/experimental/agent-team-profile/README.i18n.yaml +++ b/packages/experimental/agent-team-profile/README.i18n.yaml @@ -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 packages/experimental/agent-team-profile/README.md -README.md: 9aa294991d76d63a596c1d9afed2f9991f424f20 -README.zh.md: 98db275bbf583b02d1f370008345fa68882dbba1 +README.md: 2d94b4d1c0e7de9079ac28945f1e79594b17bb76 +README.zh.md: b1126a266c472d5548b0adbc4139d701ac5b4f6f diff --git a/packages/experimental/agent-team-profile/README.md b/packages/experimental/agent-team-profile/README.md index 9aa294991d..2d94b4d1c0 100644 --- a/packages/experimental/agent-team-profile/README.md +++ b/packages/experimental/agent-team-profile/README.md @@ -1,5 +1,5 @@ --- -description: "Published experimental Agent Teams profile layer over dsh-base with teammate delegation and fresh workflow children." +description: "Enable team collaboration, tools, and the Web roster and task board with one experimental bundle." kind: "package-bundle" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-experimental-agent-team-profile` is a published experimental profile layer that enables [Agent Teams](../agent-team/README.md) over `@deepseek-ai/dsh-base`. Its patch inserts the Team domain and Team-scoped tools and disables ordinary subagent delegation and the overlapping global continuable-child controls. Workflow remains available with fresh children. The dsh installation ships it as an optional bundle that no shipped profile enables; switch it on from the Web sidebar's Plugins page, or add it explicitly to an initialized profile. +`dsh-experimental-agent-team-profile` enables [Agent Teams](../agent-team/README.md) collaboration, tools, and Web UI with one bundle. Delegate work to teammates and view members, shared tasks, and teammate sessions in Web. Ordinary subagent delegation and overlapping global child controls are disabled; Workflow can still create fresh children. The bundle ships with dsh switched off. Enable it from the Plugins page or add it to an initialized profile. ## Table of Contents @@ -36,10 +36,20 @@ dsh --profile headless "Use Agent Teams to split this task between two teammates The profile must already contain `@deepseek-ai/dsh-base`, whose Subagent services and provider rows this layer consumes. Removing the package with `dsh plugin --profile remove @deepseek-ai/dsh-experimental-agent-team-profile` removes the bundle from the profile's ordered layer list. +Enable Agent Teams on the Web or Desktop Plugins page to activate both tools and UI. For a CLI Web profile, use: + +```sh +dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile +``` + +In an existing profile’s `package.json`, keep `@deepseek-ai/dsh-experimental-agent-team-profile` in `dsh.profile.bundles` and remove the separate `@deepseek-ai/dsh-experimental-agent-team-web-profile` entry. User patches targeting `ui-agent-team` still apply. + ### What you get The layer adds the Agent Teams domain and its scoped creation, roster, messaging, interruption, waiting, and task-board tools. Direct delegation uses `spawn_teammate`, which supports fresh and fork context. The `subagent` and `subagent_fork` tools and overlapping global child controls are disabled. Workflow retains the base profile’s `spawn` provider, while the underlying Subagent services and both providers remain available to teammates and workflow. +In Web and Desktop conversations, the [Team UI](../client-ui-agent-team/README.md) displays the member roster and shared task board and opens teammate sessions. The same bundle switch controls the tools and browser UI. + ----- @@ -48,13 +58,13 @@ The layer adds the Agent Teams domain and its scoped creation, roster, messaging
    Implementation internals — click to expand -The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-base`, the patch disables `tool-subagent-control`, `tool-subagent-list-agents`, `tool-subagent`, and `tool-subagent-fork`, and inserts the Team service and tool rows with explicit providers and limits. +The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-base`, the patch disables `tool-subagent-control`, `tool-subagent-list-agents`, `tool-subagent`, and `tool-subagent-fork`, and inserts the Team service, tools, and UI with explicit providers and limits. The UI plugin has an inert Host entry; only the Web Client loader mounts its browser entry, so headless needs no Web server. | File | Role | |---|---| | [`cordis.patch.yml`](cordis.patch.yml) | Ordered patch over `dsh-base` | | [`src/index.ts`](src/index.ts) | Empty module entry; the patch is the runtime content | -| — | No runtime invariant companion is published; the package carries only a static profile patch. The Team domain and tool packages own the mutable relationships it activates. | +| — | No runtime invariant companion is published; the package carries only a static profile patch. The Team service and tools own their mutable relationships; the UI package owns its disposable slot registration. |
    @@ -66,6 +76,7 @@ The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied - [Experimental packages](../README.md) — incubation status and publication policy. - [Agent Teams service](../agent-team/README.md) — durable roster, messaging, and task-board behavior. - [Agent Teams tools](../tool-agent-team/README.md) — the Team-scoped model tool surface. +- [Agent Teams browser UI](../client-ui-agent-team/README.md) — roster, task board, and teammate session navigation. - [Base bundle](../../bundle/base/README.md) — the profile layer this patch extends. ----- @@ -94,6 +105,7 @@ The bundle's composition is prefix-stable while its patch, Team identity, and co - **Opt-in only** — the package ships with the installation switched off; no shipped CLI, Web, SDK, ACP, or Python profile enables it. - **Workflow child tools** — the [Team tool visibility limitation](../tool-agent-team/README.md#known-limitations-and-deferred-work) also applies to workflow children. - **Shared checkout** — every teammate observes the same working directory; this bundle adds no worktree isolation or filesystem locking. +- **Preset-scoped child controls** — Web presets can still mount continuable Subagent controls in their own scope; this top-level bundle does not replace those registrations. - **Base profile required** — the patch depends on row ids and Subagent providers supplied by `dsh-base`; it is not a standalone profile. diff --git a/packages/experimental/agent-team-profile/README.zh.md b/packages/experimental/agent-team-profile/README.zh.md index 98db275bbf..b1126a266c 100644 --- a/packages/experimental/agent-team-profile/README.zh.md +++ b/packages/experimental/agent-team-profile/README.zh.md @@ -1,5 +1,5 @@ --- -description: "叠加在 dsh-base 上公开发布的实验性 Agent Teams profile 层,提供 teammate 委派与 fresh workflow 子代理。" +description: "在一个实验性组合包中启用团队协作、工具与 Web 成员和任务看板。" kind: "package-bundle" --- @@ -9,7 +9,7 @@ kind: "package-bundle" ## 概述 -`dsh-experimental-agent-team-profile` 是在 `@deepseek-ai/dsh-base` 之上启用 [Agent Teams](../agent-team/README.zh.md) 的公开实验性 profile 层。它的 patch 会插入 Team domain 与 Team-scoped 工具,并禁用普通 subagent 委派和名称重叠的全局 continuable-child control。Workflow 仍可创建 fresh 子代理。dsh 安装随附本包作为可选组合包,随附 profile 都不会启用它;可在 Web 侧栏的插件页开启,或显式添加到已初始化的 profile。 +`dsh-experimental-agent-team-profile` 让 [Agent Teams](../agent-team/README.zh.md) 的团队协作、工具和 Web 界面通过一个组合包启用。开启后可委派 teammate,并在 Web 中查看成员、任务看板和成员会话。普通 subagent 委派及名称重叠的全局 child control 会被禁用;Workflow 仍可创建 fresh 子代理。本包随 dsh 安装提供,默认关闭,可在插件页开启或添加到已初始化的 profile。 ## 目录 @@ -36,10 +36,20 @@ dsh --profile headless "Use Agent Teams to split this task between two teammates profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Subagent 服务与提供方配置行。执行 `dsh plugin --profile remove @deepseek-ai/dsh-experimental-agent-team-profile` 移除本包时,bundle 也会从 profile 的有序层列表中移除。 +在 Web 或 Desktop 的插件页开启「智能体团队」,即可同时启用工具与界面。CLI 的 Web profile 也可使用以下命令: + +```sh +dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile +``` + +已有 profile 的 `package.json` 中,`dsh.profile.bundles` 应只保留 `@deepseek-ai/dsh-experimental-agent-team-profile`,删除独立的 `@deepseek-ai/dsh-experimental-agent-team-web-profile` 条目。用户 patch 中的 `ui-agent-team` 配置仍然有效。 + ### 获得的功能 本层会添加 Agent Teams domain,以及 Team-scoped 创建、roster、消息、interrupt、等待与任务板工具。直接委派使用支持 fresh 和 fork 上下文的 `spawn_teammate`。`subagent`、`subagent_fork` 工具和名称重叠的全局 child control 均被禁用。Workflow 保留 base profile 的 `spawn` 提供方,底层 Subagent 服务和两个提供方仍供 teammate 与 workflow 使用。 +在 Web 和 Desktop 会话中,[Team UI](../client-ui-agent-team/README.zh.md) 显示成员列表与共享任务看板,并可打开成员会话。同一个组合包开关控制工具与浏览器 UI。 + ----- @@ -48,13 +58,13 @@ profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Su
    实现细节——点击展开 -本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-base` 之后应用时,patch 会禁用 `tool-subagent-control`、`tool-subagent-list-agents`、`tool-subagent` 和 `tool-subagent-fork`,并以显式 provider 和限制插入 Team 服务与工具行。 +本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-base` 之后应用时,patch 会禁用 `tool-subagent-control`、`tool-subagent-list-agents`、`tool-subagent` 和 `tool-subagent-fork`,并以显式 provider 和限制插入 Team 服务、工具和 UI 行。UI 插件的 Host 入口不执行逻辑;只有 Web 客户端加载器会挂载其浏览器入口,因此 headless 无需启动 Web 服务。 | 文件 | 职责 | |---|---| | [`cordis.patch.yml`](cordis.patch.yml) | 叠加在 `dsh-base` 之上的有序 patch | | [`src/index.ts`](src/index.ts) | 空模块入口;patch 是运行时内容 | -| — | 不发布运行时不变式伴生入口;本包是静态 bundle,不持有可独立观察的运行时关系。 | +| — | 不发布运行时不变式伴生入口;本包只携带静态 profile patch。Team 服务与工具各自持有其可变关系,UI 包持有其可释放的 slot 注册。 |
    @@ -66,6 +76,7 @@ profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Su - [实验性包](../README.zh.md)——孵化状态与发布规则。 - [Agent Teams service](../agent-team/README.zh.md)——持久 roster、消息与任务板行为。 - [Agent Teams 工具](../tool-agent-team/README.zh.md)——Team-scoped 模型工具表层。 +- [Agent Teams 浏览器 UI](../client-ui-agent-team/README.zh.md)——成员列表、任务看板与成员会话导航。 - [Base bundle](../../bundle/base/README.zh.md)——本 patch 扩展的 profile 层。 ----- @@ -94,6 +105,7 @@ Team 策略与 schema 由 [`@deepseek-ai/dsh-experimental-tool-agent-team`](../t - **仅显式启用**——本包随安装提供但默认关闭;随附 CLI、Web、SDK、ACP 与 Python profile 都不会启用它。 - **Workflow 子代理工具**——[Team 工具可见性限制](../tool-agent-team/README.zh.md#known-limitations-and-deferred-work)也适用于 workflow 子代理。 - **共享 checkout**——所有 teammate 都观察同一个工作目录;本 bundle 不提供 worktree 隔离或文件系统锁。 +- **预设内的子代理控件**——Web 预设仍可在预设作用域挂载 continuable Subagent 控件;顶层组合包不会替换这些注册。 - **需要 base profile**——本 patch 依赖 `dsh-base` 提供的配置行 id 与 Subagent 提供方;它不是独立 profile。 diff --git a/packages/experimental/agent-team-profile/cordis.patch.yml b/packages/experimental/agent-team-profile/cordis.patch.yml index f1eee0e9ba..5543297c91 100644 --- a/packages/experimental/agent-team-profile/cordis.patch.yml +++ b/packages/experimental/agent-team-profile/cordis.patch.yml @@ -28,3 +28,6 @@ config: freshProvider: spawn forkProvider: fork + + - id: ui-agent-team + name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json index 9749dec6f9..a8b8967147 100644 --- a/packages/experimental/agent-team-profile/package.json +++ b/packages/experimental/agent-team-profile/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-profile", - "description": "Experimental profile bundle enabling Agent Teams over dsh-base", + "description": "Agent Teams collaboration, tools, and Web UI in one experimental bundle", "version": "0.1.6-alpha.2", "publishConfig": { "access": "public" @@ -35,6 +35,7 @@ }, "dependencies": { "@deepseek-ai/dsh-experimental-agent-team": "workspace:^", + "@deepseek-ai/dsh-experimental-client-ui-agent-team": "workspace:^", "@deepseek-ai/dsh-experimental-tool-agent-team": "workspace:^" }, "peerDependencies": { diff --git a/packages/experimental/agent-team-profile/tests/profile.spec.ts b/packages/experimental/agent-team-profile/tests/profile.spec.ts index fdebc49182..dd65a021ec 100644 --- a/packages/experimental/agent-team-profile/tests/profile.spec.ts +++ b/packages/experimental/agent-team-profile/tests/profile.spec.ts @@ -21,6 +21,7 @@ describe('Agent Teams profile bundle', () => { expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') expect(manifest.dependencies).toMatchObject({ '@deepseek-ai/dsh-experimental-agent-team': 'workspace:^', + '@deepseek-ai/dsh-experimental-client-ui-agent-team': 'workspace:^', '@deepseek-ai/dsh-experimental-tool-agent-team': 'workspace:^', }) @@ -48,5 +49,8 @@ describe('Agent Teams profile bundle', () => { name: '@deepseek-ai/dsh-experimental-tool-agent-team', config: { freshProvider: 'spawn', forkProvider: 'fork' }, }) + expect(inserted.find(entry => entry.id === 'ui-agent-team')).toMatchObject({ + name: '@deepseek-ai/dsh-experimental-client-ui-agent-team', + }) }) }) diff --git a/packages/experimental/agent-team-web-profile/README.i18n.yaml b/packages/experimental/agent-team-web-profile/README.i18n.yaml deleted file mode 100644 index 9febb49882..0000000000 --- a/packages/experimental/agent-team-web-profile/README.i18n.yaml +++ /dev/null @@ -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 packages/experimental/agent-team-web-profile/README.md -README.md: e21a7eeebc83a97d3e4630c95a9aeaa4213bc227 -README.zh.md: 9966697cb7225083a6fff515675b9dd9ecc51c82 diff --git a/packages/experimental/agent-team-web-profile/README.md b/packages/experimental/agent-team-web-profile/README.md deleted file mode 100644 index e21a7eeebc..0000000000 --- a/packages/experimental/agent-team-web-profile/README.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -description: "Add the published experimental Agent Teams panel to a Web profile after the Host Team layer." -kind: "package-bundle" ---- - -# @deepseek-ai/dsh-experimental-agent-team-web-profile - -English | [中文](README.zh.md) - -## Summary - -`dsh-experimental-agent-team-web-profile` is the published experimental Web layer for [Agent Teams](../agent-team/README.md). Add it after `@deepseek-ai/dsh-web-app` and [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.md) to show the Team roster, task board, and teammate navigation in the browser. Removing either experimental layer leaves the stable base and Web composition unchanged. The dsh installation ships it as an optional bundle that no shipped Web profile enables; switch it on from the Web sidebar's Plugins page after the Host layer. - -## Table of Contents - -- [Use this package](#use-this-package) -- [Understand the implementation](#understand-the-implementation) -- [Further Exploration](#further-exploration) -- [Model Experience](#model-experience) -- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) -- [Dev Note](#dev-note) - ------ - - -## Use this package - -### Install into a profile - -Add the Host and Web Agent Teams layers to an initialized `web` profile in this order: - -```sh -dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile -dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-web-profile -``` - -The first command supplies the Team domain, generated Remote methods, and model tools. The second command activates this package's declared patch and its browser presentation. Removing the package with `dsh plugin --profile web remove @deepseek-ai/dsh-experimental-agent-team-web-profile` removes the Web layer from the profile's ordered bundle list. - -### What you get - -The conversation header gains the Team roster, shared task board, and teammate navigation. [`@deepseek-ai/dsh-experimental-client-ui-agent-team`](../client-ui-agent-team/README.md) owns those browser interactions and mounts the generated Client Remote namespace used to reach the Host Team service. - ------ - - -## Understand the implementation - -
    -Implementation internals — click to expand - -The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-web-app` and the Host Agent Teams layer, its single `insert` entry adds the `ui-agent-team` row for `@deepseek-ai/dsh-experimental-client-ui-agent-team`. The inserted Client plugin owns the generated Remote assembly and Team UI; this static bundle holds no mutable state and installs no runtime invariant. - -| File | Role | -|---|---| -| [`cordis.patch.yml`](cordis.patch.yml) | Ordered Web patch containing the `ui-agent-team` row | -| [`src/index.ts`](src/index.ts) | Empty module entry; the patch is the runtime content | -| — | No runtime invariant companion is published; the package carries only a static profile patch. The Remote assembly and Team UI own their activation requirements. | - -
    - ------ - - -## Further Exploration - -- [Experimental packages](../README.md) — incubation status and publication policy. -- [Agent Teams Host profile](../agent-team-profile/README.md) — the required domain, Remote, and model-tool layer. -- [Agent Teams browser UI](../client-ui-agent-team/README.md) — roster, task-board, and teammate-navigation behavior. -- [Web bundle](../../bundle/web-app/README.md) — the stable browser layer this patch extends. - ------ - - -## Model Experience - -Indirectly, through the Host-side Agent Teams profile selected alongside this Web layer. - -#### KV Cache effect - -This Web bundle adds no model request content; the Host-side Team tools own prompt, schema, and cache effects. - -## Known Limitations and Deferred Work - - - -- **Ordered composition** — `dsh-base`, `dsh-web-app`, `dsh-experimental-agent-team-profile`, and this package must remain in that order. -- **Preset-scoped legacy controls** — stable Web presets still mount continuable Subagent controls inside the preset scope. Top-level Host profile overrides do not replace those scoped registrations, so the Team roster and legacy child controls can both appear until Web has a Team-aware preset. The [Web Agent Teams decision](../../../.agents/notes/archived/feature/2026-08-06-agent-teams-web.md) records this deferred composition work. -- **Opt-in only** — the package ships with the installation switched off; no shipped Web profile enables either Agent Teams layer. - - -### Dev Note - -
    -Working context for maintainers — click to expand - -None. - -
    diff --git a/packages/experimental/agent-team-web-profile/README.zh.md b/packages/experimental/agent-team-web-profile/README.zh.md deleted file mode 100644 index 9966697cb7..0000000000 --- a/packages/experimental/agent-team-web-profile/README.zh.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -description: "在 Host Team 层之后,为 Web profile 添加公开发布的实验性 Agent Teams 面板。" -kind: "package-bundle" ---- - -# @deepseek-ai/dsh-experimental-agent-team-web-profile - -[English](README.md) | 中文 - -## 概述 - -`dsh-experimental-agent-team-web-profile` 是 [Agent Teams](../agent-team/README.zh.md) 公开发布的实验性 Web 层。把它放在 `@deepseek-ai/dsh-web-app` 与 [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.zh.md) 之后,即可在浏览器中显示 Team roster、任务板与 teammate 导航。移除任一实验层都会让稳定的 base 与 Web composition 保持不变。dsh 安装随附本包作为可选组合包,随附 Web profile 不会启用它;在开启 Host 层后可在 Web 侧栏的插件页开启。 - -## 目录 - -- [使用本包](#use-this-package) -- [理解实现](#understand-the-implementation) -- [进一步探索](#further-exploration) -- [模型体验](#model-experience) -- [已知限制与延期工作](#known-limitations-and-deferred-work) -- [开发备注](#dev-note) - ------ - - -## 使用本包 - -### 安装到 profile - -按以下顺序把 Host 与 Web Agent Teams 层添加到已初始化的 `web` profile: - -```sh -dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-profile -dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-web-profile -``` - -第一条命令提供 Team domain、生成的 Remote 方法与模型工具。第二条命令激活本包声明的 patch 及其浏览器 presentation。执行 `dsh plugin --profile web remove @deepseek-ai/dsh-experimental-agent-team-web-profile` 移除本包时,Web 层也会从 profile 的有序 bundle 列表中移除。 - -### 获得的功能 - -对话标题栏会获得 Team roster、共享任务板与 teammate 导航。[`@deepseek-ai/dsh-experimental-client-ui-agent-team`](../client-ui-agent-team/README.zh.md) 负责这些浏览器交互,并挂载用于访问 Host Team service 的生成 Client Remote namespace。 - ------ - - -## 理解实现 - -
    -实现细节——点击展开 - -本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-web-app` 与 Host Agent Teams 层之后应用时,它唯一的 `insert` 条目会为 `@deepseek-ai/dsh-experimental-client-ui-agent-team` 添加 `ui-agent-team` 行。插入的 Client 插件负责生成的 Remote assembly 与 Team UI;这个静态 bundle 不持有可变状态,也不安装运行时不变式。 - -| 文件 | 职责 | -|---|---| -| [`cordis.patch.yml`](cordis.patch.yml) | 包含 `ui-agent-team` 行的有序 Web patch | -| [`src/index.ts`](src/index.ts) | 空模块入口;patch 是运行时内容 | -| — | 不发布运行时不变式伴生入口;本包只携带静态 profile patch,Remote assembly 与 Team UI 负责各自的激活要求。 | - -
    - ------ - - -## 进一步探索 - -- [实验性包](../README.zh.md)——孵化状态与发布规则。 -- [Agent Teams Host profile](../agent-team-profile/README.zh.md)——所需的 domain、Remote 与模型工具层。 -- [Agent Teams 浏览器 UI](../client-ui-agent-team/README.zh.md)——roster、任务板与 teammate 导航行为。 -- [Web bundle](../../bundle/web-app/README.zh.md)——本 patch 扩展的稳定浏览器层。 - ------ - - -## 模型体验 - -通过与本 Web 层同时选择的 Host-side Agent Teams profile 间接产生影响。 - -#### KV Cache 影响 - -本 Web bundle 不添加任何模型请求内容;Host-side Team 工具负责提示词、schema 与缓存影响。 - -## 已知限制与延期工作 - - - -- **有序组合**——`dsh-base`、`dsh-web-app`、`dsh-experimental-agent-team-profile` 与本包必须保持这个顺序。 -- **Preset-scoped 旧控制项**——稳定 Web preset 仍会在 preset scope 内挂载 continuable Subagent 控制项。顶层 Host profile override 不会替换这些 scoped registration,因此在 Web 获得 Team-aware preset 前,Team roster 与旧 child 控制项可能同时出现。[Web Agent Teams 决策](../../../.agents/notes/archived/feature/2026-08-06-agent-teams-web.md)记录了这项暂缓的 composition 工作。 -- **仅显式启用**——本包随安装提供但默认关闭;随附 Web profile 不会启用任何 Agent Teams 层。 - - -### 开发备注 - -
    -维护者的工作上下文——点击展开 - -无。 - -
    diff --git a/packages/experimental/agent-team-web-profile/cordis.patch.yml b/packages/experimental/agent-team-web-profile/cordis.patch.yml deleted file mode 100644 index e9cffef438..0000000000 --- a/packages/experimental/agent-team-web-profile/cordis.patch.yml +++ /dev/null @@ -1,6 +0,0 @@ -# Experimental Agent Teams Web layer. Apply after dsh-web-app and the host-side -# dsh-agent-team-profile so the browser mounts only when the Team service is present. - -- insert: - - id: ui-agent-team - name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' diff --git a/packages/experimental/agent-team-web-profile/package.json b/packages/experimental/agent-team-web-profile/package.json deleted file mode 100644 index c90fe666c2..0000000000 --- a/packages/experimental/agent-team-web-profile/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@deepseek-ai/dsh-experimental-agent-team-web-profile", - "description": "Experimental Web profile layer for Agent Teams Remote and UI plugins", - "version": "0.1.6-alpha.2", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", - "directory": "packages/experimental/agent-team-web-profile" - }, - "type": "module", - "main": "lib/index.js", - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/index.d.ts", - "default": "./lib/index.js" - }, - "./cordis.patch.yml": "./cordis.patch.yml", - "./src/*": "./src/*", - "./package.json": "./package.json" - }, - "files": [ - "lib/index.js", - "cordis.patch.yml", - "lib/types/**/*.d.ts" - ], - "license": "MIT", - "dsh": { - "bundle": { - "patch": "./cordis.patch.yml" - } - }, - "dependencies": { - "@deepseek-ai/dsh-experimental-client-ui-agent-team": "workspace:^" - }, - "peerDependencies": { - "@deepseek-ai/cordis": "workspace:^" - }, - "devDependencies": { - "@deepseek-ai/cordis-plugin-include": "workspace:^", - "@deepseek-ai/cordis": "workspace:^", - "js-yaml": "^4.2.0" - } -} diff --git a/packages/experimental/agent-team-web-profile/src/index.ts b/packages/experimental/agent-team-web-profile/src/index.ts deleted file mode 100644 index 726f066041..0000000000 --- a/packages/experimental/agent-team-web-profile/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** Experimental Web profile layer for the Agent Teams Client plugins. */ - -export {} diff --git a/packages/experimental/agent-team-web-profile/tests/profile.spec.ts b/packages/experimental/agent-team-web-profile/tests/profile.spec.ts deleted file mode 100644 index 21fde2cab5..0000000000 --- a/packages/experimental/agent-team-web-profile/tests/profile.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** The experimental Web bundle must carry one parseable Team Client layer. */ - -import { readFileSync } from 'node:fs' -import { fileURLToPath } from 'node:url' -import { resolve } from 'node:path' -import { describe, expect, it } from 'vitest' -import * as yaml from 'js-yaml' -import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' - -describe('Agent Teams Web profile bundle', () => { - it('declares a public parseable layer containing the Team UI', () => { - const root = fileURLToPath(new URL('..', import.meta.url)) - const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { - private?: boolean - publishConfig?: { access?: string } - dependencies?: Record - dsh?: { bundle?: { patch?: string } } - } - expect(manifest.private).toBeUndefined() - expect(manifest.publishConfig?.access).toBe('public') - expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') - expect(manifest.dependencies).toEqual({ - '@deepseek-ai/dsh-experimental-client-ui-agent-team': 'workspace:^', - }) - - const parsed = yaml.load( - readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'), - { schema: entryListSchema }, - ) as { insert?: { id?: string; name?: string }[] }[] - expect(parsed.flatMap(patch => patch.insert ?? [])).toEqual([ - { id: 'ui-agent-team', name: '@deepseek-ai/dsh-experimental-client-ui-agent-team' }, - ]) - }) -}) diff --git a/packages/experimental/agent-team-web-profile/tsconfig.json b/packages/experimental/agent-team-web-profile/tsconfig.json deleted file mode 100644 index d4186340bf..0000000000 --- a/packages/experimental/agent-team-web-profile/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": ["src"], - "references": [ - { "path": "../../../vendor/cordis" } - ] -} diff --git a/packages/experimental/client-ui-agent-team/README.i18n.yaml b/packages/experimental/client-ui-agent-team/README.i18n.yaml index d4a0ae340d..a4250bc7be 100644 --- a/packages/experimental/client-ui-agent-team/README.i18n.yaml +++ b/packages/experimental/client-ui-agent-team/README.i18n.yaml @@ -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 packages/experimental/client-ui-agent-team/README.md -README.md: ab40e2de09406991c55160005f2f16cf6931e195 -README.zh.md: 2913ff65930f260d53a0bee3b3ed6cb9634fcd5a +README.md: f9609eba7cc156151b7e62bc0a298730f6f258a8 +README.zh.md: 4e47e162008f567b08debf18adb2423f81d18486 diff --git a/packages/experimental/client-ui-agent-team/README.md b/packages/experimental/client-ui-agent-team/README.md index ab40e2de09..f9609eba7c 100644 --- a/packages/experimental/client-ui-agent-team/README.md +++ b/packages/experimental/client-ui-agent-team/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This package adds an Agent Teams action to the Web conversation header, where a user can inspect the current roster, manage the shared task board, and navigate into a teammate's conversation. It reads authoritative Team state through the generated `ctx.remote.agentTeams` contribution and keeps ordinary child-history navigation on the stable addressed-subagent path. Choose it through the published experimental Agent Teams Web profile. The browser projection does not extend the stable API Proxy, store Team state, or register model-facing input. +This package adds an Agent Teams action to the Web conversation header, where a user can inspect the current roster, manage the shared task board, and navigate into a teammate's conversation. It reads authoritative Team state through the generated `ctx.remote.agentTeams` contribution and keeps ordinary child-history navigation on the stable addressed-subagent path. Choose it through the published experimental Agent Teams bundle. The browser projection does not extend the stable API Proxy, store Team state, or register model-facing input. ## Table of Contents @@ -25,14 +25,16 @@ This package adds an Agent Teams action to the Web conversation header, where a ## Use this package -Install the package through [`@deepseek-ai/dsh-experimental-agent-team-web-profile`](../agent-team-web-profile/README.md) after the stable Web bundle and the Host-side Agent Teams profile. The Web Client loader mounts the `/client` export; the root Host export is inert, and the package has no user configuration fields. +Enable this package through [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.md), which supplies the Team service, tools, and Web UI together. The Web Client loader mounts the `/client` export; the root Host export is inert, and the package has no user configuration fields. ### Inspect and navigate the roster -Opening the panel calls `agentTeams/view`. Roster rows show durable names, runtime status, model, and diagnostics. Selecting a healthy teammate refreshes the existing direct-child catalog and opens the ordinary `{ parentSessionId, childSessionId, mode: 'continuable' }` address. History and later human prompts continue through the stable addressed-subagent conversation path; this package adds no Team-specific address field. +Opening the panel calls `agentTeams/view`. Roster rows show durable names, runtime status, model, and diagnostics. Provisioning and running members use the shared ongoing loader, idle and inactive members use idle, and failed members use error. Selecting a healthy teammate refreshes the existing direct-child catalog and opens the ordinary `{ parentSessionId, childSessionId, mode: 'continuable' }` address. History and later human prompts continue through the stable addressed-subagent conversation path; this package adds no Team-specific address field. ### Manage the task board +Ready pending tasks use idle, blocked pending tasks use warning, in-progress tasks use ongoing, and completed tasks use done. + The task board shows task identity, owner, blockers, readiness, advisory write scopes, and overlap warnings. A user can create, edit, assign or unassign, complete, reopen, and delete tasks through `agentTeams/createTask` and `agentTeams/updateTask`. Every update sends the displayed revision, and create or update rejections remain explicit business results. ----- @@ -61,7 +63,7 @@ Starting a create or update invalidates older refreshes. Success reloads the com ## Further Exploration -- [Agent Teams Web profile](../agent-team-web-profile/README.md) — the published opt-in bundle that mounts this Client plugin. +- [Agent Teams bundle](../agent-team-profile/README.md) — the published opt-in bundle that mounts this Client plugin. - [Agent Teams service](../agent-team/README.md) — authoritative roster, task, and Remote behavior. - [Conversation UI](../../client/ui-conversation/README.md) — the stable header slot and addressed-subagent navigation surface. - [Experimental packages](../README.md) — incubation status and publication policy. diff --git a/packages/experimental/client-ui-agent-team/README.zh.md b/packages/experimental/client-ui-agent-team/README.zh.md index 2913ff6593..4e47e16200 100644 --- a/packages/experimental/client-ui-agent-team/README.zh.md +++ b/packages/experimental/client-ui-agent-team/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -本包向 Web 会话页头添加 Agent Teams action,让用户检查当前 roster、管理共享任务板并导航到 teammate 会话。它通过生成的 `ctx.remote.agentTeams` contribution 读取权威 Team 状态,并让普通 child history 导航继续使用稳定的 addressed-subagent 路径。通过公开发布的实验性 Agent Teams Web profile 选择本包。这个浏览器 projection 不扩展稳定 API Proxy、不存储 Team 状态,也不注册面向模型的输入。 +本包向 Web 会话页头添加 Agent Teams action,让用户检查当前 roster、管理共享任务板并导航到 teammate 会话。它通过生成的 `ctx.remote.agentTeams` contribution 读取权威 Team 状态,并让普通 child history 导航继续使用稳定的 addressed-subagent 路径。通过公开发布的实验性 Agent Teams bundle 选择本包。这个浏览器 projection 不扩展稳定 API Proxy、不存储 Team 状态,也不注册面向模型的输入。 ## 目录 @@ -25,14 +25,16 @@ kind: "package-reference" ## 使用本包 -在稳定 Web bundle 与 Host-side Agent Teams profile 之后,通过 [`@deepseek-ai/dsh-experimental-agent-team-web-profile`](../agent-team-web-profile/README.zh.md) 安装本包。Web Client loader 挂载 `/client` export;root Host export 不执行行为,本包也没有用户配置字段。 +通过 [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.zh.md) 启用本包。这个组合包同时提供团队服务、工具与 Web 界面。Web Client loader 挂载 `/client` export;root Host export 不执行行为,本包也没有用户配置字段。 ### 检查并导航 roster -打开 panel 会调用 `agentTeams/view`。Roster row 展示持久 name、运行时 status、model 与 diagnostics。选择健康 teammate 时,系统刷新既有直接 child catalog,并打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address。History 与后续人类提示词继续使用稳定 addressed-subagent 会话路径;本包不会添加 Team 专用 address 字段。 +打开 panel 会调用 `agentTeams/view`。Roster row 展示持久 name、运行时 status、model 与 diagnostics。provisioning 和 running 成员使用共享 ongoing loading,idle 和 inactive 成员使用 idle 灰点,failed 成员使用 error 红点。选择健康 teammate 时,系统刷新既有直接 child catalog,并打开普通的 `{ parentSessionId, childSessionId, mode: 'continuable' }` address。History 与后续人类提示词继续使用稳定 addressed-subagent 会话路径;本包不会添加 Team 专用 address 字段。 ### 管理任务板 +可开始的 pending 任务使用 idle 灰点,被依赖阻塞的 pending 任务使用 warning 橙点,in-progress 任务使用 ongoing loading,completed 任务使用 done 绿点。 + 任务板展示 task identity、owner、blocker、readiness、提示性 write scope 与重叠 warning。用户可以通过 `agentTeams/createTask` 与 `agentTeams/updateTask` 创建、编辑、分配或取消分配、完成、重开和删除任务。每次 update 都发送当前显示的 revision,create 或 update rejection 都保留为显式 business result。 ----- @@ -61,7 +63,7 @@ Client export 挂载来自 [`@deepseek-ai/dsh-experimental-agent-team/remote`](. ## 进一步探索 -- [Agent Teams Web profile](../agent-team-web-profile/README.zh.md)——挂载本 Client plugin 的公开 opt-in bundle。 +- [Agent Teams bundle](../agent-team-profile/README.zh.md)——挂载本 Client plugin 的公开 opt-in bundle。 - [Agent Teams service](../agent-team/README.zh.md)——权威 roster、task 与 Remote 行为。 - [会话 UI](../../client/ui-conversation/README.zh.md)——稳定 header slot 与 addressed-subagent 导航表层。 - [实验性包](../README.zh.md)——孵化状态与发布规则。 diff --git a/packages/experimental/client-ui-agent-team/src/client/TeamAction.module.css b/packages/experimental/client-ui-agent-team/src/client/TeamAction.module.css index bd9f75be52..e8051dc387 100644 --- a/packages/experimental/client-ui-agent-team/src/client/TeamAction.module.css +++ b/packages/experimental/client-ui-agent-team/src/client/TeamAction.module.css @@ -55,6 +55,7 @@ border: 0; border-radius: 12px; background: var(--dsw-specific-menu); + backdrop-filter: var(--dsw-menu-backdrop-filter); --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); box-shadow: var(--dsw-elevation-prominent); } @@ -157,7 +158,10 @@ background: var(--dsw-alias-bg-base); } -.taskTitle span { +.taskState { + display: inline-flex; + align-items: center; + gap: 6px; margin-left: auto; color: var(--dsw-alias-label-tertiary); font-size: 11px; @@ -235,6 +239,9 @@ .notice, .error { + display: flex; + align-items: center; + gap: 6px; padding: 9px; font-size: 12px; } diff --git a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx index 93b6c6d858..250caa9da3 100644 --- a/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx +++ b/packages/experimental/client-ui-agent-team/src/client/TeamAction.tsx @@ -10,8 +10,9 @@ import type { } from '@deepseek-ai/dsh-experimental-agent-team/client' import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client' import { - IconCheckOutline14, IconCloseOutline16, IconEditOutline16, IconPlusOutline16, - IconRefreshOutline14, IconTrashOutline16, IconUserOutline16, StateDot, + IconCheckOutlineRegular, IconCloseOutlineRegular, IconEditOutlineRegular, IconPlusOutlineRegular, + IconRefreshOutlineRegular, IconTrashOutlineRegular, IconUserOutlineRegular, StateDot, + type StateDotState, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' @@ -95,6 +96,26 @@ function memberStatusKey(status: TeamRosterMember['status']): TeamKey { } } +function memberDotState(status: TeamRosterMember['status']): StateDotState { + switch (status) { + case 'running': + case 'provisioning': return 'ongoing' + case 'idle': + case 'inactive': return 'idle' + case 'failed': return 'error' + } +} + +function taskDotState(task: TeamTask): StateDotState { + switch (task.status) { + case 'pending': return task.ready ? 'idle' : 'warning' + case 'in_progress': return 'ongoing' + case 'completed': return 'done' + /* v8 ignore next -- Team views omit deleted task tombstones. */ + case 'deleted': return 'idle' + } +} + /** Render the live Team roster and compare-and-set task board. */ export function TeamAction({ sessionId, load, createTask, updateTask, openTeammate, t, @@ -255,7 +276,7 @@ export function TeamAction({ if (next) void refresh() }} > - + {t('trigger')} {teammates.length > 0 && {teammates.length}} @@ -264,15 +285,22 @@ export function TeamAction({
    {t('trigger')} + {loading && view !== null && ( + + )}
    - {error !== null &&
    {error}
    } - {loading && view === null &&
    {t('loading')}
    } + {error !== null && ( +
    {error}
    + )} + {loading && view === null && ( +
    {t('loading')}
    + )} {view !== null && ( <>
    @@ -289,7 +317,7 @@ export function TeamAction({ void openTeammate(sessionId, member).catch((reason: unknown) => { setError(String(reason)) }) }} > - + {member.name} {t(memberStatusKey(member.status))}{member.model === undefined ? '' : ` · ${t('model')}: ${member.model}`} @@ -303,7 +331,7 @@ export function TeamAction({

    {t('tasks')}

    {creating && ( @@ -334,7 +362,10 @@ export function TeamAction({
    {task.subject} - {t(statusKey(task.status))} + + + {t(statusKey(task.status))} +

    {task.description}

    @@ -365,14 +396,14 @@ export function TeamAction({ {task.status === 'in_progress' && ( + }}> {t('complete')} )} {task.status === 'completed' && ( + }}> {t('delete')}
    ))} diff --git a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx index 51d59f1732..7485435a35 100644 --- a/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx +++ b/packages/experimental/client-ui-agent-team/tests/team-action.client.spec.tsx @@ -145,6 +145,7 @@ describe('TeamAction', () => { const refresh = screen.getByRole('button', { name: zh.refresh }) fireEvent.click(refresh) + expect(screen.getByRole('status', { name: zh.loading })).toBeTruthy() fireEvent.click(refresh) newer.resolve({ ok: true, value: newestView }) expect(await screen.findByText('Newest task')).toBeTruthy() @@ -500,8 +501,15 @@ describe('TeamAction', () => { expect(await screen.findByText('provider failed')).toBeTruthy() expect(screen.getByText(zh.ready)).toBeTruthy() expect(screen.getByText(zh.blocked)).toBeTruthy() - expect(screen.getByRole('button', { name: /failed-worker/u }).disabled).toBe(true) - expect(screen.getByRole('button', { name: /provisioning-worker/u }).disabled).toBe(true) + const failedMember = screen.getByRole('button', { name: /failed-worker/u }) + const provisioningMember = screen.getByRole('button', { name: /provisioning-worker/u }) + expect(failedMember.disabled).toBe(true) + expect(failedMember.querySelector('[data-state="error"]')).not.toBeNull() + expect(provisioningMember.disabled).toBe(true) + expect(provisioningMember.querySelector('[data-state="ongoing"]')).not.toBeNull() + const tasks = [...document.querySelectorAll('article')] + expect(tasks.map(card => card.querySelector('[data-state]')?.getAttribute('data-state'))) + .toEqual(['idle', 'warning', 'done']) fireEvent.click(screen.getByRole('button', { name: /^worker运行中/u })) expect(await screen.findByText('Error: navigation failed')).toBeTruthy() diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 0caa5b38bf..98f9ec66c4 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -241,7 +241,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Stable owner currency delivered to a keyed Chat renderer. */\nexport interface ChatNodeOwnerProps {\n cwd?: string | undefined\n /** Open the current source file of a skill referenced by a sent message. */\n openSkill: (name: string) => void\n openFile: (path: string, options?: OpenFileOptions) => void\n inspectCall: (callId: ToolCallId) => void\n forkAt: (seq: number) => void\n /**\n * Session-authorized image loader, down-threaded from the Chat view so a\n * chat-node renderer can render the attachment presentation slot directly\n * with only the durable references plus this loader, instead of receiving a\n * rendering closure.\n */\n loadImage: MessageImageLoader\n renderMessageImages: RenderMessageImages\n fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined\n /** Turn-process state when this Node belongs to a projected Turn. */\n turnProcess?: TurnProcessOwnerProps | undefined\n}', + '/** Stable owner currency delivered to a keyed Chat renderer. */\nexport interface ChatNodeOwnerProps {\n cwd?: string | undefined\n /** Open the current source file of a skill referenced by a sent message. */\n openSkill: (name: string) => void\n openFile: (path: string, options?: OpenFileOptions) => void\n inspectCall: ((callId: ToolCallId) => void) | undefined\n forkAt: (seq: number) => void\n /**\n * Session-authorized image loader, down-threaded from the Chat view so a\n * chat-node renderer can render the attachment presentation slot directly\n * with only the durable references plus this loader, instead of receiving a\n * rendering closure.\n */\n loadImage: MessageImageLoader\n renderMessageImages: RenderMessageImages\n fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined\n /** Turn-process state when this Node belongs to a projected Turn. */\n turnProcess?: TurnProcessOwnerProps | undefined\n}', ], ownerPropsReferences: [ 'MarkdownFileMentions', @@ -1506,7 +1506,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Conversation View entries obtain their data from registered standard hooks. */\nexport interface ConvViewOwnerProps {\n /** Focus request addressed to the selected View. */\n viewRequest: import(\'./views.ts\').ConversationViewRequest | null\n /** Select a View and address one opaque focus identity to it. */\n openView: (view: string, focus: string) => void\n /** Acknowledge the current one-shot focus request. */\n completeViewRequest: () => void\n}', + '/** Conversation View entries obtain their data from registered standard hooks. */\nexport interface ConvViewOwnerProps {\n /** Open a tool call\'s inspector when an inspection target is available. */\n inspectCall: ((callId: string) => void) | undefined\n /** Focus request addressed to the selected View. */\n viewRequest: import(\'./views.ts\').ConversationViewRequest | null\n /** Select a View and address one opaque focus identity to it. */\n openView: (view: string, focus: string) => void\n /** Acknowledge the current one-shot focus request. */\n completeViewRequest: () => void\n}', ], ownerPropsReferences: [ 'ConversationViewRequest', @@ -1967,6 +1967,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ 'client-ui-chat TranscriptViewRow id \'transcript-view\'', 'client-ui-conversation EnterBehaviorRow id \'composer-enter\'', 'client-ui-permission-presets PermissionRow id \'permission\'', + 'client-ui-settings-general DeveloperToolsRow id \'developer-tools\'', 'client-ui-theme AppearanceRow id \'appearance\'', 'client-ui-theme FontSizeRow id \'font-size\'', ], @@ -2709,6 +2710,54 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.document\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.document\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', source: 'packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts:45', }, + { + key: 'sidebar.right.tab.document.action', + kind: 'keyed', + scope: 'session', + summary: 'Renderer-specific controls before the document toolbar\'s reload button.', + doc: 'Renderer-specific controls before the document toolbar\'s reload button.', + registerOptions: [ + { + name: 'key', + requirement: 'required', + type: 'string', + doc: 'Your cell key: the entry renders where the owner dispatches this exact key. Registering an already-occupied key replaces that occupant.', + }, + ], + ownerProps: [ + '/**\n * Ordinary file contents, or a request for the selected renderer to load its content.\n * Byte arrays are transient UI input, never persisted layout or Session data.\n */\nexport type DocumentContent =\n | { readonly kind: \'text\'; readonly text: string; readonly pages: readonly DocumentTextPage[]; readonly eof: boolean }\n | { readonly kind: \'bytes\'; readonly data: Uint8Array }\n | {\n readonly kind: \'renderer\'\n /** Changes on reload or implementation replacement; retained contents belong to one revision. */\n readonly revision: number\n /** Report the displayed source version; stale revisions cannot update the owner. @param version - loaded source version. */\n readonly loaded: (version: string) => void\n /** Cancel the current load and start a new revision. */\n readonly reload: () => void\n }', + ], + ownerPropsReferences: [ + 'DocumentTextPage', + ], + standardProps: [ + 'useResource: UseResource', + 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', + 'useSessions: UseSessions', + 'useSessionStatus: UseSessionStatus', + 'useSessionRetainInfo: UseSessionRetainInfo', + 'useWorkspaces: SnapshotSelectorHook', + 'useChat: UseChat', + 'useConversation: UseConversation', + 'useInput: SnapshotSelectorHook', + 'inputActions: InputActions', + 'useSession: SessionSnapshotSelector', + 'sessionId: SessionId', + 'useProjection: UseProjection', + 'useTrajectory: UseTrajectory', + ], + keyDomain: 'open: any string the owner dispatches (no compile-time key set), none are taken yet', + hookContext: 'UseSidebarRightTabInfo', + slotInject: '{ hooks: { tabInfo: SlotHookFactory<\'sidebar.right.tab.document\', UseSidebarRightTabInfo> } }', + declaredBy: 'an entry in \'sidebar.right.pane.tab\' (client-ui-sidebar-documentpreview), so it exists while that entry is mounted', + occupants: [ + 'client-ui-sidebar-documentpreview OfficeFontAction', + ], + replaceRisk: 'none', + example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.document.action\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.document.action\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', + source: 'packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts:57', + }, { key: 'sidebar.right.tab.document.office.pdf', kind: 'keyed', @@ -2755,7 +2804,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.document.office.pdf\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.document.office.pdf\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx:21', + source: 'packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx:20', }, { key: 'sidebar.right.tab.guide', diff --git a/packages/extensions/ui-cordis/README.i18n.yaml b/packages/extensions/ui-cordis/README.i18n.yaml index b28964efed..a164478ef6 100644 --- a/packages/extensions/ui-cordis/README.i18n.yaml +++ b/packages/extensions/ui-cordis/README.i18n.yaml @@ -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 packages/extensions/ui-cordis/README.md -README.md: 17fea955dae2ecc4b4549775d8f9176ee5eba1f5 -README.zh.md: 35726f2451a70586c0600789d1c27b15f2d779ea +README.md: 1f3677d48eda13cad18a24ed6021f30535db686a +README.zh.md: f3855ce6f75efc7510abdaff8d211cec167d5117 diff --git a/packages/extensions/ui-cordis/README.md b/packages/extensions/ui-cordis/README.md index 17fea955da..1f3677d48e 100644 --- a/packages/extensions/ui-cordis/README.md +++ b/packages/extensions/ui-cordis/README.md @@ -29,11 +29,11 @@ Compose this package with the Host and Client runners to render historical tool ### What the panel shows -A `sidebar.footer.action` seat shows a badge counting what runs plus what waits; opening it lists every definition with its run controls. The list is never filtered by session: the current session's rows group first, everyone else's stay listed below. Rows come from the host's current inventory and update whenever an announcement changes what exists. A pending run request whose definition the last read does not cover still gets a row, rendered from the request's own session, label, purpose, and identity. Each row shows two independent facts — what the host runs and what this page has loaded — so a reloaded page offers "load back into this page" before the global stop, while a host-only definition reads plainly running and offers the stop alone. The row also carries this page's last render failure inline, in the same place as a load failure: one is "it never loaded", the other "it loaded and then threw". +A `sidebar.footer.action` seat shows a badge counting what runs plus what waits; opening it lists every definition with its run controls. The list is never filtered by session: the current session's rows group first, everyone else's stay listed below. Rows come from the host's current inventory and update whenever an announcement changes what exists. A pending run request whose definition the last read does not cover still gets a row, rendered from the request's own session, label, purpose, and identity. Each row shows two independent facts — what the host runs and what this page has loaded — and maps them onto the shared status marker: idle uses idle, client loading and an action in flight use ongoing, a running definition uses done, approval waits use warning, and failures use error. A reloaded page offers "load back into this page" before the global stop, while a host-only definition reads plainly running and offers the stop alone. The row also carries this page's last render failure inline, in the same place as a load failure: one is "it never loaded", the other "it loaded and then threw". ### What the tool cards show -The `cordis_define` card is a record: the name and purpose the model wrote, the source it wrote, and whether the definition is running — no switch, no approval, and a pointer to the panel. The `cordis_run` card shows the mode, the plugin, package, and run ids, the outcome, and offers the package's own business view through the `tool.view.cordis` slot when the package registered one. `cordis_stop` and `cordis_undefine` render compact action rows. All cards render from the recorded call and result, so replay shows the same card. +The `cordis_define` card is a record: the name and purpose the model wrote, the source it wrote, and whether the definition is running — no switch, no approval, and a pointer to the panel. The `cordis_run` card shows the mode, the plugin, package, and run ids, the outcome, and offers the package's own business view through the `tool.view.cordis` slot when the package registered one. Define, run, stop, and undefine cards retain their ordinary business glyph when they fail or are interrupted, including a correlated activation failure after a successful run receipt. All cards render from the recorded call and result, so replay shows the same card. ### Boundaries to plan around diff --git a/packages/extensions/ui-cordis/README.zh.md b/packages/extensions/ui-cordis/README.zh.md index 35726f2451..f3855ce6f7 100644 --- a/packages/extensions/ui-cordis/README.zh.md +++ b/packages/extensions/ui-cordis/README.zh.md @@ -29,11 +29,11 @@ kind: "package-reference" ### 面板显示什么 -一个 `sidebar.footer.action` 席位显示角标,计数在跑数加待确认数;点开后列出每个定义及其运行控件。列表从不按会话过滤:当前会话的行置顶成组,其他会话的行仍在下方列出。行来自 host 的当前清单,并在公告改变「有哪些定义」时更新。上一次读取覆盖不到的待审批 run 请求仍然有行,直接用请求自带的会话、标签、用途与标识渲染。每一行显示两个独立事实——host 在跑什么与本页装载了什么——因此刷新后的页面会先给「装回本页」、再给全局 stop,而纯 host 定义的行如实读作运行中、只给 stop。该行还会把本页最后一次渲染失败就地显示,与装载失败共用同一个位置:一个是「它从来没装上」,另一个是「它装上了、然后抛了」。 +一个 `sidebar.footer.action` 席位显示角标,计数在跑数加待确认数;点开后列出每个定义及其运行控件。列表从不按会话过滤:当前会话的行置顶成组,其他会话的行仍在下方列出。行来自 host 的当前清单,并在公告改变「有哪些定义」时更新。上一次读取覆盖不到的待审批 run 请求仍然有行,直接用请求自带的会话、标签、用途与标识渲染。每一行显示两个独立事实——host 在跑什么与本页装载了什么——并把它们映射到共享状态标记:idle 使用 idle,客户端装载及正在执行的操作使用 ongoing,运行中的定义使用 done,等待批准使用 warning,失败使用 error。刷新后的页面会先给「装回本页」、再给全局 stop,而纯 host 定义的行如实读作运行中、只给 stop。该行还会把本页最后一次渲染失败就地显示,与装载失败共用同一个位置:一个是「它从来没装上」,另一个是「它装上了、然后抛了」。 ### 工具卡片显示什么 -`cordis_define` 卡片是一份记录:模型写下的 name 与 purpose、它写的源码,以及该定义是否在跑——没有开关、没有审批,只有一句指向面板的指引。`cordis_run` 卡片显示模式、插件标识、包标识与运行标识、结果,并在包注册了业务视图时经 `tool.view.cordis` slot 提供它。`cordis_stop` 与 `cordis_undefine` 渲染紧凑的动作行。所有卡片都渲染会话记录下的 call 与 result,因此回放显示同一张卡。 +`cordis_define` 卡片是一份记录:模型写下的 name 与 purpose、它写的源码,以及该定义是否在跑——没有开关、没有审批,只有一句指向面板的指引。`cordis_run` 卡片显示模式、插件标识、包标识与运行标识、结果,并在包注册了业务视图时经 `tool.view.cordis` slot 提供它。define、run、stop 与 undefine 卡片在失败或中断时继续保留普通业务图标,包括调用回执成功后发生的关联激活失败。所有卡片都渲染会话记录下的 call 与 result,因此回放显示同一张卡。 ### 需要规划的边界 diff --git a/packages/extensions/ui-cordis/src/client/CordisActionRow.tsx b/packages/extensions/ui-cordis/src/client/CordisActionRow.tsx index 2856dc90b3..61fe374573 100644 --- a/packages/extensions/ui-cordis/src/client/CordisActionRow.tsx +++ b/packages/extensions/ui-cordis/src/client/CordisActionRow.tsx @@ -1,7 +1,7 @@ /** Localized cards for `cordis_stop` and `cordis_undefine`. */ import { - IconInspectOutline12, IconStopFill16, IconTrashOutline16, StateDot, + IconInspectOutlineRegular, IconStopFillRegular, IconTrashOutlineRegular, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' @@ -21,18 +21,14 @@ export function CordisActionRow({ callId, toolName, block, inspect, t }: CordisA
    - {card.state === 'error' - ? - : card.state === 'stopped' - ? - : remove ? : } + {remove ? : } {t(remove ? 'row.removeTitle' : 'row.stopTitle')} {summary} {inspect !== undefined && ( )}
    diff --git a/packages/extensions/ui-cordis/src/client/CordisDefineRow.module.css b/packages/extensions/ui-cordis/src/client/CordisDefineRow.module.css index 7390f9a0de..c74fdc2654 100644 --- a/packages/extensions/ui-cordis/src/client/CordisDefineRow.module.css +++ b/packages/extensions/ui-cordis/src/client/CordisDefineRow.module.css @@ -1,8 +1,6 @@ /* Cordis definition card: DisclosureRow chrome (leading/chevron/title come from that primitive) plus this card's own trailing run switch and source body. - No running sweep: `cordis_define` only registers a definition in host memory, - so the call settles in the same turn it starts and the state dot carries the - whole signal. */ + No running sweep: `cordis_define` settles in the same turn it starts. */ .card { display: flex; diff --git a/packages/extensions/ui-cordis/src/client/CordisDefineRow.tsx b/packages/extensions/ui-cordis/src/client/CordisDefineRow.tsx index 0f2207c591..0c0806a1ef 100644 --- a/packages/extensions/ui-cordis/src/client/CordisDefineRow.tsx +++ b/packages/extensions/ui-cordis/src/client/CordisDefineRow.tsx @@ -1,8 +1,8 @@ /** Read-only `cordis_define` card with Host and Client source tabs. */ -import { useId, useState, type ReactNode } from 'react' +import { useId, useState } from 'react' import { - CodeBlock, DisclosureRow, IconCodeOutline16, IconInspectOutline12, StateDot, + CodeBlock, DisclosureRow, IconCodeOutlineRegular, IconInspectOutlineRegular, } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' @@ -34,14 +34,6 @@ function stateStatus(state: CordisToolState): CordisKey | null { } } -function leadingFor(state: CordisToolState): ReactNode { - switch (state) { - case 'error': return - case 'stopped': return - default: return - } -} - /** Render one immutable Package definition. */ export function CordisDefineRow({ callId, block, inspect, useInventory, useLoaded, t, @@ -88,7 +80,7 @@ export function CordisDefineRow({ rowClassName={css.row} titleClassName={css.title} chevronClassName={css.chevron} - icon={leadingFor(card.state)} + icon={} title={t('row.defineTitle')} open={open} expandable={expandable} @@ -160,7 +152,7 @@ export function CordisDefineRow({ {card.pluginId !== null &&
    {t('panel.hint')}
    } {inspect !== undefined && ( )} diff --git a/packages/extensions/ui-cordis/src/client/CordisPanel.module.css b/packages/extensions/ui-cordis/src/client/CordisPanel.module.css index 3b7764f5f1..8bedbf169e 100644 --- a/packages/extensions/ui-cordis/src/client/CordisPanel.module.css +++ b/packages/extensions/ui-cordis/src/client/CordisPanel.module.css @@ -83,6 +83,7 @@ left/bottom offsets are measured from the trigger before paint. */ .panel { position: fixed; + isolation: isolate; z-index: 30; display: flex; flex-direction: column; @@ -92,7 +93,6 @@ overflow: hidden; border: 0; border-radius: 12px; - background: var(--dsw-specific-menu); --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); box-shadow: var(--dsw-elevation-prominent); /* Only `.body` scrolls; the header remains fixed above it. */ @@ -100,6 +100,17 @@ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); } +.panel::before { + position: absolute; + z-index: -1; + inset: 0; + border-radius: inherit; + background: var(--dsw-specific-menu); + backdrop-filter: var(--dsw-menu-backdrop-filter); + content: ''; + pointer-events: none; +} + .header { flex: none; display: flex; @@ -199,36 +210,13 @@ flex: none; display: inline-flex; align-items: center; + gap: 4px; height: 20px; - padding: 0 6px; - border-radius: 10px; - background: var(--dsw-alias-button-ghost-active-fill); font-size: 11px; line-height: 20px; color: var(--dsw-alias-label-caption); } -.row[data-cordis-status='idle'] .rowStatus { - background: var(--dsw-alias-button-ghost-active-fill); - color: var(--dsw-alias-label-caption); -} - -.row[data-cordis-status='awaiting-approval'] .rowStatus, -.row[data-cordis-status='client-pending'] .rowStatus { - background: var(--dsw-alias-state-warn-tertiary); - color: var(--dsw-alias-state-warn-label); -} - -.row[data-cordis-status='failed'] .rowStatus { - background: var(--dsw-alias-interactive-bg-hover-danger); - color: var(--dsw-alias-state-error-primary); -} - -.row[data-cordis-status='running'] .rowStatus { - background: var(--dsw-alias-state-success-tertiary); - color: var(--dsw-alias-state-success-primary); -} - .rowDetail { display: flex; align-items: center; diff --git a/packages/extensions/ui-cordis/src/client/CordisPanel.tsx b/packages/extensions/ui-cordis/src/client/CordisPanel.tsx index 17ca34848e..739dfd16f7 100644 --- a/packages/extensions/ui-cordis/src/client/CordisPanel.tsx +++ b/packages/extensions/ui-cordis/src/client/CordisPanel.tsx @@ -3,8 +3,9 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react' import type { ButtonHTMLAttributes, ReactNode } from 'react' import { - IconCheckOutline16, IconCloseOutline16, IconCordisPluginOutline14, IconPlayOutline16, - IconStopFill16, IconTrashOutline16, Tooltip, useDismissOnOutsidePointer, + IconCheckOutlineRegular, IconCloseOutlineRegular, IconCordisPluginOutlineRegular, IconPlayOutlineRegular, + IconStopFillRegular, IconTrashOutlineRegular, StateDot, Tooltip, useDismissOnOutsidePointer, + type StateDotState, } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' @@ -73,6 +74,17 @@ function visiblePanelStatus( return cordisVisibleStatus(listed, listed.activeRun.packageId, loaded) } +function panelDotState(status: PanelStatus, busy: boolean): StateDotState { + if (busy) return 'ongoing' + switch (status) { + case 'idle': return 'idle' + case 'client-pending': return 'ongoing' + case 'running': return 'done' + case 'awaiting-approval': return 'warning' + case 'failed': return 'error' + } +} + function blockingFirst(rows: readonly RowView[]): readonly RowView[] { return [ ...rows.filter(row => row.activity?.phase === 'awaiting-approval'), @@ -96,8 +108,8 @@ function RowAction({ label, children, ...props }: { function DoubleCheckIcon() { return ( - - + + ) } @@ -247,7 +259,10 @@ export function CordisPanel({
    {pluginId} {name} - {t(STATUS_LABELS[status])} + + + {t(STATUS_LABELS[status])} +
    {listed !== undefined && listed.packages.length > 1 && selectedPackageId !== undefined && (
    @@ -478,7 +493,7 @@ export function CordisPanel({ aria-expanded={open} onClick={() => { setOpen(value => !value) }} > - + {wide && ( <> {t('panel.trigger')} diff --git a/packages/extensions/ui-cordis/src/client/CordisRunRow.tsx b/packages/extensions/ui-cordis/src/client/CordisRunRow.tsx index 10e44ee689..44cfbccc53 100644 --- a/packages/extensions/ui-cordis/src/client/CordisRunRow.tsx +++ b/packages/extensions/ui-cordis/src/client/CordisRunRow.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { - IconCodeOutline16, IconInspectOutline12, StateDot, + IconCodeOutlineRegular, IconInspectOutlineRegular, } from '@deepseek-ai/dsh-client-ui-primitives' import type { InjectFace, PropsLocale, PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' @@ -81,7 +81,6 @@ export function CordisRunRow({ const summary = card.errorSummary ?? (card.pluginId === null ? callId : `${card.pluginId}${card.packageId === null ? '' : ` · ${card.packageId}`}`) const showBusiness = reading === 'running' && key !== null - return (
    - - {card.state === 'error' - ? - : card.state === 'stopped' - ? - : } - + {t(card.mode === 'update' ? 'row.updateTitle' : 'row.runTitle')} {summary} {status} {inspect !== undefined && ( )}
    diff --git a/packages/extensions/ui-cordis/tests/status-icons.client.spec.tsx b/packages/extensions/ui-cordis/tests/status-icons.client.spec.tsx new file mode 100644 index 0000000000..3d5a8920f1 --- /dev/null +++ b/packages/extensions/ui-cordis/tests/status-icons.client.spec.tsx @@ -0,0 +1,126 @@ +// @vitest-environment jsdom + +import { cleanup, render } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { ToolResultNode } from '@deepseek-ai/dsh-client-ui-chat/client' +import { CordisActionRow } from '../src/client/CordisActionRow.tsx' +import { CordisDefineRow } from '../src/client/CordisDefineRow.tsx' +import { CordisRunRow } from '../src/client/CordisRunRow.tsx' +import { zh } from '../src/client/locales.ts' + +afterEach(cleanup) + +const t = ((key: keyof typeof zh) => zh[key]) as Parameters[0]['t'] + +function failed(name: string): ToolResultNode { + return { + kind: 'tool-result', seq: 2, time: 2, callId: `call-${name}`, + call: { name, argsRaw: '{}' }, callTime: 1, + content: [{ type: 'text', text: 'failed' }], isError: true, subCalls: [], + } +} + +const useValue = (value: T) => (selector: (snapshot: T) => Selected): Selected => selector(value) + +describe('Cordis tool failure icons', () => { + it('retains the code glyph for define and run failures', () => { + const common = { + openFile: vi.fn(), + inspect: undefined, + t, + useInventory: useValue({ rows: [], removed: new Set(), read: true }), + useLoaded: useValue([]), + } + const define = render([0]} />) + expect(define.container.querySelector('[data-tool="cordis_define"] [data-disclosure-row] > :first-child svg')).not.toBeNull() + expect(define.container.querySelector('[data-tool="cordis_define"] [data-state]')).toBeNull() + define.unmount() + + const run = render([0]} />) + expect(run.container.querySelector('[data-tool="cordis_run"] > div:first-child > span:first-child svg')).not.toBeNull() + expect(run.container.querySelector('[data-tool="cordis_run"] [data-state]')).toBeNull() + }) + + it('retains the code glyph for interrupted define and run calls', () => { + const interrupted = (name: string): ToolResultNode => ({ + ...failed(name), + error: { name: 'InterruptedError', code: 'interrupted' }, + }) + const common = { + openFile: vi.fn(), inspect: undefined, t, + useInventory: useValue({ rows: [], removed: new Set(), read: true }), + useLoaded: useValue([]), + } + const define = render([0]} />) + expect(define.container.querySelector('[data-tool="cordis_define"] [data-disclosure-row] > :first-child svg')).not.toBeNull() + expect(define.container.querySelector('[data-tool="cordis_define"] [data-state]')).toBeNull() + define.unmount() + + const run = render([0]} />) + expect(run.container.querySelector('[data-tool="cordis_run"] > div:first-child > span:first-child svg')).not.toBeNull() + expect(run.container.querySelector('[data-tool="cordis_run"] [data-state]')).toBeNull() + }) + + it('uses the dashed code glyph when a successful run receipt later reports activation failure', () => { + const pluginId = 'plugin-1' + const packageId = 'package-1' + const pluginRunId = 'run-1' + const block: ToolResultNode = { + kind: 'tool-result', seq: 3, time: 3, callId: 'call-cordis_run', callTime: 2, + call: { name: 'cordis_run', argsRaw: JSON.stringify({ pluginId, packageId, mode: 'run' }) }, + content: [], isError: false, subCalls: [], meta: { pluginId, packageId, pluginRunId }, + } + const view = render([0]} />) + expect(view.container.querySelector('[data-cordis-status="failed"]')).not.toBeNull() + expect(view.container.querySelector('[data-state="ok"] svg')).not.toBeNull() + }) + + it.each(['cordis_stop', 'cordis_undefine'] as const)('%s retains its action icon on failure', (toolName) => { + const view = render([0]} />) + expect(view.container.querySelector('[data-state="error"] svg')).not.toBeNull() + expect(view.container.querySelector('[data-state="error"] [data-state]')).toBeNull() + }) + + it.each(['cordis_stop', 'cordis_undefine'] as const)('%s retains its action icon when interrupted', (toolName) => { + const block: ToolResultNode = { + ...failed(toolName), + error: { name: 'InterruptedError', code: 'interrupted' }, + } + const view = render([0]} />) + expect(view.container.querySelector('[data-state="stopped"] svg')).not.toBeNull() + expect(view.container.querySelector('[data-state="stopped"] [data-state]')).toBeNull() + }) +}) diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index f3498bf0c3..cc43d12297 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -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 packages/llm/llm-deepseek/README.md -README.md: 18f1b3de48d554dbc2225cfcadf4900f8bb17b09 -README.zh.md: 38d288d233a84c3bbf8170d560ea42d372fab3fc +README.md: 33a76a316fd6f89e5d011ab3aa7a933331bd1e7b +README.zh.md: dbc7282140268985f215efcac0b4e602cd5b0e44 diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 18f1b3de48..33a76a316f 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -171,7 +171,7 @@ Read these pages when the package-level contract is not enough. They move from t #### What the model sees -The selected DeepSeek model receives the harness system prompt, message history, tool schemas, stop sequences, and call config (`maxTokens`, `reasoningEffort`, `temperature`) without adapter-authored prompt prose. Provider-specific request-extension fields remain outside model input. The vision model normally receives retained user and tool-result images as Files API references beside attachment handles and request-preview dimensions. It also receives a normalized-object path when the current execution filesystem maps the attachment provider's host object; the descriptor marks this copy read-only and warns that normalization may have resized or re-encoded the upload. A Files resolution failure sends all retained images as inline base64 instead, and an over-budget older image keeps the access resolved for that request in its placeholder. Reasoning content from a prior assistant turn is passed back verbatim, whether or not that turn called a tool. Messages sends `{}` for historical tool arguments that are malformed JSON or are not objects. Call ids, tool names, and results remain intact; the original arguments stay in the Session log. This silent fallback also applies after switching from Chat Completions. Newly generated Messages tool arguments still require valid JSON objects. +The selected DeepSeek model receives the harness system prompt, message history, tool schemas, stop sequences, and call config (`maxTokens`, `reasoningEffort`, `temperature`) without adapter-authored prompt prose. Provider-specific request-extension fields remain outside model input. The vision model normally receives retained user and tool-result images as Files API references beside attachment handles and request-preview dimensions. It also receives a normalized-object path when the current execution filesystem maps the attachment provider's host object; the descriptor marks this copy read-only and warns that normalization may have resized or re-encoded the upload. A Files resolution failure sends all retained images as inline base64 instead, and an over-budget older image keeps the access resolved for that request in its placeholder. Reasoning content from a prior assistant turn is passed back verbatim, whether or not that turn called a tool. Messages sends `{}` for historical tool arguments that are malformed JSON or are not objects. This argument fallback preserves call ids, tool names, and tool results; the original arguments stay in the Session log. This silent fallback also applies after switching from Chat Completions. Newly generated Messages tool arguments still require valid JSON objects. Messages omits `reasoning` and `tool-call` blocks inside user messages and tool results; omission of these two block types matches Chat Completions and the pi-ai adapter. This also permits replay of saved subagent notices containing assistant output; the original Session content remains intact. Empty user messages are skipped after conversion, while empty tool results retain their call ids and error flags. Other unsupported input blocks still fail with `UNSUPPORTED_CONTENT`. #### Token effect @@ -207,7 +207,8 @@ These limits define where the adapter stops and future work begins. They are cur - **A settings `models` list replaces the composition list wholesale** — settings-layer merging is per-field, and arrays are one field; per-entry catalog merging would need a keyed shape. - **`tool_choice` is not mapped** — not part of the core vocabulary (shared with the pi-ai twin). - **Requests use raw `fetch`, not `@cordisjs/plugin-http`** — no shared proxy or interception configuration. -- **Plugin-added content block types are skipped** — core text and supported image blocks are serialized, and empty tool output crosses the wire as the literal `(no output)`. +- **Chat Completions skips plugin-added content block types** — core text and supported image blocks are serialized, and empty tool output crosses the wire as the literal `(no output)`. +- **Messages in-history system updates require a retained user or tool-result turn** — if all user input after an update is omitted and the preceding wire turn is assistant, serialization fails with `UNSUPPORTED_CONTENT` before the next assistant or at the end of the request. Text or an empty tool result can retain that turn. Moving the update to an earlier turn is not supported; the [input-history decision](../../../.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.md) records the ordering constraint. - **Images are input-only durable attachments** — direct external URLs and assistant image output are not supported; DeepSeek input normally uses the Files API and uses inline base64 only for per-request recovery. - The default catalog pre-registers `deepseek-flash` and its text/image and in-history capabilities without probing gateway availability. Requests can fail with `INVALID_REQUEST` until the gateway enables the id. With `DEEPSEEK_API_KEY` and a supporting gateway configured, `DEEPSEEK_FLASH_E2E=1` enables the Chat Completions check in [this package's e2e suite](tests/adapter.e2e.ts). - The [Messages system-update e2e checks](tests/messages/adapter.e2e.ts) require `DEEPSEEK_IN_HISTORY_MODEL` to name a supported model, such as `deepseek-flash`, and run with `high` effort. They skip when that variable is unset or empty; ordinary `off` text checks remain enabled with credentials. Known instruction-following instability with thinking disabled makes these system-update checks unsuitable for `off`. diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 38d288d233..dbc7282140 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -171,7 +171,7 @@ Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留 #### 模型看到什么 -所选 DeepSeek 模型会收到 harness 系统提示词、消息历史、工具 schema、停止序列与调用配置(`maxTokens`、`reasoningEffort`、`temperature`),不包含适配器撰写的提示词散文。提供方专用请求扩展字段留在模型输入之外。视觉模型通常接收 Files API 引用形式的用户与工具结果图片,其旁带附件句柄和请求预览尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,它还会收到规范化对象路径;描述符会把该副本标记为只读,并警告规范化可能缩放或重新编码上传内容。Files 解析失败时,全部保留图片改用内联 base64;超出预算的较旧图片则在占位文本中保留当前请求已解析的访问方式。此前 assistant 轮次的推理内容会原样传回,无论该轮次是否调用了工具。 对于非法 JSON 或非对象的历史工具参数,Messages 发送 `{}`。调用 ID、工具名和结果保持不变,原始参数仍保留在 Session 日志中。从 Chat Completions 切换后也适用此静默兜底。新生成的 Messages 工具参数仍须是有效 JSON 对象。 +所选 DeepSeek 模型会收到 harness 系统提示词、消息历史、工具 schema、停止序列与调用配置(`maxTokens`、`reasoningEffort`、`temperature`),不包含适配器撰写的提示词散文。提供方专用请求扩展字段留在模型输入之外。视觉模型通常接收 Files API 引用形式的用户与工具结果图片,其旁带附件句柄和请求预览尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,它还会收到规范化对象路径;描述符会把该副本标记为只读,并警告规范化可能缩放或重新编码上传内容。Files 解析失败时,全部保留图片改用内联 base64;超出预算的较旧图片则在占位文本中保留当前请求已解析的访问方式。此前 assistant 轮次的推理内容会原样传回,无论该轮次是否调用了工具。 对于非法 JSON 或非对象的历史工具参数,Messages 发送 `{}`。此参数兜底保留调用 ID、工具名和工具结果;原始参数仍保留在 Session 日志中。从 Chat Completions 切换后也适用此静默兜底。新生成的 Messages 工具参数仍须是有效 JSON 对象。 Messages 会省略用户消息和工具结果中的 `reasoning` 与 `tool-call` 块;对这两类块的省略行为与 Chat Completions 和 pi-ai 适配器一致。这也允许回放包含助手输出的已保存子 Agent 通知;原始 Session 内容保持完整。转换后的空用户消息会被跳过,空工具结果则保留调用 id 和错误标记。其他不支持的输入块仍会以 `UNSUPPORTED_CONTENT` 失败。 #### Token 影响 @@ -207,7 +207,8 @@ loop 保留的响应块会追加到下一个请求,并保留其更早的可复 - **设置中的 `models` 列表会整体替换组合列表**——设置层按字段合并,数组只算一个字段;按条目合并目录需要带键的形状。 - **不映射 `tool_choice`**——不属于核心词汇(与 pi-ai 孪生共享)。 - **请求使用原始 `fetch`,而非 `@cordisjs/plugin-http`**——没有共享代理或拦截配置。 -- **跳过插件新增的内容块类型**——核心文本与受支持图片块会被序列化,空工具输出以字面量 `(no output)` 过线。 +- **Chat Completions 跳过插件新增的内容块类型**——核心文本与受支持图片块会被序列化,空工具输出以字面量 `(no output)` 过线。 +- **Messages 历史内 system 更新需要保留用户或工具结果轮次**——若更新后的全部用户输入都被省略,且前一个协议轮次是 assistant,序列化会在下一个 assistant 之前或请求结束处以 `UNSUPPORTED_CONTENT` 失败。文本或空工具结果可以保留该轮次。不支持将更新移到更早的轮次;[输入历史决策](../../../.agents/notes/implemented/bug-fix/2026-09-18-messages-input-history-compatibility.zh.md)记录了排序约束。 - **图片是仅用于输入的持久附件**——不支持直接外部 URL 与 assistant 图片输出;DeepSeek 输入通常使用 Files API,仅在单次请求恢复时使用内联 base64。 - 默认目录预注册 `deepseek-flash` 及其文本、图片和历史内更新能力,不探测网关可用性。网关开放该 ID 前,请求可能以 `INVALID_REQUEST` 失败。配置 `DEEPSEEK_API_KEY` 和支持该 ID 的网关后,设置 `DEEPSEEK_FLASH_E2E=1` 可启用[本包 e2e 测试文件](tests/adapter.e2e.ts)中的 Chat Completions 协议验证。 - [Messages system 更新 e2e](tests/messages/adapter.e2e.ts) 要求通过 `DEEPSEEK_IN_HISTORY_MODEL` 指定支持该能力的模型,例如 `deepseek-flash`,并使用 `high` 思考强度。该变量未设置或为空时跳过;普通 `off` 文本检查仍在有凭据时运行。关闭思考时已知的指令遵循不稳定,使这些 system 更新检查不适合使用 `off`。 diff --git a/packages/llm/llm-deepseek/src/protocols/messages/serialize.ts b/packages/llm/llm-deepseek/src/protocols/messages/serialize.ts index a7cd5bcd89..41d3f3bb4a 100644 --- a/packages/llm/llm-deepseek/src/protocols/messages/serialize.ts +++ b/packages/llm/llm-deepseek/src/protocols/messages/serialize.ts @@ -39,6 +39,8 @@ function assistant(message: Message, model: string, onReplayDegrade?: (reason: s } /** Serialize one complete request using already prepared image bytes. + * User and tool-result content omits reasoning and tool-call blocks. + * Empty user messages are skipped; empty tool results retain their call ids. * @param options - provider-neutral request. * @param connection - validated defaults and thinking policy. * @param history - image-projected history with complete system snapshots; durable messages remain unchanged. @@ -58,6 +60,7 @@ export function serialize( const inHistory = model?.systemPromptUpdate === 'in-history' const input = (blocks: readonly ContentBlock[]): WireInput[] => blocks.flatMap((block): WireInput[] => { if (block.type === 'text') return block.text ? [{ type: 'text', text: block.text }] : [] + if (block.type === 'reasoning' || block.type === 'tool-call') return [] if (block.type !== 'image') return unsupported(`user/tool-result content ${block.type}`) const version = images.get(block.attachment.attachmentId) if (version === undefined) throw new LlmError('DeepSeek Messages request image is missing', 'INVALID_REQUEST') @@ -98,6 +101,7 @@ export function serialize( if (block.type !== 'tool-result') return input([block]) return [{ type: 'tool_result', tool_use_id: block.toolCallId, content: input(block.content), ...block.isError === undefined ? {} : { is_error: block.isError } }] }) + if (message.role === 'user' && content.length === 0) continue const previous = messages.at(-1) if (previous?.role === message.role) previous.content.push(...content) else messages.push({ role: message.role, content }) diff --git a/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts b/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts index 6af02de01b..d6752df872 100644 --- a/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts @@ -15,7 +15,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { AttachmentId } from '@deepseek-ai/dsh-attachment' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' -import LlmRuntime, { createAssistantMessage, createSystemMessage } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createAssistantMessage, createSystemMessage, createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import type { Message } from '@deepseek-ai/dsh-llm' import { credentialRef } from '@deepseek-ai/dsh-credentials' import LocalCredentials from '@deepseek-ai/dsh-credentials-local' @@ -53,6 +53,37 @@ async function send(agent: Agent, text: string) { } describe('direct Messages HTTP', () => { + it('continues through Messages with assistant blocks in saved user history', async () => { + const http = await endpoint() + const notice = createUserMessage({ source: { kind: 'plugin', plugin: 'saved-notice' }, content: [ + { type: 'text', text: 'Background subagent finished.' }, + { type: 'reasoning', text: 'child reasoning' }, + { type: 'tool-call', id: ToolCallId('child-call'), name: 'read', arguments: '{}' }, + { type: 'text', text: 'Its closing message: answer.' }, + ] }) + const empty = createUserMessage({ source: { kind: 'user' }, content: [{ type: 'reasoning', text: 'no closing text' }] }) + const history = [notice, empty] + const saved = JSON.stringify(history) + const llm = adapter({ baseURL: http.url }) + const first = await assemble(llm.stream(options({ messages: history }))) + const second = await assemble(llm.stream(options({ messages: [...history, first.message, user('continue')] }))) + + expect(first.assembler.finish.kind).toBe('stop') + expect(second.assembler.finish.kind).toBe('stop') + expect(http.requests).toHaveLength(2) + const wireNotice = { role: 'user', content: [ + { type: 'text', text: 'Background subagent finished.' }, + { type: 'text', text: 'Its closing message: answer.' }, + ] } + expect(http.requests[0]?.body.messages).toEqual([wireNotice]) + expect(http.requests[1]?.body.messages).toEqual([ + wireNotice, + { role: 'assistant', content: [{ type: 'text', text: 'Hello 世界' }] }, + { role: 'user', content: [{ type: 'text', text: 'continue' }] }, + ]) + expect(JSON.stringify(history)).toBe(saved) + }) + it('continues without a diagnostic callback when replay metadata is unusable', async () => { const http = await endpoint() const message = createAssistantMessage({ content: [{ type: 'text', text: 'Remember 731.' }], source: { diff --git a/packages/llm/llm-deepseek/tests/messages/fixtures/input-history.ts b/packages/llm/llm-deepseek/tests/messages/fixtures/input-history.ts new file mode 100644 index 0000000000..dbfbad61bf --- /dev/null +++ b/packages/llm/llm-deepseek/tests/messages/fixtures/input-history.ts @@ -0,0 +1,37 @@ +/** Replay persisted user input carrying assistant-only content through Messages. */ +import assert from 'node:assert/strict' +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-agent' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' +import { resolveAdapterOptions } from '../../../src/config.ts' +import { serialize } from '../../../src/protocols/messages/serialize.ts' + +export const name = 'messages-input-history-snapshot' +export const inject = ['llm'] + +/** Record legacy-style notices and verify their provider representation. */ +export function apply(ctx: Context): void { + ctx.on('agent/pre-step', async (_input, next) => { + const decision = await next() + if (decision.kind !== 'enter') return decision + const source = { kind: 'plugin' as const, plugin: name } + return { ...decision, messages: [ + ...decision.messages, + createUserMessage({ source, content: [ + { type: 'text', text: 'Background subagent finished. Its closing message: Saved child answer.' }, + { type: 'reasoning', text: 'unforwarded-child-thought' }, + { type: 'tool-call', id: ToolCallId('unforwarded-child-call'), name: 'bash', arguments: '{}' }, + ] }), + createUserMessage({ source, content: [{ type: 'reasoning', text: 'unforwarded-empty-notice' }] }), + ] } + }) + const connection = resolveAdapterOptions({}) + ctx.on('llm/stream', (options, next) => { + const request = serialize(options, connection, options.messages, new Map(), () => undefined) + const payload = JSON.stringify(request.messages) + assert.ok(payload.includes('Saved child answer.')) + assert.ok(!payload.includes('unforwarded-')) + assert.ok(request.messages.every(message => message.content.length > 0)) + return next() + }) +} diff --git a/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts b/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts index 6e5d755202..eb1c6bccdf 100644 --- a/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts +++ b/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts @@ -65,6 +65,39 @@ describe('Messages request conversion', () => { expect(() => nativeBody([user(), assistant([call(), call('b')]), update, result()])).toThrow(/immediate results/) }) + it.each([[], [{ type: 'reasoning', text: 'child reasoning' }], [call('child-call')]] satisfies ContentBlock[][])( + 'rejects native system updates when their user input is omitted %#', (...content) => { + const empty = createMessage({ role: 'user', source: { kind: 'user' }, content }) + const history = [user(), assistant([{ type: 'text', text: 'answer' }]), createSystemMessage('updated', 'test'), empty] + const saved = JSON.stringify(history) + for (const messages of [history, [...history, assistant([{ type: 'text', text: 'next answer' }])]]) { + expect(() => nativeBody(messages)).toThrow(expect.objectContaining({ + code: 'UNSUPPORTED_CONTENT', + message: 'DeepSeek Messages cannot represent system update without a preceding user or tool-result turn', + })) + } + expect(JSON.stringify(history)).toBe(saved) + }, + ) + + it('keeps native system updates after retained text or tool results beside omitted user input', () => { + const reasoning: ContentBlock = { type: 'reasoning', text: 'child reasoning' } + const empty = createMessage({ role: 'user', source: { kind: 'user' }, content: [reasoning] }) + const update = createSystemMessage('updated', 'test') + for (const [previous, retained, expected] of [ + [assistant([{ type: 'text', text: 'answer' }]), user('continue'), [{ type: 'text', text: 'continue' }]], + [assistant([call()]), result('a', [reasoning]), [{ type: 'tool_result', tool_use_id: 'a', content: [], is_error: false }]], + ] as const) { + const history = [user(), previous, update, empty, retained, assistant([{ type: 'text', text: 'done' }])] + const saved = JSON.stringify(history) + const request = nativeBody(history) + expect(request.messages.map(message => message.role)).toEqual(['user', 'assistant', 'user', 'system', 'assistant']) + expect(request.messages[2]?.content).toEqual(expected) + expect(request.messages[3]?.content).toEqual([{ type: 'text', text: 'updated' }]) + expect(JSON.stringify(history)).toBe(saved) + } + }) + it('groups parallel results before ordinary text and keeps tool failure content', () => { const messages = [user(), assistant([call(), call('b')]), user('follow-up'), result(), createToolResultMessage({ callId: ToolCallId('b'), content: [{ type: 'text', text: 'permission denied' }], isError: true })] expect(body(messages).messages).toEqual([ @@ -86,6 +119,53 @@ describe('Messages request conversion', () => { expect(body([assistant([call()]), minimal]).messages[1]?.content[0]).toEqual({ type: 'tool_result', tool_use_id: 'a', content: [] }) }) + it('omits assistant-only blocks from user input while preserving text and durable content', () => { + const history = [createMessage({ role: 'user', source: { kind: 'user' }, content: [ + { type: 'text', text: 'Background subagent finished.\n' }, + { type: 'reasoning', text: 'child reasoning' }, + call('child-call'), + { type: 'text', text: ' child answer ' }, + ] })] + const saved = JSON.stringify(history) + + expect(body(history).messages).toEqual([{ role: 'user', content: [ + { type: 'text', text: 'Background subagent finished.\n' }, + { type: 'text', text: ' child answer ' }, + ] }]) + expect(JSON.stringify(history)).toBe(saved) + }) + + it('omits assistant-only blocks inside tool results while retaining calls, errors and empty results', () => { + const reasoning: ContentBlock = { type: 'reasoning', text: 'tool reasoning' } + const history = [assistant([reasoning, call(), call('b')]), + createToolResultMessage({ callId: ToolCallId('a'), content: [reasoning, call('nested-call'), { type: 'text', text: ' result\n' }], isError: true }), + result('b', [reasoning, call('another-nested-call')]), + ] + const saved = JSON.stringify(history) + + expect(body(history).messages).toEqual([ + { role: 'assistant', content: [ + { type: 'thinking', thinking: 'tool reasoning' }, + ...['a', 'b'].map(id => ({ type: 'tool_use', id, name: 'read', input: { path: 'a' } })), + ] }, + { role: 'user', content: [ + { type: 'tool_result', tool_use_id: 'a', content: [{ type: 'text', text: ' result\n' }], is_error: true }, + { type: 'tool_result', tool_use_id: 'b', content: [], is_error: false }, + ] }, + ]) + expect(JSON.stringify(history)).toBe(saved) + }) + + it.each([ + [], [{ type: 'reasoning', text: 'child reasoning' }], [call('child-call')], + ] satisfies ContentBlock[][])('omits empty user input after conversion %#', (...content) => { + const empty = createMessage({ role: 'user', source: { kind: 'user' }, content }) + expect(body([empty, user(), assistant([{ type: 'text', text: 'answer' }]), empty]).messages).toEqual([ + { role: 'user', content: [{ type: 'text', text: 'hello' }] }, + { role: 'assistant', content: [{ type: 'text', text: 'answer' }] }, + ]) + }) + it('collects leading system text and maps tools, stop sequences and explicit output cap', () => { const system = createMessage({ role: 'system', source: { kind: 'plugin', plugin: 'test' }, content: [{ type: 'text', text: 'instructions' }] }) expect(body([system, user()], { system: 'top', maxTokens: 123, stop: ['END'], tools: [{ name: 'read', description: 'Read a file', parameters: { type: 'object' } }] })).toMatchObject({ @@ -313,7 +393,8 @@ describe('Messages images', () => { await expect(prepareImages([assistant([image])], connection, model, attachments, access, signal)).rejects.toMatchObject({ code: 'UNSUPPORTED_CONTENT' }) expect(() => body([result('a', [image])])).toThrow(/image/) expect(() => body([assistant([image])])).toThrow(/assistant/) - expect(() => body([result('a', [{ type: 'reasoning', text: 'bad' }])])).toThrow(/user/) + expect(() => body([result('a', [{ type: 'tool-result', toolCallId: ToolCallId('nested'), content: [] }])])) + .toThrow(/user\/tool-result content tool-result/) expect(() => serialize(options({ model }), connection, [result('a', [image])], new Map([[ref.attachmentId, version]]), access, undefined, new Map())) .toThrow(/request file id is missing/) }) diff --git a/packages/session-query/session-log-export/src/client/HeaderAction.tsx b/packages/session-query/session-log-export/src/client/HeaderAction.tsx index cb20cb9f9d..bee6e4ceaf 100644 --- a/packages/session-query/session-log-export/src/client/HeaderAction.tsx +++ b/packages/session-query/session-log-export/src/client/HeaderAction.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import type { ReactNode } from 'react' -import { IconDownloadOutline16, IconEllipsisOutline16, Menu } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconDownloadOutlineRegular, IconEllipsisOutlineRegular, Menu } from '@deepseek-ai/dsh-client-ui-primitives' import { SessionLogDownloadDialog, type SessionLogDownloadDialogProps } from './Dialog.tsx' import css from './HeaderAction.module.css' @@ -22,7 +22,7 @@ export function SessionLogDownloadHeaderAction(props: SessionLogDownloadDialogPr align="end" dense onClose={() => { setOpen(false) }} - items={[{ id: 'download', label: t('menu.download'), icon: , disabled: busy }]} + items={[{ id: 'download', label: t('menu.download'), icon: , disabled: busy }]} onSelect={() => { setOpen(false) void request(sessionId) @@ -37,7 +37,7 @@ export function SessionLogDownloadHeaderAction(props: SessionLogDownloadDialogPr aria-busy={busy} onClick={() => { setOpen(value => !value) }} > - + )} /> diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 0c480c9342..eb36df9491 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -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 packages/subagent/subagent/README.md -README.md: 4634f0a33f71709668732b7070a2bccded3db5c0 -README.zh.md: d3d8a597f23153b0c82e663d98142a9966f48c22 +README.md: c283ec0e170979ced1b735e5d22e5e2345eb6bad +README.zh.md: 37126234b64b3b45c2788db7319945ea2388c99f diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 4634f0a33f..c283ec0e17 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -132,6 +132,7 @@ Read these pages when the package-level contract is not enough. They move from t - [In-process spawn backend](../subagent-spawn-in-process/README.md) — the simplest provider to compose. - [Auto review](../../experimental/auto-review/README.md) — the current-session authorization mode inherited only by in-process DSH children. - [Out-of-process ACP backend](../subagent-acp/README.md) — children with their own runtime over the Agent Client Protocol. +- [DeepSeek input conversion](../../llm/llm-deepseek/README.md#model-experience) — provider replay rules for saved settlement notices. - [tool-subagent-control README](../tool-subagent-control/README.md) — the follow-up, interrupt, and listing surface. ----- @@ -186,7 +187,6 @@ These limits define when the seam is a poor fit or needs special operational car - **Wake gap during cancellation convergence** — a follow-up accepted after an interrupt signal but before the driver becomes idle stays queued until another waking send. - **Pending injected context retains an Activation** — settlement conservatively treats every Inbox occurrence as unfinished. Context parked after the Agent becomes idle keeps the child and its live ancestors resident until a waking delivery claims it, a queue mutation removes it, or manager teardown discards it. - **Process-local residency** — the Activation inbox and ownership graph do not coordinate two harness processes; concurrent access to one persistence store needs a durable mailbox and cross-process lease protocol. -- **Saved settlement notices are not rewritten** — a saved user-role notice containing reasoning still fails DeepSeek Messages serialization while it remains in the parent's request history. - **No replay of accepted-but-unlogged messages** — a crash can lose an accepted prompt that never reached the child's session log; the lost message is not replayed automatically. - **No durable parent mailbox** — child-to-parent messages require a resident continuable child and live direct parent, and provide acceptance identity rather than exactly-once delivery. - **Lifecycle events are observe-only** — a run-affecting `subagent/end` continuation or decision API waits for a concrete consumer. diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index d3d8a597f2..37126234b6 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -132,6 +132,7 @@ kind: "package-reference" - [进程内 spawn 后端](../subagent-spawn-in-process/README.zh.md)——最容易组合的提供方。 - [Auto review](../../experimental/auto-review/README.zh.md)——只有进程内 DSH 子级继承的当前会话授权模式。 - [进程外 ACP 后端](../subagent-acp/README.zh.md)——经 Agent Client Protocol 拥有自有运行时的子级。 +- [DeepSeek 输入转换](../../llm/llm-deepseek/README.zh.md#model-experience)——已保存结算通知的提供方回放规则。 - [tool-subagent-control README](../tool-subagent-control/README.zh.md)——后续消息、中断与列举面。 ----- @@ -186,7 +187,6 @@ You are a delegated subagent: your permission scope was fixed when you were star - **取消收敛期间存在唤醒缺口**——中断信号发出后、driver 进入 idle 前被接受的后续消息会保持排队,直到另一条唤醒发送到达。 - **待处理的注入上下文会保留 Activation**——settlement 会保守地把每个 Inbox occurrence 都视为未完成。Agent 进入 idle 后停放的上下文会让 child 及其在线祖先继续驻留,直到唤醒投递将其 claim、queue 变更将其移除,或 manager teardown 将其丢弃。 - **驻留仅限进程内**——Activation inbox 与所有权图不会在两个 harness 进程之间协调;对单个持久化存储的并发访问需要持久化邮箱与跨进程租约协议。 -- **已保存的结算通知不会被改写**——若用户角色的已保存通知含有推理块,只要它仍在父级请求历史中,DeepSeek Messages 序列化就会失败。 - **不回放已接受但未记录的消息**——崩溃可能丢失从未写入子会话日志、已被接受的提示词;丢失的消息不会自动回放。 - **没有持久化 parent mailbox**——child 到 parent 的消息要求驻留的可继续 child 与在线直接 parent,提供的是接受标识,不保证恰好一次投递。 - **生命周期事件只供观察**——影响运行的 `subagent/end` 延续或决策接口仍需等待具体消费方。 diff --git a/packages/test-support/client-runtime/README.i18n.yaml b/packages/test-support/client-runtime/README.i18n.yaml index 197eef9f88..20d4d80537 100644 --- a/packages/test-support/client-runtime/README.i18n.yaml +++ b/packages/test-support/client-runtime/README.i18n.yaml @@ -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 packages/test-support/client-runtime/README.md -README.md: 819892dfb5b433c6b4dcbe40a445e974137eff71 +README.md: 7eca8e599cf36dbfb635ec2766e7daf15034eab0 README.zh.md: ee63f4d930410f7f5069be9027327559249b56ca diff --git a/packages/test-support/client-runtime/README.md b/packages/test-support/client-runtime/README.md index 819892dfb5..7eca8e599c 100644 --- a/packages/test-support/client-runtime/README.md +++ b/packages/test-support/client-runtime/README.md @@ -123,7 +123,7 @@ The bench copies no production logic: it mounts the production `SlotRegistry`, p | [`src/snapshot.ts`](src/snapshot.ts) | DOM snapshot serializer (class-hash folding, `` fingerprint) | | [`src/remote.ts`](src/remote.ts) | `TestRemote` double for host RPC, `RemoteError` value re-export | | [`src/translate.ts`](src/translate.ts) + [`src/locale-env.ts`](src/locale-env.ts) | Translation and pinned-browser-language test helpers | -| [`src/settings-scope.ts`](src/settings-scope.ts) | `stubSettingsScope` with test-driven publications and a write spy | +| [`src/settings-scope.ts`](src/settings-scope.ts) | `stubSettingsScope` with test-driven publications and write spies | | [`src/assembly/roster.ts`](src/assembly/roster.ts) | `ClientRosterRow`, `ClientRoster` (`of`/`closure`/`pick`/`without`), the `AssemblyPlan` it annotates, and `graphFromRoster` | | [`src/assembly/modules.ts`](src/assembly/modules.ts) | Source `/client` imports and replacements, registered through the production module facade's pending factory queue | | [`src/assembly/test-client.ts`](src/assembly/test-client.ts) | `TestClient`: instance-bound Connection, shared jsdom shims, `bootClient`, mount, readiness wait, `reload`/`unload`/`dispose` | diff --git a/packages/test-support/client-runtime/src/settings-scope.ts b/packages/test-support/client-runtime/src/settings-scope.ts index 0f325a54b0..04d4063e70 100644 --- a/packages/test-support/client-runtime/src/settings-scope.ts +++ b/packages/test-support/client-runtime/src/settings-scope.ts @@ -1,4 +1,4 @@ -/** Test double for the client settings-scope seam. */ +/** Test doubles for settings transport. */ import { vi } from 'vitest' import type { SettingsScope, SettingsScopeSnapshot, @@ -35,9 +35,9 @@ export function stubSettingsScope(): StubSettingsScope { revision: undefined, writable: false, mode: 'host', } const listeners = new Set<() => void>() - const set = vi.fn(() => Promise.resolve()) - const mutate = vi.fn(() => Promise.resolve()) - const unset = vi.fn(() => Promise.resolve()) + const set = vi.fn(() => Promise.resolve(true)) + const mutate = vi.fn(() => Promise.resolve(true)) + const unset = vi.fn(() => Promise.resolve(true)) return { scope: { getSnapshot: () => snapshot, diff --git a/packages/test-support/client-runtime/tests/runtime.client.spec.tsx b/packages/test-support/client-runtime/tests/runtime.client.spec.tsx index 94f9a31d1b..8a690e1752 100644 --- a/packages/test-support/client-runtime/tests/runtime.client.spec.tsx +++ b/packages/test-support/client-runtime/tests/runtime.client.spec.tsx @@ -843,7 +843,8 @@ describe('stubbed settings scope', () => { }) await host.scope.set('preference', 'dark') - await host.scope.unset('preference') + await expect(host.scope.unset('preference')).resolves.toBe(true) + await expect(host.scope.mutate([{ op: 'set', path: ['preference'], value: 'dark' }])).resolves.toBe(true) host.publish({ status: 'ready', value: { preference: 'system' }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13ab4ef874..1a0cd9b666 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,9 +219,6 @@ importers: '@deepseek-ai/dsh-experimental-agent-team-profile': specifier: workspace:^ version: link:../../packages/experimental/agent-team-profile - '@deepseek-ai/dsh-experimental-agent-team-web-profile': - specifier: workspace:^ - version: link:../../packages/experimental/agent-team-web-profile '@deepseek-ai/dsh-fs-local': specifier: workspace:^ version: link:../../packages/fs/fs-local @@ -542,6 +539,9 @@ importers: '@deepseek-ai/dsh-home-paths': specifier: workspace:^ version: link:../../packages/util/home-paths + '@deepseek-ai/node-addon-system': + specifier: workspace:^ + version: link:../../native/system/packages/entry '@electron/get': specifier: ^5.1.0 version: 5.1.0(supports-color@9.4.0) @@ -2820,6 +2820,9 @@ importers: '@deepseek-ai/dsh-util-workspace-path': specifier: workspace:^ version: link:../../util/workspace-path + '@tanstack/react-virtual': + specifier: ^3.14.9 + version: 3.14.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -3077,6 +3080,9 @@ importers: '@deepseek-ai/dsh-client-ui-renderer': specifier: workspace:^ version: link:../ui-renderer + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:^ + version: link:../ui-settings '@deepseek-ai/dsh-client-ui-sidebar-right': specifier: workspace:^ version: link:../ui-sidebar-right @@ -4015,6 +4021,10 @@ importers: version: 18.3.1 packages/client/ui-settings: + dependencies: + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -4037,9 +4047,6 @@ importers: '@deepseek-ai/dsh-util-values': specifier: workspace:^ version: link:../../util/values - '@deepseek-ai/schemastery': - specifier: link:../../../vendor/schemastery - version: link:../../../vendor/schemastery '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -4429,6 +4436,9 @@ importers: '@deepseek-ai/dsh-client-ui-session': specifier: workspace:^ version: link:../ui-session + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:^ + version: link:../ui-settings '@deepseek-ai/dsh-client-ui-sidebar-right': specifier: workspace:^ version: link:../ui-sidebar-right @@ -4459,6 +4469,9 @@ importers: clsx: specifier: ^2.0.0 version: 2.1.1 + dompurify: + specifier: ^3.4.11 + version: 3.4.11 pdfjs-dist: specifier: 6.3.289 version: 6.3.289 @@ -6202,25 +6215,12 @@ importers: '@deepseek-ai/dsh-experimental-agent-team': specifier: workspace:^ version: link:../agent-team - '@deepseek-ai/dsh-experimental-tool-agent-team': - specifier: workspace:^ - version: link:../tool-agent-team - devDependencies: - '@deepseek-ai/cordis': - specifier: workspace:^ - version: link:../../../vendor/cordis - '@deepseek-ai/cordis-plugin-include': - specifier: workspace:^ - version: link:../../../vendor/include - js-yaml: - specifier: ^4.2.0 - version: 4.3.1 - - packages/experimental/agent-team-web-profile: - dependencies: '@deepseek-ai/dsh-experimental-client-ui-agent-team': specifier: workspace:^ version: link:../client-ui-agent-team + '@deepseek-ai/dsh-experimental-tool-agent-team': + specifier: workspace:^ + version: link:../tool-agent-team devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ diff --git a/scripts/archived-agent-notes.spec.ts b/scripts/archived-agent-notes.spec.ts index 63230b2d53..f96b0b1393 100644 --- a/scripts/archived-agent-notes.spec.ts +++ b/scripts/archived-agent-notes.spec.ts @@ -33,6 +33,33 @@ describe('archived Agent Notes', () => { expect(validateArchiveArtifacts(fixture())).toEqual([]) }) + it.each(['# Agent Note:示例', '# Historical decision'])('preserves historical formatting: %s', (title) => { + const artifacts = fixture() + const base = '2026-07-26-example' + const source = Buffer.from(`${title}\nStatus: implemented\nArchived: 2026-07-26\n[中文版本](${base}.zh.md)\n`) + artifacts.set(`process/${base}.md`, source) + const zh = artifacts.get(`process/${base}.zh.md`)! + artifacts.set(`process/${base}.i18n.yaml`, Buffer.from(`${base}.md: ${gitBlobHash(source)}\n${base}.zh.md: ${gitBlobHash(zh)}\n`)) + expect(validateArchiveArtifacts(artifacts)).toEqual([]) + }) + + it.each([ + ['Status: proposed\nArchived: 2026-07-26', /requires `Status: implemented`/], + ['Status: implemented\n\nArchived: 2026-07-26', /immediately after the status/], + ['Status: implemented\nArchived: 2026-02-30', /valid date/], + ['Status: implemented\nArchived: 2026-07-25', /predates the note filename/], + ['Status: implemented\nArchived: 2026-07-27', /English and Chinese archive dates differ/], + ['Archived: 2026-07-26\nStatus: implemented\nArchived: 2026-07-27', /English and Chinese archive dates differ/], + ])('rejects invalid archive metadata: %s', (metadata, error) => { + const artifacts = fixture() + const base = '2026-07-26-example' + const source = Buffer.from(`# Historical decision\n${metadata}\n`) + artifacts.set(`process/${base}.md`, source) + const zh = artifacts.get(`process/${base}.zh.md`)! + artifacts.set(`process/${base}.i18n.yaml`, Buffer.from(`${base}.md: ${gitBlobHash(source)}\n${base}.zh.md: ${gitBlobHash(zh)}\n`)) + expect(validateArchiveArtifacts(artifacts).join('\n')).toMatch(error) + }) + it('rejects incomplete triplets and invalid archive headers', () => { const artifacts = fixture() artifacts.delete('process/2026-07-26-example.i18n.yaml') diff --git a/scripts/archived-agent-notes.ts b/scripts/archived-agent-notes.ts index 39786cca27..5f96550e62 100644 --- a/scripts/archived-agent-notes.ts +++ b/scripts/archived-agent-notes.ts @@ -94,27 +94,21 @@ function pairMeta(content: string): Map | undefined { return entries } -function validateHeader(path: string, content: Buffer, sourceBase: string, chinese: boolean): string[] { +function validateMetadata(path: string, content: Buffer, sourceBase: string): { errors: string[]; date: string | undefined } { const errors: string[] = [] const lines = content.toString('utf8').split('\n') - if (!/^# Agent Note: \S/.test(lines[0] ?? '')) errors.push(`${path}: line 1 must be \`# Agent Note: \``) - if (lines[1] !== '') errors.push(`${path}: line 2 must be blank`) - if (lines[2] !== 'Status: implemented') errors.push(`${path}: line 3 must be \`Status: implemented\``) - const archived = /^Archived: (\d{4}-\d{2}-\d{2})$/.exec(lines[3] ?? '')?.[1] - if (archived === undefined || !validDate(archived)) { - errors.push(`${path}: line 4 must be \`Archived: YYYY-MM-DD\` with a valid date`) + const status = lines.findIndex(line => line.startsWith('Status:')) + if (lines[status] !== 'Status: implemented') errors.push(`${path}: requires \`Status: implemented\``) + const archived = /^Archived: (\d{4}-\d{2}-\d{2})$/.exec(lines[status + 1] ?? '')?.[1] + if (status < 0 || archived === undefined || !validDate(archived)) { + errors.push(`${path}: requires \`Archived: YYYY-MM-DD\` with a valid date immediately after the status`) } else if (archived < sourceBase.slice(0, 10)) { errors.push(`${path}: archive date ${archived} predates the note filename`) } - if (lines[4] !== '') errors.push(`${path}: line 5 must be blank`) - const switcher = chinese - ? `[English](${sourceBase}.md) | 中文` - : `English | [中文](${sourceBase}.zh.md)` - if (lines[5] !== switcher) errors.push(`${path}: line 6 must be ${JSON.stringify(switcher)}`) - return errors + return { errors, date: archived } } -/** Validate the closed kind tree, implemented/archive headers, and complete bilingual triplets. */ +/** Validate the closed kind tree, implemented/archive metadata, and complete bilingual triplets. */ export function validateArchiveArtifacts(artifacts: ReadonlyMap): string[] { const errors: string[] = [] const triplets = new Map() @@ -151,10 +145,9 @@ export function validateArchiveArtifacts(artifacts: ReadonlyMap) continue } const sourceBase = basename(key) - errors.push(...validateHeader(sourcePath, source, sourceBase, false)) - errors.push(...validateHeader(zhPath, zh, sourceBase, true)) - const sourceDate = /^Archived: (\d{4}-\d{2}-\d{2})$/m.exec(source.toString('utf8'))?.[1] - const zhDate = /^Archived: (\d{4}-\d{2}-\d{2})$/m.exec(zh.toString('utf8'))?.[1] + const { errors: sourceErrors, date: sourceDate } = validateMetadata(sourcePath, source, sourceBase) + const { errors: zhErrors, date: zhDate } = validateMetadata(zhPath, zh, sourceBase) + errors.push(...sourceErrors, ...zhErrors) if (sourceDate !== undefined && zhDate !== undefined && sourceDate !== zhDate) { errors.push(`${key}: English and Chinese archive dates differ (${sourceDate} vs ${zhDate})`) } diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 012fbcd8de..53d32a40aa 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -189,7 +189,9 @@ describe('CI workflow', () => { expect(job['runs-on']).toContain('self-hosted') expect(job['runs-on']).toContain('dsh-win-ci') expect(job['runs-on']).toContain('dsh-windows-2025-16core') - expect(job['runs-on']).toContain('blacksmith-16vcpu-windows-2025') + const cores = jobName === 'windows-native-tests' ? 2 : jobName === 'windows-observational' ? 8 : 16 + expect(evaluateRunsOn(job['runs-on'], { vars: { DSH_CI_FAILOVER_WINDOWS: 'blacksmith' } })) + .toBe(`blacksmith-${cores}vcpu-windows-2025`) expect(job.if).toBe("github.event_name == 'pull_request'") } @@ -230,9 +232,8 @@ describe('CI workflow', () => { expect(install!.run).not.toContain('$cloneFlag') } - // windows-coverage uses the lower 4-partition profile. expect(windowsCoverage.name).toBe('windows node 24 / coverage') - expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '4' }) + expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: "${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '4' || '' }}" }) const coverageSteps = windowsCoverage.steps as unknown[] const coverageCommands = coverageSteps.filter((step): step is Record & { run: string } => ( isRecord(step) && typeof step.run === 'string' @@ -329,7 +330,8 @@ describe('CI workflow', () => { expect(job['runs-on'], `${jobName} runs-on must use the Linux failover switch`).toContain('DSH_CI_FAILOVER_LINUX') expect(job['runs-on'], `${jobName} runs-on must not use the Windows failover switch`).not.toContain('DSH_CI_FAILOVER_WINDOWS') expect(job['runs-on']).toContain('vm-backup') - expect(job['runs-on']).toContain('blacksmith-16vcpu-ubuntu-2404') + expect(evaluateRunsOn(job['runs-on'], { vars: { DSH_CI_FAILOVER_LINUX: 'blacksmith' } })) + .toBe(`blacksmith-${jobName === 'node-24' ? 8 : 16}vcpu-ubuntu-2404`) } expect(aggregate['runs-on']).toContain('DSH_CI_FAILOVER_LINUX') expect(aggregate['runs-on']).not.toContain('DSH_CI_FAILOVER_WINDOWS') @@ -383,8 +385,7 @@ describe('CI workflow', () => { // The observational lane stays complete: it is continue-on-error by design // and exists to collect as much Windows-native evidence per run as // possible, so the first failure must not truncate the rest. - expect(windowsObservational.env).toBeDefined() - expect(windowsObservational.env).not.toMatchObject({ DSH_GATE_FAIL_FAST: '1' }) + expect(windowsObservational.env ?? {}).not.toMatchObject({ DSH_GATE_FAIL_FAST: '1' }) }) it('gates standalone keyless blacksmith jobs and benchmark tiers on the failover variables', () => { @@ -608,8 +609,8 @@ describe('Runtime and LLM e2e Blacksmith routing', () => { const workflow = loadWorkflow('.github/workflows/build-exe-for-python-sdk.yml') const build = workflowJob(workflow, 'build') for (const [target, runner, variable, blacksmith] of [ - ['node24-linux-x64', 'ubuntu-latest', 'DSH_CI_FAILOVER_LINUX', 'blacksmith-16vcpu-ubuntu-2404'], - ['node24-win-x64', 'windows-2025', 'DSH_CI_FAILOVER_WINDOWS', 'blacksmith-16vcpu-windows-2025'], + ['node24-linux-x64', 'ubuntu-latest', 'DSH_CI_FAILOVER_LINUX', 'blacksmith-2vcpu-ubuntu-2404'], + ['node24-win-x64', 'windows-2025', 'DSH_CI_FAILOVER_WINDOWS', 'blacksmith-2vcpu-windows-2025'], ['node24-linux-arm64', 'ubuntu-24.04-arm', 'DSH_CI_FAILOVER_LINUX', 'ubuntu-24.04-arm'], ['node24-macos-arm64', 'macos-latest', 'DSH_CI_FAILOVER_LINUX', 'macos-latest'], ['node24-macos-x64', 'macos-15-intel', 'DSH_CI_FAILOVER_LINUX', 'macos-15-intel'], diff --git a/scripts/release/families.spec.ts b/scripts/release/families.spec.ts index 2353f6a81e..6ae3d23702 100644 --- a/scripts/release/families.spec.ts +++ b/scripts/release/families.spec.ts @@ -49,7 +49,6 @@ describe('release families', () => { .filter(member => member.directory.startsWith('packages/experimental/')) .map(member => member.name)).toEqual([ '@deepseek-ai/dsh-experimental-agent-team-profile', - '@deepseek-ai/dsh-experimental-agent-team-web-profile', '@deepseek-ai/dsh-experimental-agent-team', '@deepseek-ai/dsh-experimental-auto-review', '@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp', diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index fb61abe8b5..c1c8fc1a28 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -1,7 +1,8 @@ import { readFileSync } from 'node:fs' -import { describe, expect, it, vi, type MockInstance } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest' import { cliGateOptions, + ciWorkerEnvironment, collectDescendants, defaultConcurrency, formatGateResultReason, @@ -14,6 +15,10 @@ import { type GateResult, } from './run-gates.ts' +// Graph fixtures select their own browser pool instead of inheriting the CI host's pool. +beforeEach(() => vi.stubEnv('DSH_WEB_SNAPSHOT_WORKERS', undefined)) +afterEach(() => vi.unstubAllEnvs()) + /** * Capture output a gate streams through runGate's streamOutput path. * @returns the accumulated chunks and the stdout spy to restore in finally. @@ -135,6 +140,59 @@ function withEnv(name: string, value: string | undefined, action: () => T): T } } +describe('CI worker allocation', () => { + it.each([1, 2, 4, 8, 16, 64])('shares a %i CPU coverage budget without multiplying pools', (cpus) => { + const env = ciWorkerEnvironment('ci-coverage', {}, cpus) + const exempt = Math.max(1, Math.floor(cpus / 3)) + const instrumented = Number(env.DSH_COVERAGE_PARTITIONS ?? 1) + expect(Number(env.DSH_COVERAGE_MAX_WORKERS)).toBe(cpus) + expect(instrumented + exempt).toBe(Math.max(2, cpus)) + expect(defaultConcurrency('ci-coverage', 3, cpus).workers).toBe(Math.min(3, cpus)) + if (cpus <= 2) expect(env.DSH_COVERAGE_PARTITIONS).toBeUndefined() + }) + + it('bounds overlapping readers and lets the isolated browser pool use the runner', () => { + const env = ciWorkerEnvironment('ci-consumers', {}, 16) + expect(env).toMatchObject({ + DSH_OXLINT_THREADS: '8', + DSH_PUBLINT_CONCURRENCY: '8', + DSH_SNAPSHOT_MAX_WORKERS: '1', + DSH_SNAPSHOT_MAX_CONCURRENCY: '8', + DSH_WEB_SNAPSHOT_WORKERS: '16', + }) + }) + + it('preserves serial reference overrides on large hosts', () => { + const inherited = { + DSH_GATE_CONCURRENCY: '1', + DSH_COVERAGE_MAX_WORKERS: '1', + DSH_OXLINT_THREADS: '1', + DSH_PUBLINT_CONCURRENCY: '1', + DSH_SNAPSHOT_MAX_CONCURRENCY: '1', + DSH_WEB_SNAPSHOT_WORKERS: '1', + } + const additions = ciWorkerEnvironment('ci-primary', inherited, 64) + expect(additions).toEqual({ DSH_SNAPSHOT_MAX_WORKERS: '1' }) + expect(inherited.DSH_COVERAGE_MAX_WORKERS).toBe('1') + }) + + it('honors an explicit coverage budget and partition override', () => { + expect(ciWorkerEnvironment('ci-coverage', { DSH_COVERAGE_MAX_WORKERS: '6' }, 16)) + .toHaveProperty('DSH_COVERAGE_PARTITIONS', '4') + expect(ciWorkerEnvironment('ci-coverage', { DSH_COVERAGE_PARTITIONS: '3' }, 16)) + .not.toHaveProperty('DSH_COVERAGE_PARTITIONS') + }) + + it.each(['0', '-1', 'NaN', '2.5'])('rejects invalid worker budget %s', (raw) => { + expect(() => ciWorkerEnvironment('ci-coverage', { DSH_COVERAGE_MAX_WORKERS: raw }, 16)) + .toThrow('DSH_COVERAGE_MAX_WORKERS must be a positive integer') + }) + + it('keeps local documentation defaults unchanged', () => { + expect(ciWorkerEnvironment('doc-sync', {}, 64)).toEqual({}) + }) +}) + describe('gate graph validation', () => { it.each([ 'ci-primary', @@ -601,8 +659,8 @@ describe('Node 24 lane ownership', () => { const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers')) expect(defaultConcurrency('ci-consumers', subject.length, 4)).toEqual({ - workers: 11, - source: 'ci-consumers gate count', + workers: 4, + source: '4 available CPU(s)', }) expect(subject.map(item => item.id)).toEqual([ 'build', diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 48ec3f0cde..b70cef453c 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -102,6 +102,9 @@ if (import.meta.main) { async function main(args: string[]): Promise { const mode = parseMode(args[0]) + const workerEnv = ciWorkerEnvironment(mode, process.env) + Object.assign(process.env, workerEnv) + if (Object.keys(workerEnv).length > 0) console.log(`run-gates: worker settings ${JSON.stringify(workerEnv)}`) const gates = gatesForMode(mode) const concurrencyDefault = defaultConcurrency(mode, gates.length) const concurrencyOverride = process.env.DSH_GATE_CONCURRENCY @@ -170,7 +173,6 @@ export function defaultConcurrency( total: number, available = availableParallelism(), ): ConcurrencyDefault { - if (selectedMode === 'ci-consumers') return { workers: total, source: 'ci-consumers gate count' } // Local modes cap workers: several doc gates each build a full ts.Program, // so an uncapped default on a large host trades wall clock for memory blowups. const localCap = selectedMode === 'check-all' @@ -186,6 +188,41 @@ export function defaultConcurrency( } } +/** + * Fill CI worker defaults from the CPU allocation without replacing explicit overrides. + * Coverage shares one budget; artifact readers share CPUs with sibling commands. + * @param mode - aggregate selected by the caller. + * @param env - inherited worker overrides, including serial reference settings. + * @param available - CPUs available to this runner process. + * @returns environment additions for this aggregate and its children. + */ +export function ciWorkerEnvironment( + mode: Mode, + env: NodeJS.ProcessEnv, + available = availableParallelism(), +): Record { + if (!mode.startsWith('ci-')) return {} + const additions: Record = {} + const setDefault = (name: string, value: number): void => { + if (env[name] === undefined || env[name] === '') additions[name] = String(value) + } + const shared = Math.max(1, Math.floor(available / 2)) + setDefault('DSH_OXLINT_THREADS', shared) + setDefault('DSH_PUBLINT_CONCURRENCY', shared) + setDefault('DSH_SNAPSHOT_MAX_WORKERS', 1) + setDefault('DSH_SNAPSHOT_MAX_CONCURRENCY', shared) + setDefault('DSH_WEB_SNAPSHOT_WORKERS', env.DSH_GATE_CONCURRENCY === '1' ? 1 : available) + setDefault('DSH_COVERAGE_MAX_WORKERS', available) + const coverageBudget = env.DSH_COVERAGE_MAX_WORKERS || String(available) + const total = Number(coverageBudget) + if (!Number.isSafeInteger(total) || total < 1) { + throw new Error(`run-gates: DSH_COVERAGE_MAX_WORKERS must be a positive integer, got ${JSON.stringify(coverageBudget)}.`) + } + const instrumented = Math.max(1, total - Math.max(1, Math.floor(total / 3))) + if (instrumented > 1) setDefault(COVERAGE_PARTITIONS_ENV, instrumented) + return additions +} + function concurrencyFromEnv(name: string, fallback: number): number { const raw = process.env[name] if (raw === undefined || raw === '') return fallback @@ -498,8 +535,8 @@ function webSnapshotGate(needs: string[], after?: string[]): Gate { const workerRaw = process.env.DSH_WEB_SNAPSHOT_WORKERS if (workerRaw !== undefined && workerRaw !== '') { const workers = Number.parseInt(workerRaw, 10) - if (!Number.isSafeInteger(workers) || workers < 2 || String(workers) !== workerRaw) { - throw new Error(`run-gates: DSH_WEB_SNAPSHOT_WORKERS must be an integer greater than 1, got ${JSON.stringify(workerRaw)}.`) + if (!Number.isSafeInteger(workers) || workers < 1 || String(workers) !== workerRaw) { + throw new Error(`run-gates: DSH_WEB_SNAPSHOT_WORKERS must be a positive integer, got ${JSON.stringify(workerRaw)}.`) } return pnpmScript('web-snapshot', 'test:web:ci', { label: 'web browser snapshot', @@ -593,10 +630,9 @@ function lintGate(options: { needs?: string[] } = {}): Gate { // under v8 instrumentation while contributing nothing the thresholds need // (membership rules in scripts/coverage-exempt.ts). // -// DSH_COVERAGE_MAX_WORKERS is the ordinary lane's worker budget, so the two -// parallel gates split it instead of each claiming it whole. When -// DSH_COVERAGE_PARTITIONS is set, its single-worker processes replace the -// instrumented share while this budget still sizes the exempt gate. The exempt +// DSH_COVERAGE_MAX_WORKERS is shared between the two parallel gates. CI +// defaults its partition count to the instrumented share; an explicit +// DSH_COVERAGE_PARTITIONS overrides that share independently. The exempt // gate's wall clock is dominated by its longest single file, so it takes the // small share. A budget of 1 gives each gate 1 worker; lanes that need a strict // total of one (the serial reference jobs) also set DSH_GATE_CONCURRENCY=1, diff --git a/scripts/run-web-snapshots.ts b/scripts/run-web-snapshots.ts index da3ed586fd..0a02632ad0 100644 --- a/scripts/run-web-snapshots.ts +++ b/scripts/run-web-snapshots.ts @@ -7,8 +7,8 @@ const serialFiles = [ ] const workerRaw = process.env.DSH_WEB_SNAPSHOT_WORKERS const workers = Number.parseInt(workerRaw ?? '', 10) -if (!Number.isSafeInteger(workers) || workers < 2 || String(workers) !== workerRaw) { - throw new Error(`DSH_WEB_SNAPSHOT_WORKERS must be an integer greater than 1, got ${JSON.stringify(workerRaw)}.`) +if (!Number.isSafeInteger(workers) || workers < 1 || String(workers) !== workerRaw) { + throw new Error(`DSH_WEB_SNAPSHOT_WORKERS must be a positive integer, got ${JSON.stringify(workerRaw)}.`) } const invocation = pnpmInvocation(['exec', 'vitest', 'run', '--config', 'vitest.web.config.ts']) let serialStatus = 0 diff --git a/scripts/verify-application-entrypoints.ts b/scripts/verify-application-entrypoints.ts index 5c98446a58..728dd0832f 100644 --- a/scripts/verify-application-entrypoints.ts +++ b/scripts/verify-application-entrypoints.ts @@ -32,6 +32,7 @@ const MANIFEST_BIN_ALLOWLIST = new Map([ /** Every JavaScript executable in an application or packaging workspace has one explicit role. */ const EXECUTABLE_SOURCE_ALLOWLIST = new Map([ ['apps/cli/src/bin.ts', 'supported dsh application launcher'], + ['apps/desktop/scripts/logged-notarytool.mjs', 'build-only notarization logging wrapper'], ['packages/context/time-context/tests/fixtures/driver.ts', 'test-only subprocess driver'], ['packages/experimental/webworker-packer/bin.js', 'build-only wrapper'], ['packages/experimental/webworker-packer/src/bin.ts', 'build-only implementation'], diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index a3c030bc0b..33363b380e 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -93,7 +93,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/file-upload': { kind: 'none', reason: 'Browser-side request-body transport; registers nothing model-facing.' }, 'packages/api/remotes': { kind: 'none', reason: 'The Remote BFF selects business methods and forwarded events; selected services own any model-visible effect.' }, - 'packages/experimental/agent-team-web-profile': { kind: 'indirect', reason: 'The Web layer delegates model composition to the Host-side Agent Teams profile.' }, 'packages/experimental/client-ui-agent-team': { kind: 'none', reason: 'The browser projection and task controls register no model-facing input.' }, 'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/ui-dockkit': { kind: 'none', reason: 'Browser-side docking layout engine and components; registers nothing model-facing.' }, diff --git a/snapshots/session/deepseek-messages-input-history/cordis.snapshot.yml b/snapshots/session/deepseek-messages-input-history/cordis.snapshot.yml new file mode 100644 index 0000000000..93ac7431eb --- /dev/null +++ b/snapshots/session/deepseek-messages-input-history/cordis.snapshot.yml @@ -0,0 +1,47 @@ +# Replay patch shared by the ordinary headless snapshot composition. The model +# script comes from the scenario's committed session JSONL. + +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + disabled: true + +- id: plugin-package-inventory-deepseek + disabled: true + +- id: session-title-llm + disabled: true + +- id: session-persistence-jsonl + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js dshHomePath('sessions') + compression: none + +- id: sandbox + name: '@deepseek-ai/dsh-sandbox-local' + config: + runnerCommand: + - bash + - -c + - while [ "$1" != "--" ]; do shift; done; shift; exec "$@" + - passthrough-runner + runnerFailureSignatures: + - 'passthrough-runner: profile rejected' + +- insert: + - id: llm-replay + name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official + name: DeepSeek + models: + - id: deepseek-v4-flash + - id: deepseek-v4-pro + - id: deepseek-messages + name: DeepSeek Messages + models: + - id: deepseek-v4-flash + - id: deepseek-v4-pro + - id: messages-input-history-snapshot + name: '../../../packages/llm/llm-deepseek/tests/messages/fixtures/input-history.ts' diff --git a/snapshots/session/deepseek-messages-input-history/cordis.yml b/snapshots/session/deepseek-messages-input-history/cordis.yml new file mode 100644 index 0000000000..d143031868 --- /dev/null +++ b/snapshots/session/deepseek-messages-input-history/cordis.yml @@ -0,0 +1,3 @@ +- insert: + - id: messages-input-history-snapshot + name: '../../../packages/llm/llm-deepseek/tests/messages/fixtures/input-history.ts' diff --git a/snapshots/session/deepseek-messages-input-history/session.v3.jsonl b/snapshots/session/deepseek-messages-input-history/session.v3.jsonl new file mode 100644 index 0000000000..5b51d9aa82 --- /dev/null +++ b/snapshots/session/deepseek-messages-input-history/session.v3.jsonl @@ -0,0 +1,19 @@ +{"type":"session","version":3,"id":"{{session:1}}","createdAt":1783352050748,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"source":{"kind":"plugin","plugin":"messages-input-history-snapshot"},"content":[{"type":"text","text":"Background subagent finished. Its closing message: Saved child answer."},{"type":"reasoning","text":"unforwarded-child-thought"},{"type":"tool-call","id":"unforwarded-child-call","name":"bash","arguments":"{}"}],"role":"user","id":"{{message:4}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"source":{"kind":"plugin","plugin":"messages-input-history-snapshot"},"content":[{"type":"reasoning","text":"unforwarded-empty-notice"}],"role":"user","id":"{{message:5}}"},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"session/title","data":{"title":"Reply DONE.","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":168,"outputTokens":25,"cacheReadTokens":2816,"totalTokens":3009},"stream":[{"type":"chunk","time":1789702637930,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1789702637930,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1789702637931,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1789702637931,"chunk":{"type":"usage","usage":{"inputTokens":168,"outputTokens":25,"cacheReadTokens":2816,"totalTokens":3009}}},{"type":"chunk","time":1789702637931,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/deepseek-messages-input-history/snapshot.yml b/snapshots/session/deepseek-messages-input-history/snapshot.yml new file mode 100644 index 0000000000..0238058e29 --- /dev/null +++ b/snapshots/session/deepseek-messages-input-history/snapshot.yml @@ -0,0 +1,10 @@ +version: 1 +scenario: deepseek-messages-input-history +profile: headless +composition: deepseek-messages-input-history +recording: authored +header: + class: deepseek-messages-input-history + pin: true + systemPromptSource: text-turn + toolSchemasSource: text-turn diff --git a/snapshots/web/auto-review-denial/ui.expected.md b/snapshots/web/auto-review-denial/ui.expected.md index b849cc3a7b..e2b23855a9 100644 --- a/snapshots/web/auto-review-denial/ui.expected.md +++ b/snapshots/web/auto-review-denial/ui.expected.md @@ -3,96 +3,64 @@ - banner: - navigation "Session hierarchy": - button "{{workspace}}" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - text: Inspect the protected operation, but do not run it unless authorized. {{clock}} -- button "Copy": - - img +- button "Copy" - text: Failed -- button "Tool call Rejected by Auto review": - - img - - text: Tool call Rejected by Auto review -- button "Code Catch an Auto-denied inner call": - - img - - img - - text: Code Catch an Auto-denied inner call +- button "Tool call Rejected by Auto review" +- button "Code Catch an Auto-denied inner call" - text: Failed -- button "Bash Rejected by Auto review": - - img - - text: Bash Rejected by Auto review +- button "Bash Rejected by Auto review" - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Auto review EXP"': - text: Auto review - superscript: EXP -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps": - - img - - text: 1 turns 2 steps +- button "1 turns 2 steps" ## Expanded - banner: - navigation "Session hierarchy": - button "{{workspace}}" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - text: Inspect the protected operation, but do not run it unless authorized. {{clock}} -- button "Copy": - - img +- button "Copy" - text: Failed -- button "Tool call Rejected by Auto review" [expanded]: - - img - - text: Tool call Rejected by Auto review +- button "Tool call Rejected by Auto review" [expanded] - text: "OUT Tool was not executed. Reason: raw reason" - button "Inspect" -- button "Code Catch an Auto-denied inner call": - - img - - img - - text: Code Catch an Auto-denied inner call +- button "Code Catch an Auto-denied inner call" - text: Failed -- button "Bash Rejected by Auto review" [expanded]: - - img - - text: Bash Rejected by Auto review +- button "Bash Rejected by Auto review" [expanded] - text: "OUT Tool was not executed. Reason: ptc raw reason" - button "Inspect" - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Auto review EXP"': - text: Auto review - superscript: EXP -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps": - - img - - text: 1 turns 2 steps +- button "1 turns 2 steps" ## Trajectory - banner: - navigation "Session hierarchy": - button "{{workspace}}" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" - tab "Trajectory" [selected] @@ -100,7 +68,6 @@ - button "Use actual duration": Duration - button "Collapse turns": Turns - button "Collapse calls": Calls - - img - searchbox "Search trajectory" - region "Trajectory timeline" - table: @@ -123,9 +90,7 @@ - cell "(tool call only)" - row "TOOL, run_code Catch an Auto-denied inner call": - cell "TOOL" - - cell "run_codeCatch an Auto-denied inner call → denial caught": - - img - - text: run_codeCatch an Auto-denied inner call → denial caught + - cell "run_codeCatch an Auto-denied inner call → denial caught" - 'row "SUBTOOL, bash {\"command\":\"echo hidden-input-ptc\"}" [selected]': - cell "SUBTOOL" - 'cell "bash{\"command\":\"echo hidden-input-ptc\"} → AUTO_REVIEW_DENIED"' @@ -141,15 +106,10 @@ - tab "Timing" - tabpanel "Result": "AutoReviewDeniedError: AUTO_REVIEW_DENIED Error: Auto review rejected tool \"bash\"; its body was not executed" - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Auto review EXP"': - text: Auto review - superscript: EXP -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps": - - img - - text: 1 turns 2 steps +- button "1 turns 2 steps" diff --git a/snapshots/web/bash-abort-row/ui.expected.md b/snapshots/web/bash-abort-row/ui.expected.md index 89f53fe375..90fbaa8f11 100644 --- a/snapshots/web/bash-abort-row/ui.expected.md +++ b/snapshots/web/bash-abort-row/ui.expected.md @@ -1,43 +1,23 @@ - banner: - navigation "Session hierarchy": - 'button "Run two shell commands: wait" [disabled]' - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Run two shell commands: wait for cancellation, then write skipped.txt. {{clock}}" -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- 'button "Failed Bash Error: tool call aborted" [expanded]': - - img - - text: "Failed Bash Error: tool call aborted" +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" +- 'button "Failed Bash Error: tool call aborted" [expanded]' - text: "IN { \"command\": \"node -e \\\"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\\\"\", \"description\": \"Wait until cancellation\" } OUT Error: tool call aborted" - button "Inspect" -- 'button "Failed Bash Error: tool call aborted before dispatch"': - - img - - text: "Failed Bash Error: tool call aborted before dispatch" +- 'button "Failed Bash Error: tool call aborted before dispatch"' - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Full access"': Full access -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps": - - img - - text: 1 turns 1 steps -- button "20 tok · Cache hit 0%": - - img - - text: 20 tokCache hit 0% +- button "1 turns 1 steps" +- button "20 tok · Cache hit 0%": 20 tokCache hit 0% diff --git a/snapshots/web/changed-files-turn/ui.expected.md b/snapshots/web/changed-files-turn/ui.expected.md index 9854b70b13..e6d546d851 100644 --- a/snapshots/web/changed-files-turn/ui.expected.md +++ b/snapshots/web/changed-files-turn/ui.expected.md @@ -1,63 +1,29 @@ -- button "系统提示词": - - img - - img - - text: 系统提示词 +- button "系统提示词" - text: 不用先查看目录,直接做四件事:把 intro.md 里的标题「示例项目」改成「项目说明」,新建 src/util.ts 导出一个两数相加的 add 函数,新建 app.local 写一行 mode=demo,最后用 bash 在 notes.txt 末尾追加一行 done。 {{clock}} -- button "复制": - - img -- button "7 次工具调用" [expanded]: - - text: 7 次工具调用 - - img -- button "上下文注入 @deepseek-ai/dsh-system-prompt": - - img - - img - - text: 上下文注入 @deepseek-ai/dsh-system-prompt -- button "思考 The user wants four things done directly. But I need to read intro.md before editing (fs-observation-policy requires reading existing file first). The user says don't look at the directory first, but reading intro.md is needed for the edit tool. Let me just do it.": - - img - - img - - text: 思考 The user wants four things done directly. But I need to read intro.md before editing (fs-observation-policy requires reading existing file first). The user says don't look at the directory first, but reading intro.md is needed for the edit tool. Let me just do it. +- button "复制" +- button "7 次工具调用" [expanded] +- button "上下文注入 @deepseek-ai/dsh-system-prompt" +- button "思考 The user wants four things done directly. But I need to read intro.md before editing (fs-observation-policy requires reading existing file first). The user says don't look at the directory first, but reading intro.md is needed for the edit tool. Let me just do it." - button "读取 intro.md": - - img - - img - text: 读取 - button "intro.md" -- button "Bash List files and show notes.txt": - - img - - img - - text: Bash List files and show notes.txt -- button "思考 Now do all four actions. Edit intro.md, write src/util.ts, write app.local, bash append.": - - img - - img - - text: 思考 Now do all four actions. Edit intro.md, write src/util.ts, write app.local, bash append. +- button "Bash List files and show notes.txt" +- button "思考 Now do all four actions. Edit intro.md, write src/util.ts, write app.local, bash append." - button "编辑 intro.md +1 -1": - - img - - img - text: 编辑 - button "intro.md" - text: +1 -1 - button "写入 src/util.ts +3 -0": - - img - - img - text: 写入 - button "src/util.ts" - text: +3 -0 - button "写入 app.local +1 -0": - - img - - img - text: 写入 - button "app.local" - text: +1 -0 -- button "Bash Append done to notes.txt": - - img - - img - - text: Bash Append done to notes.txt -- 'button "思考 All four done. Present the files? The user asked to create files. I should call present for the deliverables. Files: intro.md, src/util.ts, app.local, notes.txt. All are outputs. Let me present them."': - - img - - img - - text: "思考 All four done. Present the files? The user asked to create files. I should call present for the deliverables. Files: intro.md, src/util.ts, app.local, notes.txt. All are outputs. Let me present them." -- button "交付文件 已交付 intro.md, src/util.ts, app.local, notes.txt": - - img - - text: 交付文件 已交付 intro.md, src/util.ts, app.local, notes.txt +- button "Bash Append done to notes.txt" +- 'button "思考 All four done. Present the files? The user asked to create files. I should call present for the deliverables. Files: intro.md, src/util.ts, app.local, notes.txt. All are outputs. Let me present them."' +- button "交付文件 已交付 intro.md, src/util.ts, app.local, notes.txt" - paragraph: 四件事全部完成: - list: - listitem: @@ -110,42 +76,28 @@ - button "查看 intro.md 的改动": intro.md +1 -1 - listitem: - button "查看 notes.txt 的改动": notes.txt +1 -0 -- button "展开全部 4 个改动文件": - - text: 全部 4 个文件 - - img +- button "展开全部 4 个改动文件": 全部 4 个文件 - text: 此主机没有可用的桌面,无法使用外部程序打开文件或文件夹;文件仍可在侧边栏预览 - button "在侧边栏预览 intro.md" - text: intro.md 标题已改为「项目说明」 - button "在侧边栏打开 intro.md": 打开 -- button "intro.md 的更多文件操作" [disabled]: - - img +- button "intro.md 的更多文件操作" [disabled] - button "在侧边栏预览 src/util.ts" - text: util.ts 新增 add 两数相加函数 - button "在侧边栏打开 src/util.ts": 打开 -- button "src/util.ts 的更多文件操作" [disabled]: - - img +- button "src/util.ts 的更多文件操作" [disabled] - button "在侧边栏预览 app.local" - text: app.local 新增配置文件,内容 mode=demo - button "在侧边栏打开 app.local": 打开 -- button "app.local 的更多文件操作" [disabled]: - - img +- button "app.local 的更多文件操作" [disabled] - button "在侧边栏预览 notes.txt" - text: notes.txt 末尾追加了 done 一行 - button "在侧边栏打开 notes.txt": 打开 -- button "notes.txt 的更多文件操作" [disabled]: - - img -- button "复制": - - img -- button "好的回答": - - img -- button "有问题的回答": - - img -- button "在新对话中分支": - - img -- button "用量 36.2K tok": - - img - - text: 用量 36.2K tok -- button "用时 {{duration}}": - - img - - text: 用时 {{duration}} +- button "notes.txt 的更多文件操作" [disabled] +- button "复制" +- button "好的回答" +- button "有问题的回答" +- button "在新对话中分支" +- button "用量 36.2K tok" +- button "用时 {{duration}}" - text: {{clock}} diff --git a/snapshots/web/deepseek-messages-chat/ui.expected.md b/snapshots/web/deepseek-messages-chat/ui.expected.md index af650f99d3..3822fac786 100644 --- a/snapshots/web/deepseek-messages-chat/ui.expected.md +++ b/snapshots/web/deepseek-messages-chat/ui.expected.md @@ -1,57 +1,30 @@ - banner: - navigation "会话层级": - button "只回复 MESSAGES_WEB_READY,不调用" [disabled] - - img - text: 标准模式 - - button "更多操作": - - img - - button "打开右侧边栏": - - img + - button "更多操作" + - button "打开右侧边栏" - tablist: - tab "对话" [selected] - tab "轨迹" -- button "系统提示词": - - img - - img - - text: 系统提示词 +- button "系统提示词" - text: 只回复 MESSAGES_WEB_READY,不调用工具。 {{clock}} -- button "复制": - - img -- button "已思考": - - text: 已思考 - - img -- button "思考": - - img - - img - - text: 思考 +- button "复制" +- button "已思考" +- button "思考" - paragraph: MESSAGES_WEB_READY -- button "复制": - - img -- button "好的回答": - - img -- button "有问题的回答": - - img -- button "在新对话中分支": - - img -- button "用量 8.2K tok": - - img - - text: 用量 8.2K tok -- button "用时 {{duration}}": - - img - - text: 用时 {{duration}} +- button "复制" +- button "好的回答" +- button "有问题的回答" +- button "在新对话中分支" +- button "用量 8.2K tok" +- button "用时 {{duration}}" - text: {{clock}} - textbox "发消息或创建任务, / 调用指令, @ 文件或对话" -- button "添加文件或调用指令": - - img +- button "添加文件或调用指令" - button "访问模式,当前:工作区内修改": 工作区内修改 -- button "选择模型,当前 DeepSeek-V4-Flash,推理等级 high": - - text: DeepSeek-V4-Flash high - - img +- button "选择模型,当前 DeepSeek-V4-Flash,推理等级 high": DeepSeek-V4-Flash high - button "发送消息" [disabled] -- button "1 轮 1 步 · {{throughput}} tok/s": - - img - - text: 1 轮 1 步{{throughput}} tok/s -- button "8.2K tok · 缓存命中 16%": - - img - - text: 8.2K tok缓存命中 16% +- button "1 轮 1 步 · {{throughput}} tok/s": 1 轮 1 步{{throughput}} tok/s +- button "8.2K tok · 缓存命中 16%": 8.2K tok缓存命中 16% - button "上下文已用 1%": 1% diff --git a/snapshots/web/diff-bounded/ui.expected.md b/snapshots/web/diff-bounded/ui.expected.md index bc413d74a5..056f1e2156 100644 --- a/snapshots/web/diff-bounded/ui.expected.md +++ b/snapshots/web/diff-bounded/ui.expected.md @@ -1,5 +1,4 @@ - button "Edit large.txt +130 -130" [expanded]: - - img - text: Edit - button "large.txt" - text: +130 -130 diff --git a/snapshots/web/diff-context/ui.expected.md b/snapshots/web/diff-context/ui.expected.md index 8f2068d63e..628877899b 100644 --- a/snapshots/web/diff-context/ui.expected.md +++ b/snapshots/web/diff-context/ui.expected.md @@ -1,5 +1,4 @@ - button "Edit config.txt +1 -1" [expanded]: - - img - text: Edit - button "config.txt" - text: +1 -1 diff --git a/snapshots/web/document-preview/document.expected.md b/snapshots/web/document-preview/document.expected.md index 28689fa8ca..41159c8576 100644 --- a/snapshots/web/document-preview/document.expected.md +++ b/snapshots/web/document-preview/document.expected.md @@ -14,6 +14,14 @@ - Viewers: Markdown -> Code -> Plain text - Same tab: true +## Basic HTML + +- Developer tools: off by default on Web and desktop +- Sandbox: no permissions +- Inline script: pending +- Local script: pending +- Network requests: 0 + ## HTML - Viewer: HTML diff --git a/snapshots/web/feedback-command/ack-expanded.expected.md b/snapshots/web/feedback-command/ack-expanded.expected.md index 04f10b71f7..679a7f2ecc 100644 --- a/snapshots/web/feedback-command/ack-expanded.expected.md +++ b/snapshots/web/feedback-command/ack-expanded.expected.md @@ -1,62 +1,31 @@ - banner: - navigation "Session hierarchy": - button "Reply with the single word" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while" [expanded]: - - text: Thought for a while - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to reply with a single word. Let me comply.": - - img - - img - - text: Think The user wants me to reply with a single word. Let me comply. +- button "Copy" +- button "Thought for a while" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to reply with a single word. Let me comply." - paragraph: LIGHTHOUSE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."': - - img - - img - - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}." +- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."' - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.8K tok · Cache hit 99%": - - img - - text: 7.8K tokCache hit 99% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "7.8K tok · Cache hit 99%": 7.8K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/feedback-command/ack.expected.md b/snapshots/web/feedback-command/ack.expected.md index a7cc478f85..8de2c855f3 100644 --- a/snapshots/web/feedback-command/ack.expected.md +++ b/snapshots/web/feedback-command/ack.expected.md @@ -1,54 +1,29 @@ - banner: - navigation "Session hierarchy": - button "Reply with the single word" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: LIGHTHOUSE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."': - - img - - img - - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}." +- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."' - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.8K tok · Cache hit 99%": - - img - - text: 7.8K tokCache hit 99% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "7.8K tok · Cache hit 99%": 7.8K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/feedback-release/ack-expanded.expected.md b/snapshots/web/feedback-release/ack-expanded.expected.md index 04f10b71f7..679a7f2ecc 100644 --- a/snapshots/web/feedback-release/ack-expanded.expected.md +++ b/snapshots/web/feedback-release/ack-expanded.expected.md @@ -1,62 +1,31 @@ - banner: - navigation "Session hierarchy": - button "Reply with the single word" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while" [expanded]: - - text: Thought for a while - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to reply with a single word. Let me comply.": - - img - - img - - text: Think The user wants me to reply with a single word. Let me comply. +- button "Copy" +- button "Thought for a while" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to reply with a single word. Let me comply." - paragraph: LIGHTHOUSE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."': - - img - - img - - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}." +- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."' - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.8K tok · Cache hit 99%": - - img - - text: 7.8K tokCache hit 99% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "7.8K tok · Cache hit 99%": 7.8K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/feedback-release/ack.expected.md b/snapshots/web/feedback-release/ack.expected.md index a7cc478f85..8de2c855f3 100644 --- a/snapshots/web/feedback-release/ack.expected.md +++ b/snapshots/web/feedback-release/ack.expected.md @@ -1,54 +1,29 @@ - banner: - navigation "Session hierarchy": - button "Reply with the single word" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: LIGHTHOUSE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."': - - img - - img - - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}." +- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}."' - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.8K tok · Cache hit 99%": - - img - - text: 7.8K tokCache hit 99% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "7.8K tok · Cache hit 99%": 7.8K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/file-upload-round/ui.expected.md b/snapshots/web/file-upload-round/ui.expected.md index 35a8587b86..dd833a9103 100644 --- a/snapshots/web/file-upload-round/ui.expected.md +++ b/snapshots/web/file-upload-round/ui.expected.md @@ -1,56 +1,32 @@ - banner: - navigation "Session hierarchy": - button "Read the attached file with" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: poem.txt TXT 16B - button "reference-1.png, click to view original": - img "reference-1.png" - text: Read the attached file with the read tool, reply with exactly the single word it contains, and stop. {{clock}} -- button "Copy": - - img -- button "1 tool call": - - text: 1 tool call - - img +- button "Copy" +- button "1 tool call" - paragraph: UPLOAD_ROUND_OK -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Usage 17.3K tok": - - img - - text: Usage 17.3K tok -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Usage 17.3K tok" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash-Vision-Exp, reasoning effort high": - - text: DeepSeek-V4-Flash-Vision-Exp high - - img +- button "Select model, current DeepSeek-V4-Flash-Vision-Exp, reasoning effort high": DeepSeek-V4-Flash-Vision-Exp high - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "17.3K tok · Cache hit 51%": - - img - - text: 17.3K tokCache hit 51% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "17.3K tok · Cache hit 51%": 17.3K tokCache hit 51% - button "1% of context used": 1% diff --git a/snapshots/web/fresh-round-trip/ui-expanded.expected.md b/snapshots/web/fresh-round-trip/ui-expanded.expected.md index d8a1930f9d..59857b3a36 100644 --- a/snapshots/web/fresh-round-trip/ui-expanded.expected.md +++ b/snapshots/web/fresh-round-trip/ui-expanded.expected.md @@ -1,66 +1,32 @@ - banner: - navigation "Session hierarchy": - button "Use the bash tool to" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to run a simple bash command and reply with \"DONE\".": - - img - - img - - text: Think The user wants me to run a simple bash command and reply with "DONE". -- button "Bash Echo the test string": - - img - - img - - text: Bash Echo the test string -- button "Think The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\".": - - img - - img - - text: Think The command executed successfully and output "WEB_E2E_OK". I just need to reply with "DONE". +- button "Copy" +- button "1 tool call" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to run a simple bash command and reply with \"DONE\"." +- button "Bash Echo the test string" +- button "Think The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\"." - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tok · Cache hit 99%": - - img - - text: 15.8K tokCache hit 99% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tok · Cache hit 99%": 15.8K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/fresh-round-trip/ui.expected.md b/snapshots/web/fresh-round-trip/ui.expected.md index cad0bf3ea0..8fcd0762e2 100644 --- a/snapshots/web/fresh-round-trip/ui.expected.md +++ b/snapshots/web/fresh-round-trip/ui.expected.md @@ -1,50 +1,28 @@ - banner: - navigation "Session hierarchy": - button "Use the bash tool to" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "1 tool call": - - text: 1 tool call - - img +- button "Copy" +- button "1 tool call" - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tok · Cache hit 99%": - - img - - text: 15.8K tokCache hit 99% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tok · Cache hit 99%": 15.8K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md index 71df065b9c..0e399fa243 100644 --- a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md @@ -1,12 +1,9 @@ - banner: - navigation "Session hierarchy": - button "workspace" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" @@ -14,72 +11,23 @@ - button "Jump to turn 1" - button "Jump to turn 2" - group "Command input": /goal 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 -- 'button "goal Goal created Status: active Objective: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear"': - - img - - img - - text: "goal Goal created Status: active Objective: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear" -- button "System prompt": - - img - - img - - text: System prompt -- button "6 tool calls" [expanded]: - - text: 6 tool calls - - img -- button "Context injection goal": - - img - - img - - text: Context injection goal -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- 'button "Think The user''s goal is: \"做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的\" — which translates to: \"Do two turns, each turn output the file structure of a random package. Note that after you finish one turn, directly output the content and stop; our system will help you open another turn, and you just do something similar.\""': - - img - - img - - text: "Think The user's goal is: \"做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的\" — which translates to: \"Do two turns, each turn output the file structure of a random package. Note that after you finish one turn, directly output the content and stop; our system will help you open another turn, and you just do something similar.\"" -- button "Bash List package directories deterministically": - - img - - img - - text: Bash List package directories deterministically -- 'button "Tool call get_goal · {}"': - - img - - img - - text: "Tool call get_goal · {}" -- 'button "Think There''s a `packages` directory. Let me look inside to see what packages are available."': - - img - - img - - text: "Think There's a `packages` directory. Let me look inside to see what packages are available." -- button "Bash List packages directory contents": - - img - - img - - text: Bash List packages directory contents -- button "Think There are 4 packages:": - - img - - img - - text: "Think There are 4 packages:" -- button "Failed Bash Randomly pick one package directory": - - img - - text: Failed Bash Randomly pick one package directory -- 'button "Think shuf is not available on macOS. I''ll use a different approach for random selection. Let me use $RANDOM in bash or use `sort -R` or Python."': - - img - - img - - text: "Think shuf is not available on macOS. I'll use a different approach for random selection. Let me use $RANDOM in bash or use `sort -R` or Python." -- button "Bash Randomly select one package with Python": - - img - - img - - text: Bash Randomly select one package with Python -- 'button "Think The random pick for turn 1 is `packages/context/session-reference`. Now I''ll output its file structure. Let me get the file tree."': - - img - - img - - text: "Think The random pick for turn 1 is `packages/context/session-reference`. Now I'll output its file structure. Let me get the file tree." -- button "Bash List all files in session-reference package": - - img - - img - - text: Bash List all files in session-reference package -- 'button "Think Turn 1 is done. Per the objective: \"你做完一个turn之后,直接输出内容,停止\" — after finishing a turn, directly output the content and stop. The system will open another turn."': - - img - - img - - text: "Think Turn 1 is done. Per the objective: \"你做完一个turn之后,直接输出内容,停止\" — after finishing a turn, directly output the content and stop. The system will open another turn." +- 'button "goal Goal created Status: active Objective: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear"' +- button "System prompt" +- button "6 tool calls" [expanded] +- button "Context injection goal" +- button "Context injection @deepseek-ai/dsh-system-prompt" +- 'button "Think The user''s goal is: \"做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的\" — which translates to: \"Do two turns, each turn output the file structure of a random package. Note that after you finish one turn, directly output the content and stop; our system will help you open another turn, and you just do something similar.\""' +- button "Bash List package directories deterministically" +- 'button "Tool call get_goal · {}"' +- 'button "Think There''s a `packages` directory. Let me look inside to see what packages are available."' +- button "Bash List packages directory contents" +- button "Think There are 4 packages:" +- button "Failed Bash Randomly pick one package directory" +- 'button "Think shuf is not available on macOS. I''ll use a different approach for random selection. Let me use $RANDOM in bash or use `sort -R` or Python."' +- button "Bash Randomly select one package with Python" +- 'button "Think The random pick for turn 1 is `packages/context/session-reference`. Now I''ll output its file structure. Let me get the file tree."' +- button "Bash List all files in session-reference package" +- 'button "Think Turn 1 is done. Per the objective: \"你做完一个turn之后,直接输出内容,停止\" — after finishing a turn, directly output the content and stop. The system will open another turn."' - paragraph: - strong: Turn 1 / 2 - text: — 随机选中的包: @@ -90,53 +38,21 @@ - button "Copy" - code: "session-reference/ ├── README.md # 包说明文档 ├── package.json # 包元数据与依赖声明 ├── src/ │ ├── index.ts # 入口,导出公共 API │ └── uri.ts # 会话引用 URI 相关实现 └── tests/ └── session-reference.spec.ts # 单元测试" - paragraph: 这是一个很典型的轻量 TypeScript 包结构:入口 + 实现 + 测试。这一轮到此结束,等系统开启下一个 turn。 -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- button "System prompt": - - img - - img - - text: System prompt -- button "5 tool calls · 2 messages" [expanded]: - - text: 5 tool calls · 2 messages - - img -- button "Context injection goal": - - img - - img - - text: Context injection goal -- 'button "Think This is turn 2 of 2. I need to randomly pick another package and output its file structure. In the first turn I picked `packages/context/session-reference`. Let me pick a different one randomly this time."': - - img - - img - - text: "Think This is turn 2 of 2. I need to randomly pick another package and output its file structure. In the first turn I picked `packages/context/session-reference`. Let me pick a different one randomly this time." -- button "Bash Randomly select another package with Python": - - img - - img - - text: Bash Randomly select another package with Python -- 'button "Think The random pick returned the same package as turn 1. Since turn 1 already covered `session-reference`, it would be better to pick a different one for variety. Let me re-roll, excluding the already-covered package."': - - img - - img - - text: "Think The random pick returned the same package as turn 1. Since turn 1 already covered `session-reference`, it would be better to pick a different one for variety. Let me re-roll, excluding the already-covered package." -- button "Bash Re-roll excluding the package from turn 1": - - img - - img - - text: Bash Re-roll excluding the package from turn 1 -- button "Bash List all files in token-meter package": - - img - - img - - text: Bash List all files in token-meter package -- 'button "Think This is turn 2 of 2. I''ve output the file structure for `packages/llm/token-meter`. Both turns are done. I should output the content, stop, and then mark the goal as complete since both turns are finished."': - - img - - img - - text: "Think This is turn 2 of 2. I've output the file structure for `packages/llm/token-meter`. Both turns are done. I should output the content, stop, and then mark the goal as complete since both turns are finished." +- button "System prompt" +- button "5 tool calls · 2 messages" [expanded] +- button "Context injection goal" +- 'button "Think This is turn 2 of 2. I need to randomly pick another package and output its file structure. In the first turn I picked `packages/context/session-reference`. Let me pick a different one randomly this time."' +- button "Bash Randomly select another package with Python" +- 'button "Think The random pick returned the same package as turn 1. Since turn 1 already covered `session-reference`, it would be better to pick a different one for variety. Let me re-roll, excluding the already-covered package."' +- button "Bash Re-roll excluding the package from turn 1" +- button "Bash List all files in token-meter package" +- 'button "Think This is turn 2 of 2. I''ve output the file structure for `packages/llm/token-meter`. Both turns are done. I should output the content, stop, and then mark the goal as complete since both turns are finished."' - paragraph: - strong: Turn 2 / 2 - text: — 随机选中的包: @@ -152,19 +68,10 @@ - text: ,turn 2: - code: token-meter - text: )。这一轮到此结束。 -- 'button "Tool call get_goal · {}"': - - img - - img - - text: "Tool call get_goal · {}" +- 'button "Tool call get_goal · {}"' - paragraph: 两个 turn 均已完成,目标达成,标记 goal 为完成。 -- button "Tool call update_goal · goal-{{uuid}}": - - img - - img - - text: Tool call update_goal · goal-{{uuid}} -- 'button "Context injection tool-goal complete: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的"': - - img - - img - - text: "Context injection tool-goal complete: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的" +- button "Tool call update_goal · goal-{{uuid}}" +- 'button "Context injection tool-goal complete: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的"' - paragraph: 两个 turn 都完成了 ✅ - paragraph: - strong: 总结: @@ -213,32 +120,18 @@ - text: 、 - code: skill-filesystem - text: )的结构,或者其他格式的输出(比如带文件大小的树形图),随时告诉我。 -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- button "Back to bottom": - - img +- button "Back to bottom" - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "2 turns 12 steps · {{throughput}} tok/s": - - img - - text: 2 turns 12 steps{{throughput}} tok/s -- button "115K tok · Cache hit 91%": - - img - - text: 115K tokCache hit 91% +- button "2 turns 12 steps · {{throughput}} tok/s": 2 turns 12 steps{{throughput}} tok/s +- button "115K tok · Cache hit 91%": 115K tokCache hit 91% - button "9% of context used": 9% diff --git a/snapshots/web/goal-multi-turn-actions/ui.expected.md b/snapshots/web/goal-multi-turn-actions/ui.expected.md index 1743627de4..22d915bc3e 100644 --- a/snapshots/web/goal-multi-turn-actions/ui.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui.expected.md @@ -1,12 +1,9 @@ - banner: - navigation "Session hierarchy": - button "workspace" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" @@ -14,17 +11,9 @@ - button "Jump to turn 1" - button "Jump to turn 2" - group "Command input": /goal 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 -- 'button "goal Goal created Status: active Objective: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear"': - - img - - img - - text: "goal Goal created Status: active Objective: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear" -- button "System prompt": - - img - - img - - text: System prompt -- button "6 tool calls": - - text: 6 tool calls - - img +- 'button "goal Goal created Status: active Objective: 做两个turn,每个turn输出随机一个包的文件结构。注意你做完一个turn之后,直接输出内容,停止,我们的系统会帮你再开一个turn,你看着做一个类似的 Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear"' +- button "System prompt" +- button "6 tool calls" - paragraph: - strong: Turn 1 / 2 - text: — 随机选中的包: @@ -35,25 +24,14 @@ - button "Copy" - code: "session-reference/ ├── README.md # 包说明文档 ├── package.json # 包元数据与依赖声明 ├── src/ │ ├── index.ts # 入口,导出公共 API │ └── uri.ts # 会话引用 URI 相关实现 └── tests/ └── session-reference.spec.ts # 单元测试" - paragraph: 这是一个很典型的轻量 TypeScript 包结构:入口 + 实现 + 测试。这一轮到此结束,等系统开启下一个 turn。 -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- button "System prompt": - - img - - img - - text: System prompt -- button "5 tool calls · 2 messages": - - text: 5 tool calls · 2 messages - - img +- button "System prompt" +- button "5 tool calls · 2 messages" - paragraph: 两个 turn 都完成了 ✅ - paragraph: - strong: 总结: @@ -102,31 +80,18 @@ - text: 、 - code: skill-filesystem - text: )的结构,或者其他格式的输出(比如带文件大小的树形图),随时告诉我。 -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" - tooltip "Branch into a new conversation" -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "2 turns 12 steps · {{throughput}} tok/s": - - img - - text: 2 turns 12 steps{{throughput}} tok/s -- button "115K tok · Cache hit 91%": - - img - - text: 115K tokCache hit 91% +- button "2 turns 12 steps · {{throughput}} tok/s": 2 turns 12 steps{{throughput}} tok/s +- button "115K tok · Cache hit 91%": 115K tokCache hit 91% - button "9% of context used": 9% diff --git a/snapshots/web/lifecycle-chrome/connection-error.expected.md b/snapshots/web/lifecycle-chrome/connection-error.expected.md index d8526e9a57..6674078cb3 100644 --- a/snapshots/web/lifecycle-chrome/connection-error.expected.md +++ b/snapshots/web/lifecycle-chrome/connection-error.expected.md @@ -1,4 +1,2 @@ -- button "Settings": - - img - - text: Settings +- button "Settings" - button "Reconnecting, reconnect now": Reconnecting diff --git a/snapshots/web/lifecycle-chrome/hero.expected.md b/snapshots/web/lifecycle-chrome/hero.expected.md index 586271521d..fb7a487585 100644 --- a/snapshots/web/lifecycle-chrome/hero.expected.md +++ b/snapshots/web/lifecycle-chrome/hero.expected.md @@ -1,45 +1,25 @@ - button "New session" -- button "Collapse sidebar": - - img -- button "New session": - - img - - text: New Session +- button "Collapse sidebar" +- button "New session": New Session - navigation "Global panels": - button "Plugins" - text: Workspaces -- button "Search sessions": - - img +- button "Search sessions" - textbox "Search sessions..." -- button "View options": - - img -- button "Add workspace": - - img +- button "View options" +- button "Add workspace" - tree "Sessions": - - treeitem "workspace" [expanded]: - - img - - text: workspace + - treeitem "workspace" [expanded] - treeitem "New Session" [selected] -- button "Settings": - - img - - text: Settings +- button "Settings" - banner: - - button "Open right sidebar": - - img + - button "Open right sidebar" - text: Into the Unknown Preview -- button "Choose workspace": - - img - - text: workspace - - img -- button "Standard mode": - - img - - text: Standard mode - - img +- button "Choose workspace": workspace +- button "Standard mode" - textbox "Describe what you want to build, / commands, @ files or sessions": - paragraph -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] diff --git a/snapshots/web/lifecycle-chrome/plan-active.expected.md b/snapshots/web/lifecycle-chrome/plan-active.expected.md index 044f7ebb12..9f0c91f1c3 100644 --- a/snapshots/web/lifecycle-chrome/plan-active.expected.md +++ b/snapshots/web/lifecycle-chrome/plan-active.expected.md @@ -1,45 +1,25 @@ - button "New session" -- button "Collapse sidebar": - - img -- button "New session": - - img - - text: New Session +- button "Collapse sidebar" +- button "New session": New Session - navigation "Global panels": - button "Plugins" - text: Workspaces -- button "Search sessions": - - img +- button "Search sessions" - textbox "Search sessions..." -- button "View options": - - img -- button "Add workspace": - - img +- button "View options" +- button "Add workspace" - tree "Sessions": - - treeitem "workspace" [expanded]: - - img - - text: workspace + - treeitem "workspace" [expanded] - treeitem "New Session" [selected] -- button "Settings": - - img - - text: Settings +- button "Settings" - banner: - - button "Open right sidebar": - - img + - button "Open right sidebar" - text: Into the Unknown Preview -- button "Choose workspace": - - img - - text: workspace - - img -- button "Standard mode": - - img - - text: Standard mode - - img +- button "Choose workspace": workspace +- button "Standard mode" - textbox "Describe what you want to build, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write - button "Plan mode on, press to turn off": Plan -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] diff --git a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md index 58fca8bc9d..3b481f8cca 100644 --- a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md @@ -1,58 +1,30 @@ - banner: - navigation "Session hierarchy": - button "Reply with the single word" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while" [expanded]: - - text: Thought for a while - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to reply with a single word. Let me comply.": - - img - - img - - text: Think The user wants me to reply with a single word. Let me comply. +- button "Copy" +- button "Thought for a while" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to reply with a single word. Let me comply." - paragraph: LIGHTHOUSE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "10K tok · Cache hit 99.5%": - - img - - text: 10K tokCache hit 99.5% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "10K tok · Cache hit 99.5%": 10K tokCache hit 99.5% - button "8% of context used": 8% diff --git a/snapshots/web/lifecycle-chrome/reloaded.expected.md b/snapshots/web/lifecycle-chrome/reloaded.expected.md index 166c32248b..7c17dfc106 100644 --- a/snapshots/web/lifecycle-chrome/reloaded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded.expected.md @@ -1,50 +1,28 @@ - banner: - navigation "Session hierarchy": - button "Reply with the single word" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: LIGHTHOUSE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "10K tok · Cache hit 99.5%": - - img - - text: 10K tokCache hit 99.5% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "10K tok · Cache hit 99.5%": 10K tokCache hit 99.5% - button "8% of context used": 8% diff --git a/snapshots/web/live-interactions/cancel-expanded.expected.md b/snapshots/web/live-interactions/cancel-expanded.expected.md index 8cf1e25180..94a19be9f9 100644 --- a/snapshots/web/live-interactions/cancel-expanded.expected.md +++ b/snapshots/web/live-interactions/cancel-expanded.expected.md @@ -1,51 +1,28 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while" [expanded]: - - text: Thought for a while - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Thought for a while" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - text: Stopped -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps": - - img - - text: 1 turns 1 steps +- button "1 turns 1 steps" diff --git a/snapshots/web/live-interactions/cancel.expected.md b/snapshots/web/live-interactions/cancel.expected.md index 9abc3ddf5a..b5f6d26fd0 100644 --- a/snapshots/web/live-interactions/cancel.expected.md +++ b/snapshots/web/live-interactions/cancel.expected.md @@ -1,47 +1,27 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: partial - text: Stopped -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps": - - img - - text: 1 turns 1 steps +- button "1 turns 1 steps" diff --git a/snapshots/web/live-interactions/error-auth.expected.md b/snapshots/web/live-interactions/error-auth.expected.md index ada2676413..f9a7c1eb37 100644 --- a/snapshots/web/live-interactions/error-auth.expected.md +++ b/snapshots/web/live-interactions/error-auth.expected.md @@ -1,36 +1,22 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - status: - text: This turn failedAPI key is invalid - code: AUTH - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- img - text: 1 turns 1 steps diff --git a/snapshots/web/live-interactions/loading.expected.md b/snapshots/web/live-interactions/loading.expected.md index b5975fd0b5..4d5591c3d5 100644 --- a/snapshots/web/live-interactions/loading.expected.md +++ b/snapshots/web/live-interactions/loading.expected.md @@ -1,33 +1,20 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Stop generating" diff --git a/snapshots/web/live-interactions/retry-exhausted.expected.md b/snapshots/web/live-interactions/retry-exhausted.expected.md index 9d680e2941..d4a77f298a 100644 --- a/snapshots/web/live-interactions/retry-exhausted.expected.md +++ b/snapshots/web/live-interactions/retry-exhausted.expected.md @@ -1,38 +1,24 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - group: - status: Retried model request (2/2) · {{duration}} - status: - text: This turn failedupstream 503 - code: SERVER - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- img - text: 1 turns 1 steps diff --git a/snapshots/web/live-interactions/retry-expanded.expected.md b/snapshots/web/live-interactions/retry-expanded.expected.md index b797171a3b..0e6187e973 100644 --- a/snapshots/web/live-interactions/retry-expanded.expected.md +++ b/snapshots/web/live-interactions/retry-expanded.expected.md @@ -1,60 +1,32 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while" [expanded]: - - text: Thought for a while - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Thought for a while" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" - group: - status: Retried model request (1/5) · {{duration}} -- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.": - - img - - img - - text: Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls. +- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls." - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.9K tok · Cache hit 99%": - - img - - text: 7.9K tokCache hit 99% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "7.9K tok · Cache hit 99%": 7.9K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/live-interactions/retry.expected.md b/snapshots/web/live-interactions/retry.expected.md index 2cbb64219d..af9acdfd3a 100644 --- a/snapshots/web/live-interactions/retry.expected.md +++ b/snapshots/web/live-interactions/retry.expected.md @@ -1,50 +1,28 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.9K tok · Cache hit 99%": - - img - - text: 7.9K tokCache hit 99% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "7.9K tok · Cache hit 99%": 7.9K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/live-interactions/running-draft.expected.md b/snapshots/web/live-interactions/running-draft.expected.md index 62cb3ead70..077e94936c 100644 --- a/snapshots/web/live-interactions/running-draft.expected.md +++ b/snapshots/web/live-interactions/running-draft.expected.md @@ -1,34 +1,21 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - textbox "Message or run a task, / commands, @ files or sessions": - paragraph: Queue this follow-up while the current turn is running. -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Queue message" diff --git a/snapshots/web/markdown-file-links/preview.expected.md b/snapshots/web/markdown-file-links/preview.expected.md index ea54abc808..8d209fa3ae 100644 --- a/snapshots/web/markdown-file-links/preview.expected.md +++ b/snapshots/web/markdown-file-links/preview.expected.md @@ -1,6 +1,5 @@ - text: {{cwd}}/other/example.txt - button "Line wrap" [pressed]: - img -- button "Read the file again": - - img +- button "Read the file again" - text: other file diff --git a/snapshots/web/message-actions/fork.expected.md b/snapshots/web/message-actions/fork.expected.md index f7f9efb3ab..2adb2d0d6a 100644 --- a/snapshots/web/message-actions/fork.expected.md +++ b/snapshots/web/message-actions/fork.expected.md @@ -1,7 +1,5 @@ - tree "Sessions": - - treeitem "Ungrouped" [expanded]: - - img - - text: Ungrouped + - treeitem "Ungrouped" [expanded] - treeitem "Use the read tool twice (2) now" [selected] - treeitem "Use the read tool twice (1) now" - treeitem "Use the read tool twice 1min" diff --git a/snapshots/web/message-actions/ui.expected.md b/snapshots/web/message-actions/ui.expected.md index 2ea4938f10..98a48bb121 100644 --- a/snapshots/web/message-actions/ui.expected.md +++ b/snapshots/web/message-actions/ui.expected.md @@ -1,10 +1,8 @@ - banner: - navigation "Session hierarchy": - button "Use the read tool twice" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" @@ -12,88 +10,47 @@ - button "Jump to turn 1" - button "Jump to turn 2" - button "Jump to turn 3" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img +- button "Copy" - tooltip "Copy" -- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.": - - img - - img - - text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel. +- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel." - paragraph: I will read both files before answering. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation" [disabled]: - - img +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" [disabled] - text: Available only on the last message of a completed turn -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Ran for {{duration}}" - text: {{clock}} - button "Read a.txt": - - img - - img - text: Read - button "a.txt" - button "Read b.txt": - - img - - img - text: Read - button "b.txt" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Now give the final answer. {{clock}} -- button "Copy": - - img +- button "Copy" - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} Keep this later input in the original conversation. {{clock}} -- button "Copy": - - img +- button "Copy" - paragraph: ORIGINAL ONLY -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "3 turns 4 steps · {{throughput}} tok/s": - - img - - text: 3 turns 4 steps{{throughput}} tok/s -- button "7.9K tok · Cache hit 98%": - - img - - text: 7.9K tokCache hit 98% +- button "3 turns 4 steps · {{throughput}} tok/s": 3 turns 4 steps{{throughput}} tok/s +- button "7.9K tok · Cache hit 98%": 7.9K tokCache hit 98% diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md index cc49ac2618..7ad491e0cf 100644 --- a/snapshots/web/minimal-preset/ui.expected.md +++ b/snapshots/web/minimal-preset/ui.expected.md @@ -1,55 +1,31 @@ - banner: - navigation "Session hierarchy": - button "Use the bash tool to" [disabled] - - img - text: Minimal mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the bash tool to run exactly: printf 'MINIMAL_BASH_CARD_OK\\n'. Then reply exactly MINIMAL_PRESET_REQUEST_OK and stop. {{clock}}" -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Bash printf 'MINIMAL_BASH_CARD_OK\\n'" [expanded]: - - img - - text: Bash printf 'MINIMAL_BASH_CARD_OK\n' +- button "Copy" +- button "1 tool call" [expanded] +- button "Bash printf 'MINIMAL_BASH_CARD_OK\\n'" [expanded] - text: "IN { \"command\": \"printf 'MINIMAL_BASH_CARD_OK\\\\n'\" } OUT MINIMAL_BASH_CARD_OK [Command finished with exit code 0]" - button "Inspect" - paragraph: MINIMAL_PRESET_REQUEST_OK -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "28 tok · Cache hit 0%": - - img - - text: 28 tokCache hit 0% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "28 tok · Cache hit 0%": 28 tokCache hit 0% - button "0% of context used": 0% diff --git a/snapshots/web/navigation-panes/trajectory.expected.md b/snapshots/web/navigation-panes/trajectory.expected.md index 355d81e6cf..9eb80faeb3 100644 --- a/snapshots/web/navigation-panes/trajectory.expected.md +++ b/snapshots/web/navigation-panes/trajectory.expected.md @@ -2,7 +2,6 @@ - button "Use actual duration": Duration - button "Collapse turns": Turns - button "Collapse calls": Calls - - img - searchbox "Search trajectory" - region "Trajectory timeline" - table: diff --git a/snapshots/web/open-in-app-ssh/header.expected.md b/snapshots/web/open-in-app-ssh/header.expected.md index 12ecc45a49..bdbf2c4c21 100644 --- a/snapshots/web/open-in-app-ssh/header.expected.md +++ b/snapshots/web/open-in-app-ssh/header.expected.md @@ -1,10 +1,8 @@ - banner: - navigation "Session hierarchy": - button "Use the read tool twice" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/snapshots/web/plan-review/approved-expanded.expected.md b/snapshots/web/plan-review/approved-expanded.expected.md index 9114b312b6..1c7fce2311 100644 --- a/snapshots/web/plan-review/approved-expanded.expected.md +++ b/snapshots/web/plan-review/approved-expanded.expected.md @@ -1,75 +1,38 @@ - banner: - navigation "Session hierarchy": - 'button "Plan a small change: add" [disabled]' - - img - text: Standard mode - - button "More actions": - - img + - button "More actions" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- img - text: plan Plan mode on. Use /plan off to leave. -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "1 tool call · 1 message" [expanded]: - - text: 1 tool call · 1 message - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- 'button "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."': - - img - - img - - text: "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly." +- button "Copy" +- button "1 tool call · 1 message" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- 'button "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."' - paragraph: - text: Since the user has explicitly asked me not to read or write any files and to go straight to planning, I'll proceed with - code: exit_plan_mode - text: . -- 'button "Tool call exit_plan_mode · # Add `--greeting` flag to CLI"': - - img - - img - - text: "Tool call exit_plan_mode · # Add `--greeting` flag to CLI" -- button "System prompt": - - img - - img - - text: System prompt -- 'button "Think The plan was approved. The user''s last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."': - - img - - img - - text: "Think The plan was approved. The user's last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop." +- 'button "Tool call exit_plan_mode · # Add `--greeting` flag to CLI"' +- button "System prompt" +- 'button "Think The plan was approved. The user''s last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."' - paragraph: DONE - 'button "Open plan: Add `--greeting` flag to CLI"': "Add `--greeting` flag to CLI Plan · Markdown Open" -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "10.6K tok · Cache hit 51%": - - img - - text: 10.6K tokCache hit 51% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "10.6K tok · Cache hit 51%": 10.6K tokCache hit 51% - button "4% of context used": 4% diff --git a/snapshots/web/plan-review/approved.expected.md b/snapshots/web/plan-review/approved.expected.md index 96557e3e34..da51f924a1 100644 --- a/snapshots/web/plan-review/approved.expected.md +++ b/snapshots/web/plan-review/approved.expected.md @@ -1,55 +1,30 @@ - banner: - navigation "Session hierarchy": - 'button "Plan a small change: add" [disabled]' - - img - text: Standard mode - - button "More actions": - - img + - button "More actions" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- img - text: plan Plan mode on. Use /plan off to leave. -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "1 tool call · 1 message": - - text: 1 tool call · 1 message - - img -- button "System prompt": - - img - - img - - text: System prompt +- button "Copy" +- button "1 tool call · 1 message" +- button "System prompt" - paragraph: DONE - 'button "Open plan: Add `--greeting` flag to CLI"': "Add `--greeting` flag to CLI Plan · Markdown Open" -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "10.6K tok · Cache hit 51%": - - img - - text: 10.6K tokCache hit 51% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "10.6K tok · Cache hit 51%": 10.6K tokCache hit 51% - button "4% of context used": 4% diff --git a/snapshots/web/plan-review/preview.expected.md b/snapshots/web/plan-review/preview.expected.md index 8b9c5daeb5..1bddbaa901 100644 --- a/snapshots/web/plan-review/preview.expected.md +++ b/snapshots/web/plan-review/preview.expected.md @@ -1,6 +1,4 @@ - 'region "Add `--greeting` flag to CLI"': - - button "Copy": - - img - heading "Add --greeting flag to CLI" [level=1]: - text: Add - code: "--greeting" diff --git a/snapshots/web/plan-review/review.expected.md b/snapshots/web/plan-review/review.expected.md index 3cf92836c4..bae9089b17 100644 --- a/snapshots/web/plan-review/review.expected.md +++ b/snapshots/web/plan-review/review.expected.md @@ -1,12 +1,8 @@ - region "Approve this plan and leave plan mode?": - text: Plan review - - button "Open plan in sidebar": - - text: View full plan - - img + - button "Open plan in sidebar": View full plan - heading "Add --greeting flag to CLI" [level=3] - paragraph: Locate the CLI entry point (e.g., cli.py, main.go, index.js etc.) and find the argument parser definition (argparse, click, cobra, yargs, or similar). - status - - button "Request changes": - - img - - text: Request changes + - button "Request changes" - button "Approve" diff --git a/snapshots/web/plan-review/sidebar.expected.md b/snapshots/web/plan-review/sidebar.expected.md index 0e184c2f6f..c4ec636ee8 100644 --- a/snapshots/web/plan-review/sidebar.expected.md +++ b/snapshots/web/plan-review/sidebar.expected.md @@ -1 +1 @@ -- 'treeitem "Plan awaiting review Plan a small change: add now" [selected]' +- 'treeitem "Plan awaiting review Plan a small change: add" [selected]' diff --git a/snapshots/web/present-svg/ui.expected.md b/snapshots/web/present-svg/ui.expected.md index 7ad3d0f167..8d1982ff68 100644 --- a/snapshots/web/present-svg/ui.expected.md +++ b/snapshots/web/present-svg/ui.expected.md @@ -1,35 +1,16 @@ -- button "系统提示词": - - img - - img - - text: 系统提示词 +- button "系统提示词" - text: 简单画一个 SVG 表示冯诺依曼架构, 保存为 von-neumann.svg {{clock}} -- button "复制": - - img -- button "2 次工具调用 · 1 条消息" [expanded]: - - text: 2 次工具调用 · 1 条消息 - - img -- button "上下文注入 @deepseek-ai/dsh-system-prompt": - - img - - img - - text: 上下文注入 @deepseek-ai/dsh-system-prompt -- button "思考 The user wants a simple SVG diagram of the von Neumann architecture, saved as von-neumann.svg. Let me create a clean SVG showing CPU (control unit, ALU), memory, input, and output with bidirectional data/control buses.": - - img - - img - - text: 思考 The user wants a simple SVG diagram of the von Neumann architecture, saved as von-neumann.svg. Let me create a clean SVG showing CPU (control unit, ALU), memory, input, and output with bidirectional data/control buses. +- button "复制" +- button "2 次工具调用 · 1 条消息" [expanded] +- button "上下文注入 @deepseek-ai/dsh-system-prompt" +- button "思考 The user wants a simple SVG diagram of the von Neumann architecture, saved as von-neumann.svg. Let me create a clean SVG showing CPU (control unit, ALU), memory, input, and output with bidirectional data/control buses." - paragraph: 我来创建一个冯诺依曼架构的 SVG 示意图。 - button "写入 von-neumann.svg +83 -0": - - img - - img - text: 写入 - button "von-neumann.svg" - text: +83 -0 -- button "思考 The file is created. Let me present it as a deliverable.": - - img - - img - - text: 思考 The file is created. Let me present it as a deliverable. -- button "交付文件 已交付 von-neumann.svg": - - img - - text: 交付文件 已交付 von-neumann.svg +- button "思考 The file is created. Let me present it as a deliverable." +- button "交付文件 已交付 von-neumann.svg" - paragraph: - text: 已创建 - code: @@ -57,20 +38,11 @@ - button "在侧边栏预览 von-neumann.svg" - text: von-neumann.svg 冯诺依曼架构示意图 SVG - button "在侧边栏打开 von-neumann.svg": 打开 -- button "von-neumann.svg 的更多文件操作" [disabled]: - - img -- button "复制": - - img -- button "好的回答": - - img -- button "有问题的回答": - - img -- button "在新对话中分支": - - img -- button "用量 32K tok": - - img - - text: 用量 32K tok -- button "用时 {{duration}}": - - img - - text: 用时 {{duration}} +- button "von-neumann.svg 的更多文件操作" [disabled] +- button "复制" +- button "好的回答" +- button "有问题的回答" +- button "在新对话中分支" +- button "用量 32K tok" +- button "用时 {{duration}}" - text: {{clock}} diff --git a/snapshots/web/present/ui.expected.md b/snapshots/web/present/ui.expected.md index a4f851375e..e4c3ec81e6 100644 --- a/snapshots/web/present/ui.expected.md +++ b/snapshots/web/present/ui.expected.md @@ -1,51 +1,24 @@ - banner: - navigation "Session hierarchy": - button "Use one run_code program to" [disabled] - - img - text: PTC mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use one run_code program to do the following in order. Call present for missing.txt and catch its error without creating that file. Use bash to run exactly `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`. Call present for report.txt and 说明.txt. After present succeeds, deliberately throw the string \"AFTER_PRESENT\" (not an Error object) from that same run_code program. Do not retry the program or create any other files. Finish by mentioning `report.txt` and `说明.txt` in inline code, and put PRESENT_DONE in a separate paragraph. {{clock}}" -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants one run_code program that:": - - img - - img - - text: "Think The user wants one run_code program that:" +- button "Copy" +- button "1 tool call" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants one run_code program that:" - text: Failed -- 'button "Code Error: code run failed (exception): AFTER_PRESENT"': - - img - - text: "Code Error: code run failed (exception): AFTER_PRESENT" -- button "Present files Delivery failed missing.txt": - - img - - text: Present files Delivery failed missing.txt -- button "Bash Write DELIVERED_REPORT and DELIVERED_NOTE to files": - - img - - img - - text: Bash Write DELIVERED_REPORT and DELIVERED_NOTE to files -- button "Present files Delivered report.txt, 说明.txt": - - img - - text: Present files Delivered report.txt, 说明.txt -- button "Think The program ran as intended:": - - img - - img - - text: "Think The program ran as intended:" +- 'button "Code Error: code run failed (exception): AFTER_PRESENT"' +- button "Present files Delivery failed missing.txt" +- button "Bash Write DELIVERED_REPORT and DELIVERED_NOTE to files" +- button "Present files Delivered report.txt, 说明.txt" +- button "Think The program ran as intended:" - paragraph: - text: "The single program ran exactly as ordered:" - code: present @@ -67,41 +40,24 @@ - text: report.txt - status: Opened in default app - button "Open report.txt in sidebar": Open -- button "More file actions for report.txt": - - img +- button "More file actions for report.txt" - button "Preview 说明.txt in sidebar" - text: 说明.txt - status: Opened in default app - button "Open 说明.txt in sidebar": Open -- button "More file actions for 说明.txt": - - img -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Usage 19K tok": - - img - - text: Usage 19K tok -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "More file actions for 说明.txt" +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Usage 19K tok" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "19K tok · Cache hit 94%": - - img - - text: 19K tokCache hit 94% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "19K tok · Cache hit 94%": 19K tokCache hit 94% - button "8% of context used": 8% diff --git a/snapshots/web/ptc-round/code.expected.md b/snapshots/web/ptc-round/code.expected.md index 5e1d42ae59..1eb120612b 100644 --- a/snapshots/web/ptc-round/code.expected.md +++ b/snapshots/web/ptc-round/code.expected.md @@ -2,6 +2,5 @@ - text: Code typescript - button "Wrap lines" [pressed] - button "Original JSON" - - button "Copy code": - - img + - button "Copy code" - code: "const bashResult = await tools.bash({ command: \"echo CODE_ROUND_OK\", description: \"Echo CODE_ROUND_OK\" }); let readError = null; try { await tools.read({ file_path: \"missing.txt\" }); } catch (e) { readError = { toolName: e.toolName, message: e.message }; } return { bash: bashResult.stdout.text.trim(), readError };" diff --git a/snapshots/web/ptc-round/trajectory.expected.md b/snapshots/web/ptc-round/trajectory.expected.md index ed88d3b743..ff90616e70 100644 --- a/snapshots/web/ptc-round/trajectory.expected.md +++ b/snapshots/web/ptc-round/trajectory.expected.md @@ -1,18 +1,12 @@ - heading "Code typescript Wrap lines Copy code" [level=3]: - - button "Code": - - text: Code - - img + - button "Code" - text: typescript - button "Wrap lines" - - button "Copy code": - - img + - button "Copy code" - code: "const bashResult = await tools.bash({ command: \"echo CODE_ROUND_OK\", description: \"Echo CODE_ROUND_OK\" }); let readError = null; try { await tools.read({ file_path: \"missing.txt\" }); } catch (e) { readError = { toolName: e.toolName, message: e.message }; } return { bash: bashResult.stdout.text.trim(), readError };" - heading "Output Copy output" [level=3]: - - button "Output": - - text: Output - - img - - button "Copy output": - - img + - button "Output" + - button "Copy output" - text: "{" - tree "Result JSON": - treeitem "bash:\"CODE_ROUND_OK\"," diff --git a/snapshots/web/ptc-round/ui.expected.md b/snapshots/web/ptc-round/ui.expected.md index bccbc5de56..e9212ee443 100644 --- a/snapshots/web/ptc-round/ui.expected.md +++ b/snapshots/web/ptc-round/ui.expected.md @@ -1,77 +1,39 @@ - banner: - navigation "Session hierarchy": - 'button "Using ONE run_code program: run" [disabled]' - - img - text: PTC mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop. {{clock}}" -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- 'button "Think The user wants me to write a single `run_code` program that:"': - - img - - img - - text: "Think The user wants me to write a single `run_code` program that:" -- button "Code Run bash echo and catch missing file read": - - img - - img - - text: Code Run bash echo and catch missing file read -- button "Bash Echo CODE_ROUND_OK" [expanded]: - - img - - text: Bash Echo CODE_ROUND_OK +- button "Copy" +- button "1 tool call" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- 'button "Think The user wants me to write a single `run_code` program that:"' +- button "Code Run bash echo and catch missing file read" +- button "Bash Echo CODE_ROUND_OK" [expanded] - text: Done workspace echo CODE_ROUND_OK - button "Copy" - text: CODE_ROUND_OK - button "Inspect" - text: Failed -- 'button "Read Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found"': - - img - - text: "Read Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found" -- button "Think The program ran successfully. Let me now reply DONE as instructed.": - - img - - img - - text: Think The program ran successfully. Let me now reply DONE as instructed. +- 'button "Read Error: cannot read \"{{cwd}}/workspace/missing.txt\": not found"' +- button "Think The program ran successfully. Let me now reply DONE as instructed." - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "17.5K tok · Cache hit 52%": - - img - - text: 17.5K tokCache hit 52% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "17.5K tok · Cache hit 52%": 17.5K tokCache hit 52% - button "7% of context used": 7% diff --git a/snapshots/web/question-composer/answered-expanded.expected.md b/snapshots/web/question-composer/answered-expanded.expected.md index ee5075e44f..1da8b2f2cd 100644 --- a/snapshots/web/question-composer/answered-expanded.expected.md +++ b/snapshots/web/question-composer/answered-expanded.expected.md @@ -1,68 +1,35 @@ - banner: - navigation "Session hierarchy": - button "Use the ask_user_question tool to" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the ask_user_question tool to ask me exactly one multi-select question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" Set multi_select to true. After I answer, reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that.": - - img - - img - - text: Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that. -- button "Ask question 1/1 answered" [expanded]: - - img - - text: Ask question 1/1 answered +- button "Copy" +- button "1 tool call" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that." +- button "Ask question 1/1 answered" [expanded] - term: Which color do you prefer? - definition: Blue Include accessibility notes - button "Inspect" -- button "Think The user answered \"Blue\". I should now reply with the single word DONE and stop.": - - img - - img - - text: Think The user answered "Blue". I should now reply with the single word DONE and stop. +- button "Think The user answered \"Blue\". I should now reply with the single word DONE and stop." - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "8.8K tok · Cache hit 95%": - - img - - text: 8.8K tokCache hit 95% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "8.8K tok · Cache hit 95%": 8.8K tokCache hit 95% - button "3% of context used": 3% diff --git a/snapshots/web/question-composer/answered.expected.md b/snapshots/web/question-composer/answered.expected.md index eff1b3a6dc..d865c5f8e6 100644 --- a/snapshots/web/question-composer/answered.expected.md +++ b/snapshots/web/question-composer/answered.expected.md @@ -1,50 +1,28 @@ - banner: - navigation "Session hierarchy": - button "Use the ask_user_question tool to" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the ask_user_question tool to ask me exactly one multi-select question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" Set multi_select to true. After I answer, reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "1 tool call": - - text: 1 tool call - - img +- button "Copy" +- button "1 tool call" - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "8.8K tok · Cache hit 95%": - - img - - text: 8.8K tokCache hit 95% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "8.8K tok · Cache hit 95%": 8.8K tokCache hit 95% - button "3% of context used": 3% diff --git a/snapshots/web/question-composer/cancelled.expected.md b/snapshots/web/question-composer/cancelled.expected.md index c8303cc73a..97591ecba3 100644 --- a/snapshots/web/question-composer/cancelled.expected.md +++ b/snapshots/web/question-composer/cancelled.expected.md @@ -1,47 +1,26 @@ - banner: - navigation "Session hierarchy": - button "Use the ask_user_question tool to" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the ask_user_question tool to ask me exactly one multi-select question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" Set multi_select to true. After I answer, reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that.": - - img - - img - - text: Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that. -- button "Ask question cancelled" [expanded]: - - img - - text: Ask question cancelled +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that." +- button "Ask question cancelled" [expanded] - paragraph: This question set was cancelled before answers were submitted. - list: - listitem: Which color do you prefer? - button "Inspect" - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "4.4K tok · Cache hit 97%": - - img - - text: 4.4K tokCache hit 97% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "4.4K tok · Cache hit 97%": 4.4K tokCache hit 97% - button "3% of context used": 3% diff --git a/snapshots/web/question-composer/composed.expected.md b/snapshots/web/question-composer/composed.expected.md index a3e655e8b9..2b62217f60 100644 --- a/snapshots/web/question-composer/composed.expected.md +++ b/snapshots/web/question-composer/composed.expected.md @@ -1,19 +1,15 @@ - region "Which color do you prefer?": - text: Pick one - heading "Which color do you prefer?" [level=2] - - button "Collapse the question card" [expanded]: - - img - - button "Dismiss all questions": - - img + - button "Collapse the question card" [expanded] + - button "Dismiss all questions" - group: - checkbox "Blue" [checked]: Blue A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards. - checkbox "Green": Green A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions. - textbox "Type your answer": Include accessibility notes - - button "Previous question" [disabled]: - - img + - button "Previous question" [disabled] - text: 1 / 1 - - button "Next question" [disabled]: - - img + - button "Next question" [disabled] - status - button "Skip" - button "Submit" diff --git a/snapshots/web/question-composer/sidebar.expected.md b/snapshots/web/question-composer/sidebar.expected.md index fcc2849e0f..51f99a85bc 100644 --- a/snapshots/web/question-composer/sidebar.expected.md +++ b/snapshots/web/question-composer/sidebar.expected.md @@ -1 +1 @@ -- treeitem "Waiting for answer Use the ask_user_question tool to now" [selected] +- treeitem "Waiting for answer Use the ask_user_question tool to" [selected] diff --git a/snapshots/web/question-composer/ui.expected.md b/snapshots/web/question-composer/ui.expected.md index b86e6c0223..e975922565 100644 --- a/snapshots/web/question-composer/ui.expected.md +++ b/snapshots/web/question-composer/ui.expected.md @@ -1,19 +1,15 @@ - region "Which color do you prefer?": - text: Pick one - heading "Which color do you prefer?" [level=2] - - button "Collapse the question card" [expanded]: - - img - - button "Dismiss all questions": - - img + - button "Collapse the question card" [expanded] + - button "Dismiss all questions" - group: - checkbox "Blue": Blue A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards. - checkbox "Green": Green A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions. - textbox "Type your answer" - - button "Previous question" [disabled]: - - img + - button "Previous question" [disabled] - text: 1 / 1 - - button "Next question" [disabled]: - - img + - button "Next question" [disabled] - status - button "Skip" - button "Submit" [disabled] diff --git a/snapshots/web/queue-actions/collapsed.expected.md b/snapshots/web/queue-actions/collapsed.expected.md index 2586c98553..e34a92efa9 100644 --- a/snapshots/web/queue-actions/collapsed.expected.md +++ b/snapshots/web/queue-actions/collapsed.expected.md @@ -1,34 +1,21 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - button "2 queued messages" - textbox "Cmd/Ctrl+Enter steers all queued messages" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Stop generating" diff --git a/snapshots/web/queue-actions/editing.expected.md b/snapshots/web/queue-actions/editing.expected.md index 667235bee2..1d790c53f4 100644 --- a/snapshots/web/queue-actions/editing.expected.md +++ b/snapshots/web/queue-actions/editing.expected.md @@ -1,50 +1,32 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - button "2 queued messages" [disabled] [expanded] - list: - listitem: - text: Queue item to remove - - button "Edit queued message": - - img - - button "Remove queued message": - - img - - button "Steer queued message": - - img + - button "Edit queued message" + - button "Remove queued message" + - button "Steer queued message" - listitem: - textbox "Edit queued message": Edited queue item - - button "Save queued message": - - img + - button "Save queued message" - tooltip "Save queued message" - - button "Cancel editing": - - img + - button "Cancel editing" - textbox "Cmd/Ctrl+Enter steers all queued messages" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Stop generating" diff --git a/snapshots/web/queue-actions/failed.expected.md b/snapshots/web/queue-actions/failed.expected.md index 044a159013..feaae8172d 100644 --- a/snapshots/web/queue-actions/failed.expected.md +++ b/snapshots/web/queue-actions/failed.expected.md @@ -1,10 +1,7 @@ - alert: Queue submission failed (session/agent-busy) - textbox "Message or run a task, / commands, @ files or sessions": - paragraph: Queue submission to retry -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Queue message" diff --git a/snapshots/web/queue-actions/layout.expected.md b/snapshots/web/queue-actions/layout.expected.md index eeea3d4726..ac54c9cb25 100644 --- a/snapshots/web/queue-actions/layout.expected.md +++ b/snapshots/web/queue-actions/layout.expected.md @@ -1,50 +1,28 @@ - banner: - navigation "Session hierarchy": - button "workspace" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - group "Command input": /goal Keep the composer context panels aligned -- 'button "goal Goal created Status: active Objective: Keep the composer context panels aligned Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear"': - - img - - img - - text: "goal Goal created Status: active Objective: Keep the composer context panels aligned Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear" -- button "System prompt": - - img - - img - - text: System prompt -- button "Context injection goal": - - img - - img - - text: Context injection goal -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- 'button "goal Goal created Status: active Objective: Keep the composer context panels aligned Rounds: 0/256 Activation: armed Commands: /goal edit , /goal pause, /goal clear"' +- button "System prompt" +- button "Context injection goal" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - region "To-dos": - button "To-dos 1 completed · 1 in progress" -- img - text: Ongoing Goal Keep the composer context panels aligned -- button "Pause goal": - - img -- button "Edit goal": - - img -- button "Clear goal": - - img +- button "Pause goal" +- button "Edit goal" +- button "Clear goal" - button "2 queued messages" - textbox "Cmd/Ctrl+Enter steers all queued messages" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Stop generating" diff --git a/snapshots/web/queue-actions/preserved-expanded.expected.md b/snapshots/web/queue-actions/preserved-expanded.expected.md index 37f6e40af0..22d8b78588 100644 --- a/snapshots/web/queue-actions/preserved-expanded.expected.md +++ b/snapshots/web/queue-actions/preserved-expanded.expected.md @@ -1,69 +1,40 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while" [expanded]: - - text: Thought for a while - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Thought for a while" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - text: Stopped -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - button "2 queued messages" [expanded] - list: - listitem: - text: Edited queue item - - button "Edit queued message": - - img - - button "Remove queued message": - - img - - button "Steer queued message" [disabled]: - - img + - button "Edit queued message" + - button "Remove queued message" + - button "Steer queued message" [disabled] - listitem: - text: Queue item preserved after stop - - button "Edit queued message": - - img - - button "Remove queued message": - - img - - button "Steer queued message" [disabled]: - - img + - button "Edit queued message" + - button "Remove queued message" + - button "Steer queued message" [disabled] - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps": - - img - - text: 1 turns 1 steps +- button "1 turns 1 steps" diff --git a/snapshots/web/queue-actions/preserved.expected.md b/snapshots/web/queue-actions/preserved.expected.md index 8de751f99d..6864b1fc3d 100644 --- a/snapshots/web/queue-actions/preserved.expected.md +++ b/snapshots/web/queue-actions/preserved.expected.md @@ -1,65 +1,39 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: partial - text: Stopped -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - button "2 queued messages" [expanded] - list: - listitem: - text: Edited queue item - - button "Edit queued message": - - img - - button "Remove queued message": - - img - - button "Steer queued message" [disabled]: - - img + - button "Edit queued message" + - button "Remove queued message" + - button "Steer queued message" [disabled] - listitem: - text: Queue item preserved after stop - - button "Edit queued message": - - img - - button "Remove queued message": - - img - - button "Steer queued message" [disabled]: - - img + - button "Edit queued message" + - button "Remove queued message" + - button "Steer queued message" [disabled] - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 1 steps": - - img - - text: 1 turns 1 steps +- button "1 turns 1 steps" diff --git a/snapshots/web/queue-actions/sending.expected.md b/snapshots/web/queue-actions/sending.expected.md index 0d1b88c45f..7d3eb1c9c4 100644 --- a/snapshots/web/queue-actions/sending.expected.md +++ b/snapshots/web/queue-actions/sending.expected.md @@ -1,43 +1,27 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - list: - listitem: - text: Queue item to remove - status: Sending… - - button "Edit queued message" [disabled]: - - img - - button "Remove queued message" [disabled]: - - img - - button "Steer queued message" [disabled]: - - img + - button "Edit queued message" [disabled] + - button "Remove queued message" [disabled] + - button "Steer queued message" [disabled] - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Stop generating" diff --git a/snapshots/web/queue-actions/ui.expected.md b/snapshots/web/queue-actions/ui.expected.md index 5f9dd0ffa0..35cfdc5eb4 100644 --- a/snapshots/web/queue-actions/ui.expected.md +++ b/snapshots/web/queue-actions/ui.expected.md @@ -1,43 +1,27 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - list: - listitem: - text: Edited queue item - - button "Edit queued message": - - img + - button "Edit queued message" - tooltip "Edit queued message" - - button "Remove queued message": - - img - - button "Steer queued message": - - img + - button "Remove queued message" + - button "Steer queued message" - textbox "Cmd/Ctrl+Enter steers all queued messages" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Stop generating" diff --git a/snapshots/web/queue-actions/writer-held.expected.md b/snapshots/web/queue-actions/writer-held.expected.md index 1542885bb3..dd3d6e76c7 100644 --- a/snapshots/web/queue-actions/writer-held.expected.md +++ b/snapshots/web/queue-actions/writer-held.expected.md @@ -1,10 +1,7 @@ - alert: This session is already in use, possibly by another running DSH instance (such as dsh web or the desktop app). Quit other running DSH instances and try again. - textbox "Message or run a task, / commands, @ files or sessions": - paragraph: Queue submission to retry -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Queue message" diff --git a/snapshots/web/queued-image/delivered.expected.md b/snapshots/web/queued-image/delivered.expected.md index f9d29029ac..d2ebeeb0bb 100644 --- a/snapshots/web/queued-image/delivered.expected.md +++ b/snapshots/web/queued-image/delivered.expected.md @@ -1,12 +1,9 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" @@ -14,81 +11,44 @@ - button "Jump to turn 1" - button "Jump to turn 2" - button "Jump to turn 3" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: partial - text: Stopped -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - button "queued.png, click to view original": - img "queued.png" - text: Compare with this screenshot {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} Continue with the queued comparison {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "3 turns 3 steps · {{throughput}} tok/s": - - img - - text: 3 turns 3 steps{{throughput}} tok/s -- button "15.7K tok · Cache hit 99%": - - img - - text: 15.7K tokCache hit 99% +- button "3 turns 3 steps · {{throughput}} tok/s": 3 turns 3 steps{{throughput}} tok/s +- button "15.7K tok · Cache hit 99%": 15.7K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/queued-image/queued.expected.md b/snapshots/web/queued-image/queued.expected.md index 3b18b54846..f74ae43b62 100644 --- a/snapshots/web/queued-image/queued.expected.md +++ b/snapshots/web/queued-image/queued.expected.md @@ -1,43 +1,27 @@ - banner: - navigation "Session hierarchy": - button "Reply with a one-sentence description" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - list: - listitem: - img "Queued message image" - text: Compare with this screenshot - - button "Edit queued message" [disabled]: - - img - - button "Remove queued message": - - img - - button "Steer queued message": - - img + - button "Edit queued message" [disabled] + - button "Remove queued message" + - button "Steer queued message" - textbox "Cmd/Ctrl+Enter steers all queued messages" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Stop generating" diff --git a/snapshots/web/seeded-history/command-row.expected.md b/snapshots/web/seeded-history/command-row.expected.md index 4937ff0bcf..63052fbda9 100644 --- a/snapshots/web/seeded-history/command-row.expected.md +++ b/snapshots/web/seeded-history/command-row.expected.md @@ -1,75 +1,40 @@ - banner: - navigation "Session hierarchy": - button "Use the read tool twice" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - navigation "Turn navigation": - button "Jump to turn 1" - button "Jump to turn 2" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "2 tool calls" [expanded]: - - text: 2 tool calls - - img -- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.": - - img - - img - - text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel. +- button "Copy" +- button "2 tool calls" [expanded] +- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel." - button "Read a.txt": - - img - - img - text: Read - button "a.txt" - button "Read b.txt": - - img - - img - text: Read - button "b.txt" -- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed.": - - img - - img - - text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed. +- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed." - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" -- button "Context injection AGENTS.md": - - img - - img - - text: Context injection AGENTS.md -- img +- button "Context injection AGENTS.md" - text: permission preset read-only - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Read Only"': Read Only -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "16K tok · Cache hit 98%": - - img - - text: 16K tokCache hit 98% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "16K tok · Cache hit 98%": 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/feedback-row.expected.md b/snapshots/web/seeded-history/feedback-row.expected.md index 1ce614ef2f..9238eb5070 100644 --- a/snapshots/web/seeded-history/feedback-row.expected.md +++ b/snapshots/web/seeded-history/feedback-row.expected.md @@ -1,79 +1,42 @@ - banner: - navigation "Session hierarchy": - button "Use the read tool twice" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - navigation "Turn navigation": - button "Jump to turn 1" - button "Jump to turn 2" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "2 tool calls" [expanded]: - - text: 2 tool calls - - img -- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.": - - img - - img - - text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel. +- button "Copy" +- button "2 tool calls" [expanded] +- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel." - button "Read a.txt": - - img - - img - text: Read - button "a.txt" - button "Read b.txt": - - img - - img - text: Read - button "b.txt" -- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed.": - - img - - img - - text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed. +- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed." - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" -- button "Context injection AGENTS.md": - - img - - img - - text: Context injection AGENTS.md -- img +- button "Context injection AGENTS.md" - text: permission preset read-only -- 'button "feedback Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}." [expanded]': - - img - - text: "feedback Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}." +- 'button "feedback Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}." [expanded]' - text: "Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}." - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Read Only"': Read Only -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "16K tok · Cache hit 98%": - - img - - text: 16K tokCache hit 98% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "16K tok · Cache hit 98%": 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/file-preview.expected.md b/snapshots/web/seeded-history/file-preview.expected.md index d11b2479b2..00136c7ca7 100644 --- a/snapshots/web/seeded-history/file-preview.expected.md +++ b/snapshots/web/seeded-history/file-preview.expected.md @@ -1,6 +1,5 @@ - text: {{cwd}}/a.txt - button "Line wrap" [pressed]: - img -- button "Read the file again": - - img +- button "Read the file again" - text: alpha diff --git a/snapshots/web/seeded-history/thinking-expanded.expected.md b/snapshots/web/seeded-history/thinking-expanded.expected.md index cc2dd82a38..9ead791f50 100644 --- a/snapshots/web/seeded-history/thinking-expanded.expected.md +++ b/snapshots/web/seeded-history/thinking-expanded.expected.md @@ -1,6 +1,4 @@ -- button "Think" [expanded]: - - img - - text: Think +- button "Think" [expanded] - paragraph: The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel. - Secondary font size: true diff --git a/snapshots/web/seeded-history/ui-expanded.expected.md b/snapshots/web/seeded-history/ui-expanded.expected.md index f23b05cfbb..b7606e8776 100644 --- a/snapshots/web/seeded-history/ui-expanded.expected.md +++ b/snapshots/web/seeded-history/ui-expanded.expected.md @@ -1,73 +1,39 @@ - banner: - navigation "Session hierarchy": - button "Use the read tool twice" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - navigation "Turn navigation": - button "Jump to turn 1" - button "Jump to turn 2" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "2 tool calls" [expanded]: - - text: 2 tool calls - - img -- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.": - - img - - img - - text: Think The user wants me to read a.txt and b.txt, then reply with "DONE". Let me do both reads in parallel. +- button "Copy" +- button "2 tool calls" [expanded] +- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel." - button "Read a.txt": - - img - - img - text: Read - button "a.txt" - button "Read b.txt": - - img - - img - text: Read - button "b.txt" -- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed.": - - img - - img - - text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed. +- button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed." - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" -- button "Context injection AGENTS.md": - - img - - img - - text: Context injection AGENTS.md +- button "Context injection AGENTS.md" - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "16K tok · Cache hit 98%": - - img - - text: 16K tokCache hit 98% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "16K tok · Cache hit 98%": 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/ui.expected.md b/snapshots/web/seeded-history/ui.expected.md index 1061d02a91..9a40a350d9 100644 --- a/snapshots/web/seeded-history/ui.expected.md +++ b/snapshots/web/seeded-history/ui.expected.md @@ -1,55 +1,31 @@ - banner: - navigation "Session hierarchy": - button "Use the read tool twice" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - navigation "Turn navigation": - button "Jump to turn 1" - button "Jump to turn 2" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" -- button "Copy": - - img -- button "2 tool calls": - - text: 2 tool calls - - img +- button "Copy" +- button "2 tool calls" - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" -- button "Context injection AGENTS.md": - - img - - img - - text: Context injection AGENTS.md +- button "Context injection AGENTS.md" - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "16K tok · Cache hit 98%": - - img - - text: 16K tokCache hit 98% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "16K tok · Cache hit 98%": 16K tokCache hit 98% diff --git a/snapshots/web/sidebar-scrollbar/geometry.expected.md b/snapshots/web/sidebar-scrollbar/geometry.expected.md index e58a41d358..fa9397bdc2 100644 --- a/snapshots/web/sidebar-scrollbar/geometry.expected.md +++ b/snapshots/web/sidebar-scrollbar/geometry.expected.md @@ -4,7 +4,7 @@ - --dsh-scrollbar-thumb, pointer outside the sidebar: rgba(0, 0, 0, 0) - scrollbar-gutter: stable -- ::-webkit-scrollbar width: 8px +- ::-webkit-scrollbar width: 5px - ::-webkit-scrollbar-track background: rgba(0, 0, 0, 0) - scrollbar-width: auto - scrollbar-color: auto @@ -12,7 +12,7 @@ - --dsh-scrollbar-thumb, pointer over the list: rgb(229, 229, 229) - --dsh-scrollbar-thumb-hover, pointer over the list: rgb(212, 212, 212) - list overflows: true -- reserved band: 8px +- reserved band: 5px - scrollbar inset from the sidebar edge: 2px - row background inset from the sidebar edge: 12px - relative time covered by the bar: 0px @@ -23,7 +23,7 @@ - --dsh-scrollbar-thumb, pointer outside the sidebar: rgba(0, 0, 0, 0) - scrollbar-gutter: stable -- ::-webkit-scrollbar width: 8px +- ::-webkit-scrollbar width: 5px - ::-webkit-scrollbar-track background: rgba(0, 0, 0, 0) - scrollbar-width: auto - scrollbar-color: auto @@ -31,7 +31,7 @@ - --dsh-scrollbar-thumb, pointer over the list: rgb(84, 85, 87) - --dsh-scrollbar-thumb-hover, pointer over the list: rgb(101, 103, 107) - list overflows: true -- reserved band: 8px +- reserved band: 5px - scrollbar inset from the sidebar edge: 2px - row background inset from the sidebar edge: 12px - relative time covered by the bar: 0px diff --git a/snapshots/web/skill-tool-row/ui.expected.md b/snapshots/web/skill-tool-row/ui.expected.md index 45dc6d1c61..d3dfcb56f3 100644 --- a/snapshots/web/skill-tool-row/ui.expected.md +++ b/snapshots/web/skill-tool-row/ui.expected.md @@ -1,68 +1,33 @@ - banner: - navigation "Session hierarchy": - button "Load the editing-cordis-compositions ski" [disabled] - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Load the editing-cordis-compositions skill with the skill tool, then reply DONE. {{date}} {{clock}} -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Context injection skill-catalog": - - img - - img - - text: Context injection skill-catalog -- button "Think Load the requested skill.": - - img - - img - - text: Think Load the requested skill. -- button "Skill editing-cordis-compositions" [expanded]: - - img - - text: Skill editing-cordis-compositions +- button "Copy" +- button "1 tool call" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Context injection skill-catalog" +- button "Think Load the requested skill." +- button "Skill editing-cordis-compositions" [expanded] - region "Instructions": "Instructions Base directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. # Editing Cordis compositions Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it. ## Off-limits **Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `ptc`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation. To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete. ## Decide the plane first Two planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared. **Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process. **Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it. **A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side. A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. For deployment overrides, read `${DSH_HOME:-$HOME/.dsh}/profiles//cordis.patch.yml`, `${DSH_HOME:-$HOME/.dsh}/cordis.patch.yml`, and any launch `--patch` files for the `agent-presets` row's `roots` and `includeUserRoot`. Obtain the active profile and extra patch paths from the user when they are not in the task context; do not guess a different profile. ## Authoring a preset Use shell and file tools to locate the installed `@deepseek-ai/dsh-agent-presets` package under the active profile's `node_modules` or the deployment installation. Its `presets//` directory contains each shipped preset. If those files are unavailable, ask the user to copy the preset through the Web preset picker and provide the copied directory; runtime API inspection does not execute Remote methods. Copy the complete source directory, including skills and assets, into a new `${DSH_HOME:-$HOME/.dsh}/.agent-presets//` directory (or the explicitly configured writable root). Refuse an existing destination. Set `name` and `description` in `preset.yml` and remove the copied roster `order`. Never overwrite the installed source. Edit the copy's `agent.cordis.yml` with the normal file tools. Writes outside the workspace follow the active filesystem approval policy. For profile-wide capabilities, author a workspace bundle and install it with `plugin_manager`; load `cordis-plugin-development` for packaging guidance. ## Service isolation A preset row that publishes a service needs an `isolate` realm containing both the provider and all its consumers. A tool that only consumes a host service remains outside that realm. Copy the shipped preset's existing groups rather than introducing service instances into the process-global realm. ```yaml - id: delegation name: cordis:group group: true isolate: workflowEngine: true config: - id: workflow-ptc name: '@deepseek-ai/dsh-workflow-ptc' config: provider: spawn - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' ``` ## Verify a preset Check the edited YAML and referenced local files with file tools. Ask the user to select the new preset in the Web picker and start a session; this authoring agent cannot invoke the preset Remote API or start a Web session through inspection. Once the user provides a running session or browser control, inspect the visible tools and any activation diagnostic. File validation alone does not verify imports, service dependencies, or isolation. Report which checks ran and leave activation unverified until that session check succeeds. ## Native product subagents Codex and Claude Code providers are independent optional Profile Bundles. Install only the products a Profile needs, then restart the Profile so its Host registers those providers: ```sh dsh plugin --profile add @deepseek-ai/dsh-subagent-codex dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code ``` Each Bundle owns its Host availability; the preset separately grants one Agent its ordinary delegation tool. Never move a product provider into the preset and never add a product-specific settings field. Removing one package withdraws only that provider on the next Profile start. Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested: ```yaml - id: tool-subagent-codex name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: codex toolName: subagent_codex backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: claude-code toolName: subagent_claude_code backgroundMode: one-shot maxDepth: provider-managed ``` For additional named Codex or Claude Code instances, mount a separate host-plane provider row for each instance with a unique `providerName`, then add a separate preset tool row whose `provider` exactly matches that name and whose `toolName` is also unique. Keep the shipped rows for the default `codex` and `claude-code` names; do not reuse one tool row for several providers or derive either name from permission or environment settings. The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. Production `dsh` does not install either optional provider: before enabling a row, install the matching `@deepseek-ai/dsh-subagent-codex` or `@deepseek-ai/dsh-subagent-claude-code` Bundle in the Profile and restart it. Each Bundle registers its dormant default provider and exclusively uses its pinned package-local platform CLI; additional named instances use extra host-plane rows from the same installed package. A preset cannot provide that host dependency. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Installing a Bundle or composing a preset row does not start a product, authenticate an account, select a model, probe credentials, or manage native product settings. ## What not to move into a preset `agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement. " - button "Inspect" -- button "Think The skill is loaded.": - - img - - img - - text: Think The skill is loaded. +- button "Think The skill is loaded." - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{date}} {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Full access"': Full access -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps": - - img - - text: 1 turns 2 steps -- button "310 tok · Cache hit 0%": - - img - - text: 310 tokCache hit 0% +- button "1 turns 2 steps" +- button "310 tok · Cache hit 0%": 310 tokCache hit 0% diff --git a/snapshots/web/steering/mid-steer.expected.md b/snapshots/web/steering/mid-steer.expected.md index 16fec2b6ba..755176ac89 100644 --- a/snapshots/web/steering/mid-steer.expected.md +++ b/snapshots/web/steering/mid-steer.expected.md @@ -1,51 +1,33 @@ - banner: - navigation "Session hierarchy": - button "Use the ask_user_question tool to" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - text: Running -- button "Ask question waiting": - - img - - img - - text: Ask question waiting +- button "Ask question waiting" - status: Deep diving... - text: "Interjection: include the word BANANA in your final reply." -- button "Copy": - - img +- button "Copy" - region "Ready to continue?": - text: Checkpoint - heading "Ready to continue?" [level=2] - - button "Collapse the question card" [expanded]: - - img - - button "Dismiss all questions": - - img + - button "Collapse the question card" [expanded] + - button "Dismiss all questions" - radiogroup: - radio "Yes": 1 Yes - radio "No": 2 No - textbox "Type your answer" - - button "Previous question" [disabled]: - - img + - button "Previous question" [disabled] - text: 1 / 1 - - button "Next question" [disabled]: - - img + - button "Next question" [disabled] - status - button "Skip" - button "Submit" [disabled] diff --git a/snapshots/web/steering/settled-expanded.expected.md b/snapshots/web/steering/settled-expanded.expected.md index 3205de8a9b..dab18a04b1 100644 --- a/snapshots/web/steering/settled-expanded.expected.md +++ b/snapshots/web/steering/settled-expanded.expected.md @@ -1,61 +1,32 @@ - banner: - navigation "Session hierarchy": - button "Use the ask_user_question tool to" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Ask question 1/1 answered": - - img - - img - - text: Ask question 1/1 answered +- button "Copy" +- button "1 tool call" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Ask question 1/1 answered" - text: "Interjection: include the word BANANA in your final reply. {{clock}}" -- button "Copy": - - img +- button "Copy" - paragraph: Great, let's move forward. BANANA! -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "30 tok · Cache hit 0%": - - img - - text: 30 tokCache hit 0% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "30 tok · Cache hit 0%": 30 tokCache hit 0% - button "0% of context used": 0% diff --git a/snapshots/web/steering/settled.expected.md b/snapshots/web/steering/settled.expected.md index 8d4e136199..d327666fc1 100644 --- a/snapshots/web/steering/settled.expected.md +++ b/snapshots/web/steering/settled.expected.md @@ -1,53 +1,30 @@ - banner: - navigation "Session hierarchy": - button "Use the ask_user_question tool to" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} -- button "Copy": - - img -- button "1 tool call": - - text: 1 tool call - - img +- button "Copy" +- button "1 tool call" - text: "Interjection: include the word BANANA in your final reply. {{clock}}" -- button "Copy": - - img +- button "Copy" - paragraph: Great, let's move forward. BANANA! -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "30 tok · Cache hit 0%": - - img - - text: 30 tokCache hit 0% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "30 tok · Cache hit 0%": 30 tokCache hit 0% - button "0% of context used": 0% diff --git a/snapshots/web/subagent-conversation/branchless.expected.md b/snapshots/web/subagent-conversation/branchless.expected.md index e17c69be2d..715de1e1ce 100644 --- a/snapshots/web/subagent-conversation/branchless.expected.md +++ b/snapshots/web/subagent-conversation/branchless.expected.md @@ -1,5 +1,4 @@ - tree "Subagent sessions": - treeitem "example editor continuable · not running 0 tok · {{duration}}" [level=1]: - text: example editor continuable · not running 0 tok {{duration}} - - button "Open example editor in sidebar": - - img + - button "Open example editor in sidebar" diff --git a/snapshots/web/subagent-conversation/fork.expected.md b/snapshots/web/subagent-conversation/fork.expected.md index c60eb2d064..b525c83c86 100644 --- a/snapshots/web/subagent-conversation/fork.expected.md +++ b/snapshots/web/subagent-conversation/fork.expected.md @@ -1,6 +1,4 @@ - tree "Sessions": - - treeitem "workspace" [expanded]: - - img - - text: workspace + - treeitem "workspace" [expanded] - treeitem "Explain event sourcing in one (1) now" [selected] - treeitem "Ask a research subagent to now" diff --git a/snapshots/web/subagent-conversation/nested.expected.md b/snapshots/web/subagent-conversation/nested.expected.md index 2825ca2ba0..86ebfc5c77 100644 --- a/snapshots/web/subagent-conversation/nested.expected.md +++ b/snapshots/web/subagent-conversation/nested.expected.md @@ -5,16 +5,13 @@ - 'button "Switch subagent: event-sourcing researcher"': event-sourcing researcher - text: / - 'button "Switch subagent: example editor"': example editor - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - text: Give one concrete event sourcing example. {{clock}} -- button "Copy": - - img +- button "Copy" - status: - strong: This subagent is read-only for now - text: The parent session is offline; reopen it to continue sending messages. diff --git a/snapshots/web/subagent-conversation/sidebar-chat.expected.md b/snapshots/web/subagent-conversation/sidebar-chat.expected.md index 5b0c23e774..63ccfba712 100644 --- a/snapshots/web/subagent-conversation/sidebar-chat.expected.md +++ b/snapshots/web/subagent-conversation/sidebar-chat.expected.md @@ -1,35 +1,18 @@ -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Explain event sourcing in one sentence.Your parent agent id is \"session-{{uuid}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"session-{{uuid}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn. {{clock}}" -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Custom"': Custom - button "Send message" [disabled] -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.9K tok · Cache hit 99%": - - img - - text: 7.9K tokCache hit 99% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "7.9K tok · Cache hit 99%": 7.9K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/subagent-conversation/sidebar.expected.md b/snapshots/web/subagent-conversation/sidebar.expected.md index aa3cadb40c..2d823ebe5e 100644 --- a/snapshots/web/subagent-conversation/sidebar.expected.md +++ b/snapshots/web/subagent-conversation/sidebar.expected.md @@ -1,5 +1,3 @@ - tree "Sessions": - - treeitem "workspace" [expanded]: - - img - - text: workspace + - treeitem "workspace" [expanded] - treeitem "Ask a research subagent to now" diff --git a/snapshots/web/subagent-conversation/tree.expected.md b/snapshots/web/subagent-conversation/tree.expected.md index de09b64909..7259ec1313 100644 --- a/snapshots/web/subagent-conversation/tree.expected.md +++ b/snapshots/web/subagent-conversation/tree.expected.md @@ -1,16 +1,12 @@ - tree "Subagent sessions": - treeitem "event-sourcing reviewer one-shot · not running 0 tok · {{duration}}" [level=1]: - text: event-sourcing reviewer one-shot · not running 0 tok ~6mo 12d - - button "Open event-sourcing reviewer in sidebar": - - img + - button "Open event-sourcing reviewer in sidebar" - treeitem "event-sourcing researcher Explain event sourcing in one · continuable · not running 7.9K tok · {{duration}}" [expanded] [level=1]: - - button "Collapse event-sourcing researcher descendants": - - img + - button "Collapse event-sourcing researcher descendants" - text: event-sourcing researcher Explain event sourcing in one · continuable · not running 7.9K tok {{duration}} - - button "Open event-sourcing researcher in sidebar": - - img + - button "Open event-sourcing researcher in sidebar" - group: - treeitem "example editor continuable · not running 0 tok · {{duration}}" [level=2]: - text: example editor continuable · not running 0 tok {{duration}} - - button "Open example editor in sidebar": - - img + - button "Open example editor in sidebar" diff --git a/snapshots/web/subagent-conversation/ui-expanded.expected.md b/snapshots/web/subagent-conversation/ui-expanded.expected.md index bc2cd0e280..c32ce44656 100644 --- a/snapshots/web/subagent-conversation/ui-expanded.expected.md +++ b/snapshots/web/subagent-conversation/ui-expanded.expected.md @@ -3,88 +3,45 @@ - button "Ask a research subagent to" - text: / - 'button "Switch subagent: event-sourcing researcher"': event-sourcing researcher - - button "1 subagent": - - text: 1 subagent - - img - - img + - button "1 subagent" - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - navigation "Turn navigation": - button "Jump to turn 1" - button "Jump to turn 2" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Explain event sourcing in one sentence.Your parent agent id is \"session-{{uuid}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"session-{{uuid}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn. {{clock}}" -- button "Copy": - - img -- button "Thought for a while" [expanded]: - - text: Thought for a while - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.": - - img - - img - - text: Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls. +- button "Copy" +- button "Thought for a while" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls." - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Now give the same explanation to a human reader. {{clock}} -- button "Copy": - - img -- button "Thought for a while" [expanded]: - - text: Thought for a while - - img -- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.": - - img - - img - - text: Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls. +- button "Copy" +- button "Thought for a while" [expanded] +- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls." - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Custom"': Custom - button "Send message" [disabled] -- button "2 turns 2 steps · {{throughput}} tok/s": - - img - - text: 2 turns 2 steps{{throughput}} tok/s -- button "15.7K tok · Cache hit 99%": - - img - - text: 15.7K tokCache hit 99% +- button "2 turns 2 steps · {{throughput}} tok/s": 2 turns 2 steps{{throughput}} tok/s +- button "15.7K tok · Cache hit 99%": 15.7K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/subagent-conversation/ui.expected.md b/snapshots/web/subagent-conversation/ui.expected.md index db3edcdb03..63534c81fb 100644 --- a/snapshots/web/subagent-conversation/ui.expected.md +++ b/snapshots/web/subagent-conversation/ui.expected.md @@ -3,76 +3,42 @@ - button "Ask a research subagent to" - text: / - 'button "Switch subagent: event-sourcing researcher"': event-sourcing researcher - - button "1 subagent": - - text: 1 subagent - - img - - img + - button "1 subagent" - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" - navigation "Turn navigation": - button "Jump to turn 1" - button "Jump to turn 2" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Explain event sourcing in one sentence.Your parent agent id is \"session-{{uuid}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"session-{{uuid}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn. {{clock}}" -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Now give the same explanation to a human reader. {{clock}} -- button "Copy": - - img -- button "Thought for a while": - - text: Thought for a while - - img +- button "Copy" +- button "Thought for a while" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Custom"': Custom - button "Send message" [disabled] -- button "2 turns 2 steps · {{throughput}} tok/s": - - img - - text: 2 turns 2 steps{{throughput}} tok/s -- button "15.7K tok · Cache hit 99%": - - img - - text: 15.7K tokCache hit 99% +- button "2 turns 2 steps · {{throughput}} tok/s": 2 turns 2 steps{{throughput}} tok/s +- button "15.7K tok · Cache hit 99%": 15.7K tokCache hit 99% - button "6% of context used": 6% diff --git a/snapshots/web/subagent-interrupt/offline-composer.expected.md b/snapshots/web/subagent-interrupt/offline-composer.expected.md index e7b2a6f3ab..55a2af25bc 100644 --- a/snapshots/web/subagent-interrupt/offline-composer.expected.md +++ b/snapshots/web/subagent-interrupt/offline-composer.expected.md @@ -3,40 +3,26 @@ - button "Ask a research subagent to" - text: / - 'button "Switch subagent: event-sourcing researcher"': event-sourcing researcher - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Explain event sourcing in one sentence.Your parent agent id is \"session-{{uuid}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"session-{{uuid}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn. {{clock}}" -- button "Copy": - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt +- button "Copy" +- button "Context injection @deepseek-ai/dsh-system-prompt" - paragraph: partial - status: Deep diving... - list: - listitem: - text: Keep working until I stop you again. - - button "Edit queued message": - - img - - button "Remove queued message": - - img - - button "Steer queued message": - - img + - button "Edit queued message" + - button "Remove queued message" + - button "Steer queued message" - textbox "Parent session offline; sending is unavailable but you can still stop the run" [disabled] -- button "Add files or run commands" [disabled]: - - img +- button "Add files or run commands" [disabled] - 'button "Access mode, current: Custom" [disabled]': Custom - button "Stop generating" - button "Send message" [disabled] diff --git a/snapshots/web/turn-tail-actions/completed.expected.md b/snapshots/web/turn-tail-actions/completed.expected.md index 5e20197dc4..8f3053942f 100644 --- a/snapshots/web/turn-tail-actions/completed.expected.md +++ b/snapshots/web/turn-tail-actions/completed.expected.md @@ -1,53 +1,29 @@ - banner: - navigation "Session hierarchy": - button "Begin your reply with the" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} -- button "Copy": - - img -- button "1 tool call · 1 message": - - text: 1 tool call · 1 message - - img +- button "Copy" +- button "1 tool call · 1 message" - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Usage 15.8K tok": - - img - - text: Usage 15.8K tok -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Usage 15.8K tok" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tok · Cache hit 50%": - - img - - text: 15.8K tokCache hit 50% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tok · Cache hit 50%": 15.8K tokCache hit 50% - button "6% of context used": 6% diff --git a/snapshots/web/turn-tail-actions/focused.expected.md b/snapshots/web/turn-tail-actions/focused.expected.md index ebdd9985dc..d79d578b20 100644 --- a/snapshots/web/turn-tail-actions/focused.expected.md +++ b/snapshots/web/turn-tail-actions/focused.expected.md @@ -1,66 +1,33 @@ - banner: - navigation "Session hierarchy": - button "Begin your reply with the" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} -- button "Copy": - - img -- button "1 tool call · 1 message" [expanded]: - - text: 1 tool call · 1 message - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to begin with \"Reading the workspace now.\" and call bash with \"echo alpha\" in the same message. Then after the tool result, reply with the single word DONE and stop.": - - img - - img - - text: Think The user wants me to begin with "Reading the workspace now." and call bash with "echo alpha" in the same message. Then after the tool result, reply with the single word DONE and stop. +- button "Copy" +- button "1 tool call · 1 message" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to begin with \"Reading the workspace now.\" and call bash with \"echo alpha\" in the same message. Then after the tool result, reply with the single word DONE and stop." - paragraph: Reading the workspace now. -- button "Bash Print alpha to stdout": - - img - - img - - text: Bash Print alpha to stdout +- button "Bash Print alpha to stdout" - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Usage 15.8K tok": - - img - - text: Usage 15.8K tok -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Usage 15.8K tok" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tok · Cache hit 50%": - - img - - text: 15.8K tokCache hit 50% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tok · Cache hit 50%": 15.8K tokCache hit 50% - button "6% of context used": 6% diff --git a/snapshots/web/turn-tail-actions/running.expected.md b/snapshots/web/turn-tail-actions/running.expected.md index 48e9598ba1..bb0ce72149 100644 --- a/snapshots/web/turn-tail-actions/running.expected.md +++ b/snapshots/web/turn-tail-actions/running.expected.md @@ -1,50 +1,27 @@ - banner: - navigation "Session hierarchy": - button "Begin your reply with the" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} -- button "Copy": - - img +- button "Copy" - tooltip "Copy" -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to begin with \"Reading the workspace now.\" and call bash with \"echo alpha\" in the same message. Then after the tool result, reply with the single word DONE and stop.": - - img - - img - - text: Think The user wants me to begin with "Reading the workspace now." and call bash with "echo alpha" in the same message. Then after the tool result, reply with the single word DONE and stop. +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to begin with \"Reading the workspace now.\" and call bash with \"echo alpha\" in the same message. Then after the tool result, reply with the single word DONE and stop." - paragraph: Reading the workspace now. -- button "Bash Print alpha to stdout": - - img - - img - - text: Bash Print alpha to stdout +- button "Bash Print alpha to stdout" - paragraph: partial - status: Deep diving... - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Stop generating" -- button "1 turns 1 steps · {{throughput}} tok/s": - - img - - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.9K tok · Cache hit 0%": - - img - - text: 7.9K tokCache hit 0% +- button "1 turns 1 steps · {{throughput}} tok/s": 1 turns 1 steps{{throughput}} tok/s +- button "7.9K tok · Cache hit 0%": 7.9K tokCache hit 0% - button "6% of context used": 6% diff --git a/snapshots/web/turn-tail-actions/settled.expected.md b/snapshots/web/turn-tail-actions/settled.expected.md index e15d904922..09c57d760e 100644 --- a/snapshots/web/turn-tail-actions/settled.expected.md +++ b/snapshots/web/turn-tail-actions/settled.expected.md @@ -1,52 +1,30 @@ - banner: - navigation "Session hierarchy": - button "Begin your reply with the" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} -- button "Copy": - - img -- button "1 tool call · 1 message": - - text: 1 tool call · 1 message - - img +- button "Copy" +- button "1 tool call · 1 message" - paragraph: partial - text: Stopped -- button "Copy": - - img +- button "Copy" - tooltip "Copy" -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "7.9K tok · Cache hit 0%": - - img - - text: 7.9K tokCache hit 0% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "7.9K tok · Cache hit 0%": 7.9K tokCache hit 0% - button "6% of context used": 6% diff --git a/snapshots/web/turn-tail-actions/usage-expanded.expected.md b/snapshots/web/turn-tail-actions/usage-expanded.expected.md index 84ef1f54fe..f725ddc0c2 100644 --- a/snapshots/web/turn-tail-actions/usage-expanded.expected.md +++ b/snapshots/web/turn-tail-actions/usage-expanded.expected.md @@ -1,53 +1,29 @@ - banner: - navigation "Session hierarchy": - button "Begin your reply with the" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} -- button "Copy": - - img -- button "1 tool call · 1 message": - - text: 1 tool call · 1 message - - img +- button "Copy" +- button "1 tool call · 1 message" - paragraph: DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Usage 15.8K tok" [expanded]: - - img - - text: Usage 15.8K tok -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Usage 15.8K tok" [expanded] +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tok · Cache hit 50%": - - img - - text: 15.8K tokCache hit 50% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tok · Cache hit 50%": 15.8K tokCache hit 50% - button "6% of context used": 6% diff --git a/snapshots/web/web-search-round/ui.expected.md b/snapshots/web/web-search-round/ui.expected.md index e1fabef490..32429834ab 100644 --- a/snapshots/web/web-search-round/ui.expected.md +++ b/snapshots/web/web-search-round/ui.expected.md @@ -1,58 +1,30 @@ - banner: - navigation "Session hierarchy": - button "Use web_search once with queries" [disabled] - - img - text: Standard mode - - button "More actions": - - img - - button "Open right sidebar": - - img + - button "More actions" + - button "Open right sidebar" - tablist: - tab "Chat" [selected] - tab "Trajectory" -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: Use web_search once with queries ["DeepSeek Harness snapshot search","DeepSeek Harness multi-query search"]. Then reply exactly SEARCH_DONE and stop. {{clock}} -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Search DeepSeek Harness snapshot search, DeepSeek Harness multi-query search": - - img - - img - - text: Search DeepSeek Harness snapshot search, DeepSeek Harness multi-query search +- button "Copy" +- button "1 tool call" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Search DeepSeek Harness snapshot search, DeepSeek Harness multi-query search" - paragraph: SEARCH_DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" -- button "Add files or run commands": - - img +- button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write -- button "Select model, current DeepSeek-V4-Flash": - - text: DeepSeek-V4-Flash - - img +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash - button "Send message" [disabled] -- button "1 turns 2 steps · {{throughput}} tok/s": - - img - - text: 1 turns 2 steps{{throughput}} tok/s -- button "29 tok · Cache hit 0%": - - img - - text: 29 tokCache hit 0% +- button "1 turns 2 steps · {{throughput}} tok/s": 1 turns 2 steps{{throughput}} tok/s +- button "29 tok · Cache hit 0%": 29 tokCache hit 0% - button "0% of context used": 0% diff --git a/snapshots/web/workflow-run/ui-live.expected.md b/snapshots/web/workflow-run/ui-live.expected.md index 52ac5c75fc..6efe2ff5a1 100644 --- a/snapshots/web/workflow-run/ui-live.expected.md +++ b/snapshots/web/workflow-run/ui-live.expected.md @@ -1,6 +1,2 @@ -- button "snapshot-flow 1 member Running" [expanded]: - - img - - text: snapshot-flow 1 member Running -- button "Run 1 member Running 1": - - img - - text: Run 1 member Running 1 +- button "snapshot-flow 1 member Running" [expanded] +- button "Run 1 member Running 1" diff --git a/snapshots/web/workflow-run/ui.expected.md b/snapshots/web/workflow-run/ui.expected.md index 4859403351..60d2a2b193 100644 --- a/snapshots/web/workflow-run/ui.expected.md +++ b/snapshots/web/workflow-run/ui.expected.md @@ -1,42 +1,16 @@ -- button "System prompt": - - img - - img - - text: System prompt +- button "System prompt" - text: "Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim): phase('Run') const reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.') return { reply } After the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other tool. {{clock}}" -- button "Copy": - - img -- button "1 tool call" [expanded]: - - text: 1 tool call - - img -- button "Context injection @deepseek-ai/dsh-system-prompt": - - img - - img - - text: Context injection @deepseek-ai/dsh-system-prompt -- button "Think The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:": - - img - - img - - text: "Think The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:" -- button "Tool call workflow ·": - - img - - img - - text: Tool call workflow · -- button "snapshot-flow 1 member Completed": - - img - - text: snapshot-flow 1 member Completed -- button "Think The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop.": - - img - - img - - text: Think The workflow returned successfully with the reply "WF_CHILD_OK". Now I need to reply with exactly "WORKFLOW_DONE" and stop. +- button "Copy" +- button "1 tool call" [expanded] +- button "Context injection @deepseek-ai/dsh-system-prompt" +- button "Think The user wants me to use the workflow tool exactly once with specific parameters. Let me carefully follow the instructions:" +- button "Tool call workflow ·" +- button "snapshot-flow 1 member Completed" +- button "Think The workflow returned successfully with the reply \"WF_CHILD_OK\". Now I need to reply with exactly \"WORKFLOW_DONE\" and stop." - paragraph: WORKFLOW_DONE -- button "Copy": - - img -- button "Good response": - - img -- button "Bad response": - - img -- button "Branch into a new conversation": - - img -- button "Ran for {{duration}}": - - img - - text: Ran for {{duration}} +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- button "Ran for {{duration}}" - text: {{clock}} diff --git a/snapshots/web/workspace-management/directory-browser.expected.md b/snapshots/web/workspace-management/directory-browser.expected.md index 47957dab82..ef6bd52c48 100644 --- a/snapshots/web/workspace-management/directory-browser.expected.md +++ b/snapshots/web/workspace-management/directory-browser.expected.md @@ -2,55 +2,27 @@ - heading "Select Workspace Directory" [level=2] - navigation: - button "Home" - - img - button "browse-golden" - - button "Edit path": - - img + - button "Edit path" - list: - listitem: - - button "adopted": - - img - - text: adopted - - img + - button "adopted" - listitem: - - button "alpha-ws": - - img - - text: alpha-ws - - img + - button "alpha-ws" - listitem: - - button "beta-ws": - - img - - text: beta-ws - - img + - button "beta-ws" - listitem: - - button "browse-golden": - - img - - text: browse-golden - - img + - button "browse-golden" - listitem: - - button "same-name": - - img - - text: same-name - - img + - button "same-name" - listitem: - - button "workspace": - - img - - text: workspace - - img + - button "workspace" - list: - listitem: - - button "alpha": - - img - - text: alpha - - img + - button "alpha" - listitem: - - button "beta": - - img - - text: beta - - img - - button "New folder": - - img - - text: New folder + - button "beta" + - button "New folder" - button "Show hidden files" - button "Cancel" - button "Open" diff --git a/snapshots/web/workspace-recency/manual-blank.expected.md b/snapshots/web/workspace-recency/manual-blank.expected.md index a227f2398d..11fca7353c 100644 --- a/snapshots/web/workspace-recency/manual-blank.expected.md +++ b/snapshots/web/workspace-recency/manual-blank.expected.md @@ -1,7 +1,5 @@ - tree "Sessions": - - treeitem "{{workspace}}" [expanded]: - - img - - text: {{workspace}} + - treeitem "{{workspace}}" [expanded] - treeitem "New Session" [selected] - treeitem "Newest conversation now" - treeitem "Middle conversation 1min" diff --git a/snapshots/web/workspace-recency/sidebar.expected.md b/snapshots/web/workspace-recency/sidebar.expected.md index ce3169bbaf..3fab22b226 100644 --- a/snapshots/web/workspace-recency/sidebar.expected.md +++ b/snapshots/web/workspace-recency/sidebar.expected.md @@ -1,7 +1,5 @@ - tree "Sessions": - - treeitem "{{workspace}}" [expanded]: - - img - - text: {{workspace}} + - treeitem "{{workspace}}" [expanded] - treeitem "Newest conversation now" [selected] - treeitem "Middle conversation 1min" - treeitem "Oldest conversation 2min" diff --git a/tsconfig.base.json b/tsconfig.base.json index 6ec529b39d..3cace3bae1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -272,7 +272,6 @@ "@deepseek-ai/dsh-experimental-agent-team": ["./packages/experimental/agent-team/src"], "@deepseek-ai/dsh-experimental-auto-review": ["./packages/experimental/auto-review/src"], "@deepseek-ai/dsh-experimental-agent-team-profile": ["./packages/experimental/agent-team-profile/src"], - "@deepseek-ai/dsh-experimental-agent-team-web-profile": ["./packages/experimental/agent-team-web-profile/src"], "@deepseek-ai/dsh-experimental-tool-agent-team": ["./packages/experimental/tool-agent-team/src"], "@deepseek-ai/dsh-experimental-webworker-runtime": ["./packages/experimental/webworker-runtime/src"], "@deepseek-ai/dsh-experimental-webworker-packer": ["./packages/experimental/webworker-packer/src"], diff --git a/tsconfig.host.json b/tsconfig.host.json index 2b98af6f58..2800f8cfd2 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -15,6 +15,7 @@ "apps/web/tests/default-product-isolation.e2e.ts", "apps/web/tests/diff-context.e2e.ts", "apps/web/tests/scaffold.ts", + "apps/web/tests/developer-tools-settings.e2e.ts", "apps/web/tests/auto-review-fixture.ts", "apps/web/tests/auto-review-denial.e2e.ts", "apps/web/tests/scaffold-generation.spec.ts", @@ -369,7 +370,6 @@ { "path": "./packages/experimental/agent-team" }, { "path": "./packages/experimental/agent-team-profile" }, { "path": "./packages/experimental/tool-agent-team" }, - { "path": "./packages/experimental/agent-team-web-profile" }, { "path": "./packages/workflow/workflow" }, { "path": "./packages/workflow/workflow-ptc" }, { "path": "./packages/workflow/tool-workflow" }, diff --git a/vitest.snapshot.config.ts b/vitest.snapshot.config.ts index f8ed5e36ad..e29b1a395d 100644 --- a/vitest.snapshot.config.ts +++ b/vitest.snapshot.config.ts @@ -21,6 +21,10 @@ const snapshotMaxConcurrency = positiveIntFromEnv( Math.min(DEFAULT_SNAPSHOT_MAX_CONCURRENCY, availableParallelism()), ) +const snapshotMaxWorkers = process.env.DSH_SNAPSHOT_MAX_WORKERS + ? positiveIntFromEnv('DSH_SNAPSHOT_MAX_WORKERS', availableParallelism()) + : undefined + // Replay is the keyless default: boot real subprocess paths from recorded model responses and diff // assembled requests, normalized protocol or transcript output, and persisted-log expected outputs. // `record` calls the real API and updates fixtures and expected outputs; `refresh` replays committed scripts @@ -54,14 +58,16 @@ export default defineConfig({ // Replay never writes committed outputs and every scenario owns its // mutable runtime state (the subprocess suites use a unique temp dir and // fixture set per scenario), so replay runs the snapshot files in - // parallel and bounds in-file concurrency with the environment knob - // (value 1 restores fully serial replay on constrained machines). Record + // parallel with separate file-worker and in-file concurrency budgets. + // Without a file-worker override, in-file concurrency 1 is fully serial. Record // and refresh stay serial: record spends real API quota per scenario, and // refresh write-back harvests volatile values from fixtures already on // disk, so concurrent writers would corrupt expected outputs. testTimeout: 120_000, hookTimeout: 30_000, - fileParallelism: (process.env.DSH_SNAPSHOT || 'replay') === 'replay' && snapshotMaxConcurrency > 1, + fileParallelism: (process.env.DSH_SNAPSHOT || 'replay') === 'replay' + && (snapshotMaxWorkers ?? snapshotMaxConcurrency) > 1, maxConcurrency: snapshotMaxConcurrency, + ...snapshotMaxWorkers === undefined ? {} : { maxWorkers: snapshotMaxWorkers }, }, })