chore: merge master into bundle metadata correction

This commit is contained in:
turtle1999 committed 2026-09-28 22:45:57 +08:00
commit 9cd8a888bb
527 files changed
+4450 -775

No files matched your search

@@ -9,8 +9,8 @@
en: f9721b730c7e70c7
zh: 367e7839912cb726
/agent-note-default-deepseek-session-log-upload/decision:
en: ec3b05fb70ae490f
zh: 59b0691e327855b4
en: b72ef475a41b1a23
zh: b17a68367679730b
/agent-note-default-deepseek-session-log-upload/alternatives-considered:
en: 4cc4b61d76f84ff3
zh: bdd29ea66037f744
@@ -9,7 +9,9 @@ Ordinary DeepSeek requests do not contain the complete canonical Session traject
## Decision
`session-log-deepseek.Config.enabled` defaults to `true` in every process. An explicit `enabled: false` disables the contribution. The plugin does not inspect test-runner or snapshot environment variables.
`session-log-deepseek.Config.enabled` defaults to `true` in every process. A resolved `enabled: false` disables the contribution; configuration precedence determines which value takes effect. The plugin does not inspect test-runner or snapshot environment variables.
The `enabled` value is read for each request through a volatile reference so a saved preference takes effect without restarting the plugin. Profile writes can override bundle defaults under the [profile-owned configuration rules](2026-09-19-profile-owned-live-configuration.md); home patches and command-line overlays still reject conflicting writes. Disabling upload leaves the acceptance watermark unchanged because it records provider acceptance, not user eligibility. Re-enabling therefore sends the unaccepted suffix, including events recorded while disabled. Already prepared requests retain their payload.
This supersedes only the opt-in default in the [request-extension decision](2026-08-21-deepseek-llm-api-request-extensions.md); that note still owns field serialization, destinations, acceptance, and retry semantics. The headless and ACP corpus base patches and Web scaffold explicitly disable upload. Later scenario patches can enable it. The SDK text-turn recording omits the setting and exercises the shipped default, including durable acceptance events.
@@ -9,7 +9,9 @@ Status: implemented
## 决策
`session-log-deepseek.Config.enabled` 在所有进程中均默认为 `true`。显式设置 `enabled: false` 可关闭贡献。插件不读取测试运行器或快照环境变量。
`session-log-deepseek.Config.enabled` 在所有进程中均默认为 `true`。最终生效的 `enabled: false` 会关闭贡献;配置优先级决定采用哪个值。插件不读取测试运行器或快照环境变量。
每次请求通过 volatile 引用读取 `enabled`,因此保存的偏好无需重启插件即可生效。按照 [profile 所有的配置规则](2026-09-19-profile-owned-live-configuration.zh.md),profile 写入可覆盖组合包默认值;home patch 和命令行 overlay 仍会拒绝冲突写入。关闭上传不改变接受水位,因为它记录提供方的接受结果,而不是用户是否允许上传。重新开启后会发送尚未接受的后缀,包括关闭期间记录的事件。已准备的请求保留原有载荷。
本决策仅取代[请求扩展决策](2026-08-21-deepseek-llm-api-request-extensions.zh.md)中的主动启用默认策略;该记录仍负责字段序列化、目的地址、接受与重试语义。headless 和 ACP 语料的基础 patch 以及 Web scaffold 显式关闭上传。后续场景 patch 可以启用上传。SDK text-turn 录制省略该设置,验证产品默认值及持久接受事件。
@@ -10,7 +10,10 @@
zh: 91844972d41dd6f9
/agent-note-windows-sandbox-rung-raw-acl-restricted-tokens-over-mxc-and-appcontainer/decision:
en: 125dea7427047966
zh: 5d0bfd3921b38714
zh: 1e459b1106597695
/agent-note-windows-sandbox-rung-raw-acl-restricted-tokens-over-mxc-and-appcontainer/acl-diagnosis-and-recovery:
en: 8ea2f91a914d540b
zh: cafada631044e43a
/agent-note-windows-sandbox-rung-raw-acl-restricted-tokens-over-mxc-and-appcontainer/how-the-restriction-works-why-no-new-identity:
en: ac9b3d800889ea39
zh: 9db1065b6027f65f
@@ -12,6 +12,14 @@ The original [sandbox decision](2026-07-06-sandbox.md) left `PLATFORM_CHAINS.win
Implement the rung directly on the raw ACL mechanism: duplicate the caller's token into a `WRITE_RESTRICTED` token (`CreateRestrictedToken` with `WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`) whose restricting SIDs carry distinct workspace and private-temp capabilities. `WRITE_RESTRICTED` intersects write accesses only, so reads keep the caller's ambient access while a write must also match one of these capability ACEs. That intersection covers only the object's own access check, and Windows also authorizes a write or a delete from the PARENT directory's `FILE_DELETE_CHILD` right — which no restricting SID has to co-sign, so `cmd /c del` deleted files outside the workspace wherever the caller's ambient SIDs hold Modify. The token is therefore also lowered to Low integrity and every granted directory receives a Low no-write-up mandatory label in the same call that adds its ACE; the kernel applies that policy inside the access check, whichever right supplied the authority, so the granted roots stay writable and everything else becomes delete-proof as well as write-proof. Because every granted root is labeled Low, the integrity check alone would still let one confined child delete inside ANOTHER granted root (another workspace, a live sibling temp directory), so each grant also DENIES `FILE_DELETE_CHILD` to the world SID: the capability ACE's DELETE bit becomes the only delete authority inside a granted root, for the confined child and for the user's own processes, whose ordinary deletes keep working because the same DACL grants them DELETE directly. The mechanism is the one huoyaoyuan/windows-acl-restrict-poc (`10e4dfb`) demonstrates; this port checks every API call and fails closed (the POC fail-opened on ignored failures). The per-workspace SID is derived deterministically from the canonical workspace path (`workspaceWriteSid` — sha256 → `S-1-4-x-y`); its standing workspace ACE is the cross-session reuse cache, and an exact-ACE skip prevents repeated eager tree propagation. Each live session/workspace pair instead gets a random private temp directory and a domain-separated SID derived from that path (`tempWriteSid`); its ACE is revocable, TMP/TEMP point at that directory, and the token default DACL names the temp SID so newly created temp objects do not acquire the shared workspace capability. A fork therefore cannot write its sibling's temp tree. A fresh provider chooses a new path and SID even for the same resumed session, so crash residue is inert litter rather than a collision or inherited capability; agentless calls create and remove the same shape per invocation. The ambient temp root is never an implicit grant. A workspace equal to or containing the temp root fails before any ACL mutation because its inheritable standing ACE would otherwise reach every private child; the direct API rejects overlap in either direction between a writable root and the actual private temp directory. PowerShell can complete its startup AppLocker probe through this private-temp capability, so `workspace-write` remains FullLanguage absent a host-wide policy; `read-only` cannot create the probe files and conservatively enters ConstrainedLanguage. That split is PowerShell startup behavior, not part of the ACL boundary. The token lists are read-only = [logon SID, Everyone] and workspace-write = [logon SID, Everyone, workspace SID, optional temp SID]. Logon SID + Everyone are keep-alive invariants (early DLL init dies with 0xC0000142 and CNG crashes pwsh with 0xE0434352 without them). Everyone remains in both lists but no longer confers write authority, because the label layer denies it; because NTFS ACLs belong to file objects, a granted workspace hard link still grants an external alias; and because a Low-integrity token cannot open an object whose DACL names another sandbox's AppContainer package SID, a tree stamped by such a tool becomes unreadable to the confined child. Rejecting all hard links would reject ordinary pnpm workspaces, so the provider reports `enforcement: 'partial'` and the native suite pins both gaps. Read-only carries no capability SID, so standing workspace ACEs remain inert across a mode downgrade. Authenticated Users is absent from both lists — CIM is unavailable, closing the C:\-root tree-creation escape — and INTERACTIVE/LOCAL are absent, denying Public-tree writes. New anonymous pipes and sync objects inherit the temp SID (or workspace SID when temp is disabled, Everyone under read-only) through `SetTokenInformation(TokenDefaultDacl)`; named pipes keep the Win32 layer's owner/SYSTEM/Admins-full, Everyone/ANONYMOUS-read-only template, so piped grandchild stdio remains denied. It ships as [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md), the `win32` rung of [`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md), and [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/shell/pwsh-sandbox/README.md) as the Line truncated
## ACL diagnosis and recovery
The bundled diagnosis skill runs its preserving ACL repairs outside the confined token because that token cannot inspect the conflicting objects. External PowerShell requires physical script files, so registration extracts resources from ASAR or SEA instead of exposing archive paths. The fiber owns that temporary copy. Recovery files live in the caller's persistent output directory so disposal of the skill does not remove the means to restore a DACL.
A repair backs up before mutation and verifies effective access or the resulting ACEs. Failed invocations restore attempted changes in reverse order and remain failures: continuing after a failed grant would remove access without establishing the prerequisite. Inherited package entries are repaired at their explicit sources, preserving inheritance; incomplete or out-of-scope sources prevent mutation. Labels and denies remain untouched because removing them could defeat the backend's confinement or an intentional user policy.
Both extracted and recovery scripts are user-writable, so this workflow never recommends elevating them. Missing unconfined WRITE_DAC stops the workflow for permission-policy review. Automatic recovery can itself fail and cannot survive process termination; reports retain pending recovery commands rather than promising an atomic transaction. The operator keeps those artifacts until verification of the original confined operation succeeds.
## How the restriction works (why no new identity)
The identity routes restrict by *who* runs the child; this rung restricts by *token derivation*. An identity route (landstrip's restricted-user, AppContainer) runs the child under a fresh account or container SID that starts with zero ACEs on the host's files — everything, reads included, defaults to denied, and every path the child may touch must then be opened back up by writing ACEs for that identity: the wholesale DACL mutation that disqualified both alternatives. The restricted token keeps the caller's own SID and logon session: [`CreateRestrictedToken`](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken) derives a token that adds the restricting SIDs and the `WRITE_RESTRICTED` flag, so Windows performs the access check twice — once against the normal SIDs, once against the restricting SIDs — and grants write-class access only where both checks pass. Reads pass on the normal check alone (the caller's SIDs already carry read access everywhere the caller can read), which is why this rung needs no read grants and no new account; writes must additionally clear the capability-SID check, which only the workspace and temp ACEs satisfy. `DISABLE_MAX_PRIVILEGE | LUA_TOKEN` synthesize the limited-user effect of a fresh account token-side, so even an elevated caller derives a filtered token. The same primitive could restrict reads (`SidsToDisable` turning SIDs deny-only), but a read-restricted token would need per-path read grants — reintroducing exactly the cost the identity routes pay — and the sandbox vocabulary never requires read confinement.
@@ -12,6 +12,15 @@ Status: implemented
直接基于原始 ACL 机制实现该档:把调用者令牌复制为 `WRITE_RESTRICTED` 受限令牌(`CreateRestrictedToken`,`WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`),其 restricting SIDs 携带彼此独立的工作区能力与私有临时目录能力。`WRITE_RESTRICTED` 只对写访问做交集检查,因此读取保留调用者的环境访问,而写入还必须匹配这些能力 ACE 之一。该交集只覆盖对象自身的那次访问检查,而 Windows 也会依据父目录的 `FILE_DELETE_CHILD` 权限批准写入或删除——这项权限不需要任何 restricting SID 副署,因此只要调用者的环境 SID 持有 Modify,`cmd /c del` 就能删除工作区之外的文件。所以令牌还会被降级为 Low 完整性,每个被授权目录都在添加 ACE 的同一次调用中收到 Low 禁止上调强制标签;无论权限来自哪一项,内核都会在访问检查内部执行该策略,于是被授权根目录保持可写,而其他一切不仅防写、也防删。由于每个授权根目录都被标记为 Low,仅靠完整性检查仍会让一个受限子进程删除**另一个授权根目录**(别的工作区、同级的活跃临时目录)内的文件,因此每次授权还会向 world SID 拒绝 `FILE_DELETE_CHILD`:能力 ACE 的 DELETE 位由此成为授权根目录内唯一的删除授权来源——对受限子进程如此,对用户自身进程亦如此,而后者的日常删除仍然可用,因为同一份 DACL 直接向其授予 DELETE。该机制来自 huoyaoyuan/windows-acl-restrict-poc(`10e4dfb`)的演示;本移植检查每个 API 调用并 fail-closed(POC 因忽略失败而 fail-open)。工作区 SID 由规范工作区路径确定性派生(`workspaceWriteSid`——sha256 → `S-1-4-x-y`);其常驻工作区 ACE 是跨会话复用缓存,精确 ACE 跳过可避免重复的急切全树传播。每个活跃的会话/工作区对则获得一个随机私有临时目录,以及一个从该路径派生的、经过域分离的 SID(`tempWriteSid`);其 ACE 可回收,TMP/TEMP 指向该目录,令牌默认 DACL 列入该临时 SID,因此新建的临时对象不会获得共享的工作区能力。fork 因此无法写入同级会话的临时目录树。即使恢复的是同一会话,新的提供方也会选择新的路径和 SID,因此崩溃残留只是失效垃圾,而非冲突或继承的能力;无 agent(智能体)的调用会逐调用创建并移除同样的形态。环境临时根目录绝不会被隐式授权。如果工作区等于或包含临时根目录,调用会在任何 ACL 改动发生前失败,因为否则其可继承的常驻 ACE 会向每个私有子目录授权;直接 API 会拒绝可写根目录与实际私有临时目录在任一方向上的重叠。PowerShell 可借助这项私有临时目录能力完成启动时的 AppLocker 探针,因此在没有主机范围策略时,`workspace-write` 会保持 FullLanguage;`read-only` 无法创建探针文件,会保守地进入 ConstrainedLanguage。这一区别属于 PowerShell 启动行为,不是 ACL 边界的一部分。令牌列表为 read-only = [登录 SID、Everyone],workspace-write = [登录 SID、Everyone、工作区 SID、可选临时 SID]。登录 SID + Everyone 是保活不变式(没有它们,早期 DLL 初始化会以 0xC0000142 死亡,CNG 会让 pwsh 以 0xE0434352 崩溃)。Everyone 仍留在列表中,但它带来的写权限已被标签层否定;由于 NTFS ACL 属于文件对象,工作区内获授权的硬链接仍会使同一对象的外部别名获得授权,而 DACL 中携带其他沙箱 AppContainer 包 SID 的文件对 Low 完整性子进程不可读。拒绝所有硬链接会让普通 pnpm 工作区不可用,因此提供方报告 `enforcement: 'partial'`,原生套件则钉住这些缺口。Read-only 不含任何能力 SID,因此常驻工作区 ACE 在模式降级后保持失效。Authenticated Users 在两种列表中都不存在——CIM 不可用,从而关闭 C:\-root 建树逃逸——INTERACTIVE/LOCAL 也不存在,因此 Public 树写入被拒绝。新建匿名管道和同步对象通过 `SetTokenInformation(TokenDefaultDacl)` 继承临时 SID(禁用临时目录时继承工作区 SID,read-only 下继承 Everyone);named pipe 保持 Win32 层 owner/SYSTEM/Admins 全权、Everyone/ANONYMOUS 只读的模板,因此受限孙进程的管道 stdio 仍被拒绝。它以 [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.zh.md)、[`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.zh.md) 的 `win32` 档,以及作为隔离执行器的 [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/shell/pwsh-sandbox/README.zh.md) 交付。
<a id="acl-diagnosis-and-recovery"></a>
## ACL 诊断与恢复
内置诊断技能在受限令牌之外执行保留原有权限的 ACL 修复,因为受限令牌无法检查发生冲突的对象。外部 PowerShell 需要实际脚本文件,因此注册时从 ASAR 或 SEA 提取资源,而不提供归档内路径。fiber 拥有该临时副本。恢复文件保存在调用者指定的持久输出目录中,确保技能释放后仍能恢复 DACL。
修复在修改前备份,并验证有效权限或生成的 ACE。失败调用按相反顺序恢复尝试修改的内容,且仍视为失败:授权失败后继续操作,会在前置条件尚未满足时移除访问权限。继承的包条目从显式来源处修复,保持继承不变;来源信息不完整或超出范围时不做修改。标签与拒绝条目保持不变,因为移除它们可能破坏后端隔离或用户有意设置的策略。
提取的脚本和恢复脚本均可由用户写入,因此此流程绝不建议提权运行它们。不受限调用者缺少 WRITE_DAC 时,流程停止并交由权限策略审查处理。自动恢复本身也可能失败,且无法跨进程终止继续执行;报告保留待执行的恢复命令,不承诺原子事务。操作者应保留这些文件,直到原始受限操作验证成功。
## How the restriction works (why no new identity)
身份路线靠"**谁**在跑子进程"来限制,本档靠"令牌派生"来限制。身份路线(landstrip 的 restricted-user、AppContainer)用全新账户或容器 SID 运行子进程,该身份在宿主的文件上从零条 ACE 开始——一切访问(包括读)默认拒绝,子进程要碰的每条路径都必须事后为那个身份补写 ACE 才能放行:这正是让两个备选方案出局的全盘 DACL 改造。受限令牌保留调用者自己的 SID 与 logon session:[`CreateRestrictedToken`](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-createrestrictedtoken) 派生一个加入 restricting SIDs 与 `WRITE_RESTRICTED` 标志的令牌,于是 Windows 做两次访问检查——一次按正常 SID,一次按 restricting SIDs——只有两次都放行,写类访问才被授予。读只凭正常检查即可通过(调用者的 SID 在其可读范围内本来就携带读权限),所以本档不需要任何读授权、也不需要新账户;写还必须额外通过能力 SID 检查,而只有工作区与临时目录的 ACE 能满足它。`DISABLE_MAX_PRIVILEGE | LUA_TOKEN` 在令牌侧合成了新账户的受限用户效果,即使提升过的调用者派生的也是过滤令牌。同一原语其实也能限制读(`SidsToDisable` 把 SID 变为 deny-only),但受限读的令牌需要逐路径的读授权——恰好重新引入身份路线付出的代价——而沙盒词汇表从不要求读隔离。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-09-28-dual-release-install-layout-work-budget.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.md
/agent-note-dual-release-install-layout-work-budget:
en: 808f7c1d11ce15b5
zh: 861c63dd2de66452
/agent-note-dual-release-install-layout-work-budget/problem:
en: 46916488210ba493
zh: 98107c3805a22b2c
/agent-note-dual-release-install-layout-work-budget/decision:
en: c432b68c4ad1b3c1
zh: a11e958fa68dc2f3
/agent-note-dual-release-install-layout-work-budget/what-the-resolution-costs:
en: 36d3e3ef39cbba2f
zh: 2480acf6260817b3
/agent-note-dual-release-install-layout-work-budget/alternatives-considered:
en: 29987b7634c84d28
zh: a1469e4b6aaa46e2
/agent-note-dual-release-install-layout-work-budget/consequences:
en: 8dffc0c3fa699980
zh: 99ec25001e5aa716
@@ -0,0 +1,50 @@
# Agent Note: Dual-release install layout work budget
Status: implemented
English | [中文](2026-09-28-dual-release-install-layout-work-budget.zh.md)
## Problem
The `Dependency layout` lane packs two incompatible synthetic DSH releases into a temporary consumer, runs one real npm resolution over them, and asserts the physical placement npm chose. It decided pass or fail with `TIMEOUT_MS = 300_000` on the npm child process. That deadline measures the runner, not the graph: the same graph resolves in 137-205 s on an idle developer host and in 288.59 s on the CI runner, 4% below the deadline, while the placement assertion reported no error. The diagnostic named only elapsed time, so a slow runner and a graph that had grown were indistinguishable.
## Decision
`scripts/verify-npm-install-layout.ts` decides growth with `assertResolutionWorkBudget`, which bounds `dshPackagesPerVersion * checkedDshEdges` against `MAX_RESOLUTION_WORK_UNITS = 875_000`. Both counts come from the summary `assertDualDshInstallLayout` returns, so the budget applies to the graph npm actually produced rather than to a prediction of it. A graph above the budget fails with its own counts, the budget, and the instruction to measure the new resolution cost and raise the constant.
The npm child keeps a wall-clock limit, `NPM_HANG_GUARD_MS`, derived as `4 * MAX_RESOLUTION_WORK_UNITS * SECONDS_PER_WORK_UNIT` (910 s). It is a hang guard, not the growth criterion: a runner up to four times slower than the measured host cannot decide the gate's outcome, and deriving it from the budget keeps the two values from drifting apart.
This restores the [published dependency faces](2026-08-26-published-dependency-faces.md) decision that `verify-npm-install-layout` does not enforce resolver duration; the 300 s deadline had contradicted it.
## What the resolution costs
Measured 2026-09-28 on an idle M-series host, against `origin/master` (277 DSH packages per release, 2524 internal edges), by running the lane's own npm invocation with `--timing` and `--cpu-prof`:
| Phase | Cost |
|---|---|
| Registry synthesis (`buildRegistryIndex`, 3952 installed manifests plus 319 DSH workspace manifests into 1544 names) | 1.67 s |
| npm child process | 180.66 s of 185.07 s end to end |
| `idealTree:buildDeps` inside that child | 153.30 s, and nearly all of it lands in 14 of 877 per-node timers |
| `checkCanPlace` / `canPlacePeers` peer-conflict analysis | 117.3 s, of which `new URL()` 65.9 s and `SemVer` 27.9 s |
| Registry traffic | 682 requests, 383 KB of packument bodies |
| Layout assertion and process startup | 4.4 s |
The same graph resolved in 1.12 s with `--legacy-peer-deps`, so peer placement is 152.7 s of the 153.8 s. `canPlacePeers` re-checks each internal edge against the incoming edges of its peer target, which makes the cost grow with placements times edge count; the nested release's copy of `@deepseek-ai/dsh-base` alone takes 48.8 s against 1.3 s for the identical package at the root of the same tree. About three quarters of the verified internal edges are peer edges (1874 of 2524), so this work is the assertion, not overhead around it.
Repeated runs of the same graph on one host span 137.8 s to 204.8 s of npm time, a factor of 1.5 with no input change, which is why a wall-clock threshold cannot separate a slow host from a larger graph.
## Alternatives considered
**Reuse one npm cache directory across runs.** The lane's `npm_config_cache` lives inside the temporary consumer, and the registry listens on an ephemeral port, so `make-fetch-happen` keys every cache entry as `http://127.0.0.1:<port>/<name>` and a persisted cache cannot hit. Re-running the full graph warm with `--prefer-offline` over the cold run's 4.4 MB cache produced a byte-identical package lock in 189.8 s, against 153.8 s cold: correct, and slower. The profile's 10.2 s of idle time is the ceiling on any network saving, because npm spends the rest of the run in its own placement code.
**Merge or parallelize the two releases.** The lane already resolves both releases in one npm invocation, which is what makes the co-existence claim checkable, and one release cannot reuse the other's peer analysis because `canPlacePeers` examines the whole tree. Splitting the graph into two independent resolutions would remove the shared-Cordis and cross-release placement facts the assertion exists to check.
**Raise `TIMEOUT_MS`.** A larger deadline leaves the failure mode in place: the verdict still depends on runner speed, and a graph that grows until it is slow on every machine still fails with no measurement of what grew.
**`--prefer-dedupe`.** It resolves the same graph in 126.8 s but chooses a different placement (809 lock entries against 815, eight differing entries), so the lane would assert a non-default npm placement.
**`--legacy-peer-deps`.** It resolves in 1.12 s and drops the 52 peer-installed packages, along with the peer edges that make up most of the assertion's coverage.
## Consequences
The lane's failure now depends on the graph, so a pull request that adds a few DSH packages passes on any runner, and a graph that grows the packages-times-edges product by more than about a quarter — about 12% in packages and internal edges together — fails deterministically with its measured counts. The budget is a ratchet: raising it is a deliberate edit that must come with a new resolution measurement, and the constant's comment records the counts it was derived from. A graph inside the budget still spends its full resolution time, because the change moves the decision and not the work; nothing here reduces the 153 s npm needs to place the dual release. A genuine npm hang now consumes up to 910 s of the lane before failing, where the old deadline capped it at 300 s. Growth that changes the shape of the graph without changing packages times edges stays inside the budget and is caught only if the hang guard expires.
@@ -0,0 +1,50 @@
# Agent Note: 双发布安装布局的工作量预算
Status: implemented
[English](2026-09-28-dual-release-install-layout-work-budget.md) | 中文
## Problem
`Dependency layout` lane 把两个互不兼容的合成 DSH 发布打进临时消费者目录,对它们跑一次真实 npm 解析,并断言 npm 选定的物理摆放。它用 npm 子进程上的 `TIMEOUT_MS = 300_000` 判定通过或失败。这个期限度量的是 runner,而不是图:同一张图在空闲开发机上用 137–205 秒解析完成,在 CI runner 上用 288.59 秒,比期限低 4%,而摆放断言没有报告任何错误。诊断信息只给出耗时,因此 runner 慢和依赖图变大无法区分。
## Decision
`scripts/verify-npm-install-layout.ts` 用 `assertResolutionWorkBudget` 判定规模增长,它以 `dshPackagesPerVersion * checkedDshEdges` 为工作量单位,与 `MAX_RESOLUTION_WORK_UNITS = 875_000` 比较。两个计数都来自 `assertDualDshInstallLayout` 返回的汇总,因此预算作用于 npm 实际产出的图,而不是对图的预测。超过预算的图会带着自己的计数、预算值,以及「测量新的解析开销并调高该常量」的指示失败。
npm 子进程仍保留墙钟上限 `NPM_HANG_GUARD_MS`,由 `4 * MAX_RESOLUTION_WORK_UNITS * SECONDS_PER_WORK_UNIT` 推导(910 秒)。它是挂起保护,不是增长判据:比实测主机慢四倍以内的 runner 无法决定该门禁的结果,而由预算推导可避免两个数值彼此漂移。
这恢复了[发布依赖门面](2026-08-26-published-dependency-faces.zh.md)中「`verify-npm-install-layout` 不限制 resolver 耗时」的决定;300 秒期限与该决定相矛盾。
## What the resolution costs
2026-09-28 在空闲的 M 系列主机上,针对 `origin/master`(每个发布 277 个 DSH 包、2524 条内部边),用 `--timing` 和 `--cpu-prof` 运行 lane 自身的 npm 调用测得:
| 阶段 | 开销 |
|---|---|
| Registry 合成(`buildRegistryIndex`,3952 个已安装 manifest 加 319 个 DSH workspace manifest,合成 1544 个包名) | 1.67 s |
| npm 子进程 | 端到端 185.07 s 中的 180.66 s |
| 该子进程内的 `idealTree:buildDeps` | 153.30 s,且几乎全部落在 877 个按节点计时器中的 14 个上 |
| `checkCanPlace` / `canPlacePeers` peer 冲突分析 | 117.3 s,其中 `new URL()` 65.9 s、`SemVer` 27.9 s |
| Registry 流量 | 682 次请求、383 KB packument 响应体 |
| 布局断言与进程启动 | 4.4 s |
同一个依赖图加上 `--legacy-peer-deps` 后 1.12 s 解析完成,因此 peer 摆放占 153.8 s 中的 152.7 s。`canPlacePeers` 会把每条内部边与其 peer 目标的入边重新核对一遍,这使开销随「摆放次数 × 边数」增长;嵌套发布的 `@deepseek-ai/dsh-base` 副本单独耗时 48.8 s,而同一棵树上根位置的同一包只耗时 1.3 s。被校验的内部边约有四分之三是 peer 边(2524 条中的 1874 条),所以这部分工作是断言本身,而不是断言之外的开销。
同一台主机上对同一张图的重复运行,npm 耗时在 137.8 s 到 204.8 s 之间波动,输入没有任何变化却有 1.5 倍差距,这正是墙钟阈值无法区分「主机慢」和「图更大」的原因。
## Alternatives considered
**跨次复用同一个 npm cache 目录。** lane 的 `npm_config_cache` 位于临时消费者目录内,registry 监听临时端口,因此 `make-fetch-happen` 把每个 cache 条目键为 `http://127.0.0.1:<port>/<name>`,持久化的 cache 无法命中。用冷启动那次留下的 4.4 MB cache 加 `--prefer-offline` 重跑整张图,产出的 package lock 逐字节相同,耗时 189.8 s,而冷启动为 153.8 s:结果正确,但更慢。profile 中 10.2 s 的空闲时间是任何网络节省的上限,因为 npm 把其余时间都花在自己的摆放代码里。
**合并或并行两个发布。** lane 已经在一次 npm 调用里解析两个发布,这正是共存声明可被校验的原因;而 `canPlacePeers` 会检查整棵树,一个发布无法复用另一个的 peer 分析。把图拆成两次相互独立的解析会丢掉断言存在的理由:共享 Cordis 与跨发布摆放。
**调大 `TIMEOUT_MS`。** 更大的期限保留了原有失效模式:判定仍取决于 runner 速度,而一个增长到在任何机器上都慢的图仍会失败,且不给出任何关于「什么变大了」的测量。
**`--prefer-dedupe`。** 它用 126.8 s 解析同一张图,但选择了不同的摆放(809 条 lock 条目对 815 条,8 条条目不同),因此 lane 断言的将是非默认的 npm 摆放。
**`--legacy-peer-deps`。** 它 1.12 s 完成解析,但丢掉了 52 个由 peer 安装的包,也丢掉了构成断言大部分覆盖面的 peer 边。
## Consequences
lane 的失败判定如今取决于依赖图,因此新增少量 DSH 包的 pull request 在任何 runner 上都能通过,而「包数乘边数」增长超过约四分之一的图——即包数与内部边数同步增长约 12%——会带着实测计数确定性地失败。预算是棘轮:调高它是一次刻意修改,必须附带新的解析实测,该常量注释里记录了推导所用的计数。预算内的图仍会花掉完整的解析时间,因为这次改动移动的是判定位置而不是工作本身;这里没有任何东西能减少 npm 摆放双发布所需的 153 s。真正挂起的 npm 现在会消耗该 lane 最多 910 秒才失败,而旧期限把它限制在 300 秒。若图的增长改变了形状却没有改变「包数乘边数」,它仍留在预算内,只有在挂起保护到期时才被发现。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-27-oxlint-contract-spawn-case-budgets.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.md
/agent-note-oxlint-contract-cases-that-spawn-a-process-carry-a-90-s-ceiling:
en: 152f1e58efbf506e
zh: 889504ebfe5783ca
/agent-note-oxlint-contract-cases-that-spawn-a-process-carry-a-90-s-ceiling/problem:
en: 08d6430c0b4e868f
zh: b98c15541bcea4ee
/agent-note-oxlint-contract-cases-that-spawn-a-process-carry-a-90-s-ceiling/decision:
en: 34bd113b8c9228fb
zh: 99843bf33b703b5e
/agent-note-oxlint-contract-cases-that-spawn-a-process-carry-a-90-s-ceiling/alternatives-considered:
en: 2b317170890802f7
zh: 2a5221dcfa8b9116
/agent-note-oxlint-contract-cases-that-spawn-a-process-carry-a-90-s-ceiling/consequences:
en: e484a10680ca20b8
zh: 8fd9fcf13e3dbcd7
@@ -0,0 +1,27 @@
# Agent Note: Oxlint contract cases that spawn a process carry a 90 s ceiling
Status: implemented
English | [中文](2026-09-27-oxlint-contract-spawn-case-budgets.zh.md)
## Problem
Twelve of the fourteen cases in [`scripts/oxlint-contract.spec.ts`](../../../../scripts/oxlint-contract.spec.ts) spawn a real child process: `runOxlint` starts the oxlint CLI, and `runRepositoryOxlint` boots [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts) under tsx, which runs oxlint and, after `--fix`, runs it once more. The root `vitest.config.ts` sets no `testTimeout`, so a case without its own budget inherits Vitest's 5000 ms default; the coverage lanes raise it through `DSH_COVERAGE_TEST_TIMEOUT_MS=90000` and the Windows native lane passes `--testTimeout 90000`, while the `unit tests (darwin parity, macos-latest)` job in `sandbox.yml` and a developer's `pnpm run test` run plain. Seven cases (five `it` declarations, one of them a three-way `it.each`) carried the explicit `90_000` that #3115 aligned the contended Windows spawn budgets to; the other seven ran on the default, and five of those spawn. Two runner classes exposed it: the self-hosted Windows pool, where process creation spikes to several seconds (#2581), and the hosted macos-latest runner of Sandbox run 36332053320, which ran the whole file 2.2× slower than the passing run 36320394290 and timed out `prints only the final diagnostics when a fix retry still fails` at 6309 ms, a case that took 1122 ms on the passing run. `Test timed out in 5000ms` names neither a contract violation nor the spawn that overran.
## Decision
Every case in the file that spawns oxlint or the repository lint entrypoint carries the same explicit per-case `90_000` ceiling. The two configuration-only cases, `keeps the complete stylistic contract in Oxlint` and `keeps repository lint workflows Oxlint-only`, spawn nothing and keep the default. The value is a completion ceiling for a hang, not a latency target: the slowest case on the slow darwin runner took 9507 ms, and the file under six-fold CPU oversubscription on a 12-core host peaked at 20767 ms, both under a quarter of the ceiling. Assertions, the absence of retries, and Vitest's global `testTimeout` are unchanged.
## Alternatives considered
**A `describe`-level `{ timeout: 90_000 }`.** Rejected: case-level values already bound seven cases and take precedence over a `describe` value, so a suite-level number would govern only the remaining seven, including the two configuration-only cases that spawn nothing, and the file would carry two budget styles. Per-case values keep one style, one place to read each case's ceiling, and the default on the cases that spawn nothing. Either form overrides the lane's `--testTimeout` rather than yielding to it (#2677). The archived [translation-pairing-merge](../../archived/testing/2026-08-27-translation-pairing-merge-budget.md) and [Lefthook budget](../../archived/testing/2026-08-29-windows-lane-hook-and-lefthook-budget.md) notes chose the `describe` form for files whose cases carried no constants of their own, because a case added later without an allowance inherits the default; this file already carried per-case constants, so that risk is accepted here and named under Consequences.
**Passing `--testTimeout 90000` to the darwin parity job and letting the cases inherit the lane budget**, the form the [subagent teardown budgets](2026-09-07-subagent-teardown-test-budgets.md) note prefers for suites bound by process creation. Rejected: the cost is the file's own tsx boot plus two oxlint runs, not a darwin property, so a developer's plain `pnpm run test` would keep the default; the parity job exists to run the same command developers run; and seven cases already carry constants, so a lane flag would split the file's budget between two sources.
**Raising Vitest's global `testTimeout`.** Rejected: the whole unit inventory runs on the default; widening it to absorb one file's process spawns removes the hang detection the default provides everywhere else.
**A tighter value sized to the observed 6.3 s.** Rejected: the Windows contention fires as multi-second spikes that rotated across cases under 15–30 s budgets before the 90 s alignment; a smaller number moves the flake rather than removing it.
## Consequences
A contract violation still fails through its own assertion well inside the ceiling; the five cases bounded here settle in 0.5–1.7 s on an idle host and no spawning case in the file exceeded 3.8 s there, so only a genuine hang waits the full 90 s. The Windows self-hosted lane, the Linux coverage lanes, the darwin parity job, and a local `pnpm run test` read the same ceiling for this file regardless of `DSH_COVERAGE_TEST_TIMEOUT_MS`. A spawning case added later without its own `90_000` runs on the default until the file's inventory is checked. Under 72 busy-loop processes on a 12-core host the unbounded fix-retry case reproduced `Test timed out in 5000ms` at 7084 ms before the change and passes with the ceiling after it.
@@ -0,0 +1,27 @@
# Agent Note: 会 spawn 进程的 Oxlint 契约用例携带 90 s 上限
Status: implemented
[English](2026-09-27-oxlint-contract-spawn-case-budgets.md) | 中文
## 问题
[`scripts/oxlint-contract.spec.ts`](../../../../scripts/oxlint-contract.spec.ts) 的 14 个用例中有 12 个会启动真实子进程:`runOxlint` 启动 oxlint CLI,`runRepositoryOxlint` 在 tsx 下引导 [`scripts/run-oxlint.ts`](../../../../scripts/run-oxlint.ts),后者运行 oxlint,并在 `--fix` 之后再运行一次。根 `vitest.config.ts` 没有设置 `testTimeout`,因此没有自带预算的用例继承 Vitest 的 5000 ms 默认值;coverage 通道通过 `DSH_COVERAGE_TEST_TIMEOUT_MS=90000` 抬高它,Windows 原生通道传入 `--testTimeout 90000`,而 `sandbox.yml` 中的 `unit tests (darwin parity, macos-latest)` job 与开发者本机的 `pnpm run test` 都直接运行。7 个用例(5 个 `it` 声明,其中一个是三路 `it.each`)携带 #3115 把有争抢的 Windows spawn 预算对齐到的显式 `90_000`;另外 7 个跑在默认值上,其中 5 个会 spawn。两类 runner 暴露了这一点:自托管 Windows 池,其进程创建会出现数秒尖峰(#2581);以及 Sandbox run 36332053320 的托管 macos-latest runner,它跑整个文件比通过的 run 36320394290 慢 2.2 倍,并让 `prints only the final diagnostics when a fix retry still fails` 在 6309 ms 时超时,而该用例在通过的 run 中耗时 1122 ms。`Test timed out in 5000ms` 既不指出契约违规,也不指出超时的那次 spawn。
## 决策
文件中每个会 spawn oxlint 或仓库 lint 入口的用例都携带同一个显式的用例级 `90_000` 上限。两个只检查配置的用例 `keeps the complete stylistic contract in Oxlint` 与 `keeps repository lint workflows Oxlint-only` 不 spawn 任何进程,保持默认值。该值是针对挂起的完成上限,不是延迟目标:慢 darwin runner 上最慢的用例耗时 9507 ms,12 核宿主机上六倍 CPU 超订下整个文件的峰值为 20767 ms,两者都不到上限的四分之一。断言、不加重试、以及 Vitest 全局 `testTimeout` 均不变。
## 曾考虑的替代方案
**`describe` 级 `{ timeout: 90_000 }`。** 拒绝:用例级取值已经约束 7 个用例并且优先于 `describe` 级取值,因此套件级数字只会作用于其余 7 个用例,包括两个不 spawn 的纯配置用例,而且文件会同时带两种预算风格。用例级取值保持一种风格,每个用例的上限只在一处可读,不 spawn 的用例保持默认值。两种形式都覆盖通道的 `--testTimeout` 而不是让位于它(#2677)。已归档的 [translation-pairing-merge](../../archived/testing/2026-08-27-translation-pairing-merge-budget.md) 与 [Lefthook 预算](../../archived/testing/2026-08-29-windows-lane-hook-and-lefthook-budget.md)两篇 Note 为用例本身不带常量的文件选择了 `describe` 形式,理由是之后新增而未带余量的用例会继承默认值;本文件已经带有用例级常量,因此这里接受该风险并在「后果」中点名。
**给 darwin parity job 传入 `--testTimeout 90000`,让用例继承通道预算**,这是[子代理清理预算](2026-09-07-subagent-teardown-test-budgets.zh.md)一篇对进程创建受限套件偏好的形式。拒绝:成本来自本文件自身的 tsx 引导加两次 oxlint 运行,不是 darwin 的属性,因此开发者直接运行的 `pnpm run test` 仍会停留在默认值;parity job 的存在意义就是运行与开发者相同的命令;而且 7 个用例已带常量,通道旗标会把本文件的预算拆到两个来源。
**抬高 Vitest 全局 `testTimeout`。** 拒绝:整个单元测试清单都跑在默认值上;为吸收一个文件的进程 spawn 而放宽它,会移除默认值在其他所有地方提供的挂起检测。
**按观测到的 6.3 s 收紧取值。** 拒绝:Windows 争抢表现为数秒尖峰,在对齐到 90 s 之前曾在 15–30 s 预算下于各用例间轮转出现;更小的数字只是移动 flake 而不是消除它。
## 后果
契约违规仍通过其自身断言在上限之内早早失败;本次加边界的 5 个用例在空载宿主机上 0.5–1.7 s 内结算,文件中任何会 spawn 的用例在那里都不超过 3.8 s,因此只有真正的挂起才会等满 90 s。Windows 自托管通道、Linux coverage 通道、darwin parity job 与本机 `pnpm run test` 对本文件读到同一个上限,与 `DSH_COVERAGE_TEST_TIMEOUT_MS` 无关。之后新增而未自带 `90_000` 的 spawn 用例会跑在默认值上,直到有人核对本文件的清单。在 12 核宿主机上以 72 个忙循环进程加载时,原本无边界的 fix-retry 用例在改动前于 7084 ms 复现 `Test timed out in 5000ms`,改动后在上限内通过。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.md
/agent-note-web-lane-moves-spreadsheet-selections-by-keyboard-before-reflow-assertions:
en: 015053d7e4cc8b74
zh: 8afd22975bcb9814
/agent-note-web-lane-moves-spreadsheet-selections-by-keyboard-before-reflow-assertions/problem:
en: 27ce4e01ca063f29
zh: cd8c872750c725bc
/agent-note-web-lane-moves-spreadsheet-selections-by-keyboard-before-reflow-assertions/decision:
en: 49b7f008e97f05cb
zh: 00571e867ba82057
/agent-note-web-lane-moves-spreadsheet-selections-by-keyboard-before-reflow-assertions/alternatives-considered:
en: 51c1315b1d424915
zh: 2f2f537cfdde2469
/agent-note-web-lane-moves-spreadsheet-selections-by-keyboard-before-reflow-assertions/verification:
en: 316dbc106810d68f
zh: 8771fd22b885e4ab
/agent-note-web-lane-moves-spreadsheet-selections-by-keyboard-before-reflow-assertions/consequences:
en: 03eedcc3cfdad180
zh: 8d4843e01448fe83
@@ -0,0 +1,37 @@
# Agent Note: Web lane moves spreadsheet selections by keyboard before reflow assertions
Status: implemented
English | [中文](2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.zh.md)
## Problem
[document-preview.e2e.ts](../../../../apps/web/tests/document-preview.e2e.ts) opens `meeting.xlsx`, clicks the sheet overlay at `{ x: 60, y: 40 }` to select `A1`, then widens and narrows the sidebar four times and asserts after each reflow that the name box still reads `A1`. Ten of the eleven `CI master` runs of the serial self-hosted Linux lane between 2026-09-24 and 2026-09-27 failed one or both locale cases of that scenario with `expected 'B1' to be 'A1'` at the first reflow, and hosted `node 24 / snapshots and artifacts` runs failed the same way (runs 36110572677 and 36213089675). The failure is not locale-bound: en-US failed alone in four master runs, both locales in three, zh-CN alone in three. Polling the read (#5243) changed nothing because `B1` is the committed selection, not a stale sample.
`A1` is the workbook's initial selection, so the polls that follow the click are satisfied before the click commits and never verify what the click selected. FortuneSheet's `SheetOverlay` handles the press as `setContext(draft => handleCellAreaMouseDown(draft, globalCache, nativeEvent, cellInput, cellArea, …))`, and `handleCellAreaMouseDown` resolves the cell from `cellArea.getBoundingClientRect()` and the event's page coordinates at the moment the updater runs. React runs `useState` updaters during render and runs them again when the hook queue holds a lower-priority update ahead of them; the freshly mounted workbook still has such updates pending, and Playwright precedes the press with a `mousemove` whose FortuneSheet handler is a continuous-priority update on the same state. An instrumented verbatim copy of the scenario on the Blacksmith runner recorded four executions of `handleCellAreaMouseDown` for the one click: two before the click returned reading `left=969.5`, and two after the scenario had already set the panel to `1000px`, reading `left=725.5`. The stored pointer x of 985 then resolves to 259.5 inside the grid, and because the meeting sheet has two columns `colLocation` clamps it to the last one: `B1`. On a machine that commits the workbook's initial selection normalization before the click, the same point lies on A1's own selection box, whose `onMouseDown` stops propagation; the click then selects nothing, no geometry-bound updater is queued, and the scenario passes without having tested the click.
## Decision
The meeting-sheet step selects by keyboard. The scenario focuses the sheet overlay (`tabindex="-1"`), asserts the focus landed, presses `ArrowRight` and asserts `B1` with an empty formula bar, then presses `ArrowLeft` and asserts `A1` with `会议纪要`, and only then starts the reflow loop. `handleArrowKey` → `moveHighlightCell` computes the target from committed state — the current selection and `visibledatacolumn` — so a re-executed updater yields the same cell whatever the grid's geometry is by then, and the two moves make the interaction observable because `B1` differs from the initial selection. The reflow loop, its `A1` assertion, the sheet-tab scroll-control counts, and the canvas identity check are unchanged.
The budget-sheet click at the start of the scenario stays a pointer click: it targets a cell other than the initial selection, its result is verified by the `46281` formula read, and keyboard, clipboard, and layout reads separate it from the first reflow by far more than one deferred render. The `values.csv` click followed by a narrowing stays too: a re-execution against the narrowed grid resolves a negative x, which `colLocation` clamps to column A, the same cell.
## Alternatives considered
**Poll the name box after each reflow.** #5243 did this. The re-executed updater's result is the final committed state, so the poll observes `B1` for its whole budget; there is no later state to wait for.
**Wait for the selection box to render before clicking.** That restores the passing path in which the click lands on A1's own drag handle and selects nothing, so the assertions would verify the initial selection rather than an interaction. Clicking a cell other than the initial selection instead keeps the geometry-bound updater and its re-execution window open across the reflow that follows.
**Wait for the renderer to go idle before the first reflow.** No product state names "React has no re-execution pending"; an idle or fixed wait sized to the runner is the flake-masking wait [dsh-ci-test-reliability](../../../skills/dsh-ci-test-reliability/SKILL.md) rejects.
**Extend the `@fortune-sheet/react` patch to resolve the cell outside the updater.** `handleCellAreaMouseDown` lives in `@fortune-sheet/core`, and the window a user would need to hit is a pointer press and a sidebar reflow inside one deferred render. It stays an upstream concern; the product change is out of this test fix.
## Verification
The instrumented verbatim copy of the shipped scenario, run on the hosted Linux lane of PR #5291 while it was a draft, reproduced `expected 'B1' to be 'A1'` at the first reflow in 7 of 38 runs across three lane runs (2 of 6, 1 of 6, 4 of 20). Every failure recorded `mousedown` targeting `#luckysheet-sheettable_0` rather than the drag handle and the `B1` commit preceding the deferred `cellInput.focus()` that only the press updater schedules; the five failures in the two lane runs that also hooked the cell area's `getBoundingClientRect` all carried the four-execution signature above. The keyboard-driven copy ran in those same two lane runs and passed 26 of 26. Locally, `Emulation.setCPUThrottlingRate` at 20× makes the click reach the cell area and records three executions of `handleCellAreaMouseDown` per click; the keyboard-driven copy passes 6 of 6 under that throttle and 6 of 6 with React's scheduler wake-ups deferred by 200 ms.
`env -u TSX_TSCONFIG_PATH DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/document-preview.e2e.ts -t "fills the spreadsheet pane"` passed both locale cases in 10 of 10 local repetitions, and the whole file passed its 6 cases in 2 of 3 local runs; the third failed the unrelated `Coding Tools` switch poll at `:849` while documentation gates ran concurrently, with the failure screenshot showing the switch on.
## Consequences
The scenario verifies a keyboard-made selection surviving sidebar reflows and no longer depends on where React's deferred render lands relative to the reflow. A pointer click in this lane remains valid when its target differs from the current selection and its effect is asserted before any layout change, or when a re-execution against the changed layout provably resolves the same cell; a click whose result feeds a reflow assertion within a few round trips does not. FortuneSheet's geometry-bound updater is unchanged, so a real pointer press followed within one deferred render by a sidebar reflow can still move the selection in the product.
@@ -0,0 +1,37 @@
# Agent Note: Web lane moves spreadsheet selections by keyboard before reflow assertions
Status: implemented
[English](2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.md) | 中文
## Problem
[document-preview.e2e.ts](../../../../apps/web/tests/document-preview.e2e.ts) 打开 `meeting.xlsx`,在表格 overlay 的 `{ x: 60, y: 40 }` 处点击以选中 `A1`,随后把侧栏加宽、收窄各两次,并在每次重排后断言名称框仍显示 `A1`。2026-09-24 到 2026-09-27 之间 `CI master` 的 serial 自托管 Linux lane 的 11 次运行中有 10 次在第一次重排处以 `expected 'B1' to be 'A1'` 失败该场景的一个或两个 locale 用例,托管的 `node 24 / snapshots and artifacts` 也以同样方式失败(run 36110572677 与 36213089675)。失败与 locale 无关:四次 master 运行只有 en-US 失败,三次两者都失败,三次只有 zh-CN 失败。把读取改为轮询(#5243)没有作用,因为 `B1` 是已提交的选区,不是过期采样。
`A1` 是工作簿的初始选区,所以点击之后的两个轮询在点击提交前就已满足,从未验证点击选中了什么。FortuneSheet 的 `SheetOverlay` 以 `setContext(draft => handleCellAreaMouseDown(draft, globalCache, nativeEvent, cellInput, cellArea, …))` 处理按下,而 `handleCellAreaMouseDown` 在 updater 执行的那一刻用 `cellArea.getBoundingClientRect()` 与事件的页面坐标解析目标单元格。React 在渲染期间运行 `useState` 的 updater,并且当 hook 队列里有更低优先级的更新排在它前面时会再次运行它;刚挂载的工作簿仍有这类更新待处理,而 Playwright 在按下之前先派发一次 `mousemove`,其 FortuneSheet 处理器是同一状态上的 continuous 优先级更新。在 Blacksmith runner 上运行的带探针的逐字副本记录到这一次点击触发了四次 `handleCellAreaMouseDown`:两次在点击返回前读到 `left=969.5`,两次在场景已把面板设为 `1000px` 之后读到 `left=725.5`。保存下来的指针 x 985 于是在网格内解析为 259.5,而会议表只有两列,`colLocation` 把它钳到最后一列:`B1`。在点击前就已提交工作簿初始选区归一化的机器上,同一位置落在 A1 自身的选区框上,其 `onMouseDown` 阻止了传播;点击于是什么也没选中,没有几何绑定的 updater 入队,场景在没有测试到点击的情况下通过。
## Decision
会议表这一步改为键盘选择。场景聚焦表格 overlay(`tabindex="-1"`),断言焦点已落在其上,按 `ArrowRight` 并断言 `B1` 且公式栏为空,再按 `ArrowLeft` 并断言 `A1` 且公式栏为 `会议纪要`,然后才开始重排循环。`handleArrowKey` → `moveHighlightCell` 只依据已提交状态(当前选区与 `visibledatacolumn`)计算目标,所以被重新执行的 updater 无论此时网格几何如何都得到同一单元格,而两次移动使交互可观察,因为 `B1` 与初始选区不同。重排循环、其中的 `A1` 断言、表页滚动控件计数与 canvas 身份检查保持不变。
场景开头预算表的点击仍是指针点击:它的目标不是初始选区,其结果由 `46281` 公式读取验证,而键盘、剪贴板和布局读取把它与第一次重排隔开远超一次延迟渲染。`values.csv` 的点击及其后的收窄也保持不变:针对收窄后的网格重新执行会解析出负的 x,`colLocation` 把它钳到 A 列,即同一单元格。
## Alternatives considered
**每次重排后轮询名称框。** #5243 这样做了。被重新执行的 updater 的结果就是最终提交状态,所以轮询在整个预算内都观察到 `B1`;没有更晚的状态可等。
**点击前等待选区框渲染。** 这会恢复点击落在 A1 自身拖拽手柄上、什么也不选的通过路径,断言于是验证的是初始选区而不是一次交互。改为点击初始选区以外的单元格则会保留几何绑定的 updater,其重新执行窗口跨越随后的重排。
**第一次重排前等待渲染器空闲。** 没有产品状态能表达“React 没有待重新执行的更新”;按 runner 调校的空闲等待或固定等待正是 [dsh-ci-test-reliability](../../../skills/dsh-ci-test-reliability/SKILL.md) 拒绝的掩盖式等待。
**扩展既有的 `@fortune-sheet/react` 补丁,在 updater 之外解析单元格。** `handleCellAreaMouseDown` 位于 `@fortune-sheet/core`,而用户需要命中的窗口是一次指针按下与一次侧栏重排落在同一次延迟渲染内。它仍是上游问题;产品改动不在本次测试修复范围内。
## Verification
PR #5291 处于草稿阶段时在托管 Linux lane 上运行的带探针逐字副本,在三次 lane 运行共 38 次中有 7 次(2/6、1/6、4/20)在第一次重排处复现 `expected 'B1' to be 'A1'`。每次失败都记录到 `mousedown` 的目标是 `#luckysheet-sheettable_0` 而不是拖拽手柄,且 `B1` 的提交先于只有按下 updater 才会安排的延迟 `cellInput.focus()`;在同时给 cell area 的 `getBoundingClientRect` 加了钩子的两次 lane 运行里,5 次失败全部带有上述四次执行的特征。键盘驱动的副本在这两次 lane 运行中 26 次全部通过。本地以 20× 的 `Emulation.setCPUThrottlingRate` 运行可让点击到达 cell area,并记录到每次点击三次 `handleCellAreaMouseDown` 执行;键盘驱动的副本在该限速下 6 次全部通过,在 React 调度器唤醒被推迟 200 ms 时也 6 次全部通过。
`env -u TSX_TSCONFIG_PATH DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/document-preview.e2e.ts -t "fills the spreadsheet pane"` 在本地 10 次重复中 10 次都通过两个 locale 用例;整个文件的 6 个用例在 3 次本地运行中 2 次通过,第 3 次在文档 gate 并行运行时失败于无关的 `:849` `Coding Tools` 开关轮询,失败截图显示开关已打开。
## Consequences
场景验证的是键盘做出的选区在侧栏重排后仍然保留,不再依赖 React 的延迟渲染落在重排之前还是之后。此 lane 中的指针点击在目标不同于当前选区、且其效果在任何布局变化之前被断言时仍然有效,或者在针对变化后布局重新执行可证明解析为同一单元格时也有效;而结果在几个往返之内就被送入重排断言的点击则无效。FortuneSheet 的几何绑定 updater 未改动,所以产品中一次真实的指针按下若在一次延迟渲染内紧跟着侧栏重排,仍可能移动选区。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh",
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+2 -2
View File
@@ -24,8 +24,8 @@
en: 12bba432c07573bf
zh: ab6b365cfb95003f
/dsh-cli-behavior-reference/shared-deployment-behavior:
en: 24e3da3716d261fe
zh: 060611966db4ca69
en: 6a7abc9339e8f37f
zh: 133adb834c8e195d
/dsh-cli-behavior-reference/source-execution:
en: 05c2b23790f7b296
zh: 51f809020fb230a1
+1 -1
View File
@@ -126,7 +126,7 @@ New sessions in base-backed profiles default to the `workspace-write` permission
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the public-only HTTP fetch provider, default-on DeepSeek session-log upload, and feedback-gated OTel upload for all users. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation; the provider rejects non-public destinations before connecting. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
Feedback is recorded in the Session log without starting model work. The [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) sends complete unaccepted log suffixes with subsequent DeepSeek requests by default, including requests sent through configured gateways; set its `enabled` configuration to `false` to opt out. [OTel session upload](../../../packages/session/session-telemetry-otel/README.md) applies to all users and providers, including `deepseek-official`, without requiring a request header. The base defaults to `FEEDBACK_ONLY`: new own text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including stored context; later records wait for the next explicit feedback. Inherited parent feedback does not authorize a fork. Requests, restoration, mount, and HMR do not trigger capture. SDK batching may finish an authorized upload without further interaction or model work. `DSH_TELEMETRY_MODE=DISABLED` disables OTel delivery; `FULL` is rejected, and any non-empty `DSH_TELEMETRY_DISABLED` disables its row. `DSH_TELEMETRY_OTLP_URL` selects the collector. Handoff is best-effort, not collector acceptance; no durable outbox or retry guarantee is provided. These OTel settings do not enable or disable the DeepSeek contribution. Neither path changes model input, but exports can include message text, tool arguments and results, and workspace paths.
Feedback is recorded in the Session log without starting model work. The [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) sends complete unaccepted log suffixes with subsequent DeepSeek requests by default, including requests sent through configured gateways; turn off **Settings → General → Upload Session Log when using the official model API** in Web, or set its `enabled` configuration to `false` to opt out. [OTel session upload](../../../packages/session/session-telemetry-otel/README.md) applies to all users and providers, including `deepseek-official`, without requiring a request header. The base defaults to `FEEDBACK_ONLY`: new own text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including stored context; later records wait for the next explicit feedback. Inherited parent feedback does not authorize a fork. Requests, restoration, mount, and HMR do not trigger capture. SDK batching may finish an authorized upload without further interaction or model work. `DSH_TELEMETRY_MODE=DISABLED` disables OTel delivery; `FULL` is rejected, and any non-empty `DSH_TELEMETRY_DISABLED` disables its row. `DSH_TELEMETRY_OTLP_URL` selects the collector. Handoff is best-effort, not collector acceptance; no durable outbox or retry guarantee is provided. These OTel settings do not enable or disable the DeepSeek contribution. Neither path changes model input, but exports can include message text, tool arguments and results, and workspace paths.
Install external plugin bundles through `dsh plugin --profile <name> add <package-or-git-spec>`. The installed package owns its dependencies and contributes its declared `cordis.patch.yml` layer. The CLI also ships `@deepseek-ai/dsh-mcp-client` as a dependency for patch layers, but no MCP server is enabled by default because each server command is trusted executable code outside the agent sandbox.
+1 -1
View File
@@ -128,7 +128,7 @@ dsh web --help
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、仅限公网的 HTTP fetch 提供方,默认开启的 DeepSeek 会话日志上传,以及面向所有用户的反馈门控 OTel 上传。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认;提供方会在连接前拒绝非公开目的地址。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具。
反馈记录在会话日志中,不会启动模型工作。[DeepSeek 会话日志贡献器](../../../packages/session/session-log-deepseek/README.zh.md)默认随后续 DeepSeek 请求发送尚未确认接收的完整日志后缀,包括经已配置网关发送的请求;将其 `enabled` 配置设为 `false` 可关闭上传。[OTel 会话上传](../../../packages/session/session-telemetry-otel/README.zh.md)适用于所有用户和提供方,包括 `deepseek-official`,无需请求头。基础配置默认使用 `FEEDBACK_ONLY`:新的自身文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范日志前缀,包含存储的上下文;后续记录等待下一次显式反馈。继承的父级反馈不构成 fork 的授权。请求、恢复、挂载和 HMR 不触发捕获。SDK 批处理可完成已授权上传,无需进一步交互或模型工作。`DSH_TELEMETRY_MODE=DISABLED` 禁止 OTel 投递;`FULL` 被拒绝,任何非空的 `DSH_TELEMETRY_DISABLED` 都会禁用其配置行。`DSH_TELEMETRY_OTLP_URL` 选择采集端。交接尽力而为,不代表采集端接受;不提供持久化 outbox 或重试保证。这些 OTel 设置不会开启或关闭 DeepSeek 贡献。两条路径都不改变模型输入,但导出可能包含消息文本、工具参数和结果,以及工作区路径。
反馈记录在会话日志中,不会启动模型工作。[DeepSeek 会话日志贡献器](../../../packages/session/session-log-deepseek/README.zh.md)默认随后续 DeepSeek 请求发送尚未确认接收的完整日志后缀,包括经已配置网关发送的请求;可在 Web 的**设置 → 通用 → 在使用官方模型 API 时上传 Session Log**中关闭上传,或将其 `enabled` 配置设为 `false`。[OTel 会话上传](../../../packages/session/session-telemetry-otel/README.zh.md)适用于所有用户和提供方,包括 `deepseek-official`,无需请求头。基础配置默认使用 `FEEDBACK_ONLY`:新的自身文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范日志前缀,包含存储的上下文;后续记录等待下一次显式反馈。继承的父级反馈不构成 fork 的授权。请求、恢复、挂载和 HMR 不触发捕获。SDK 批处理可完成已授权上传,无需进一步交互或模型工作。`DSH_TELEMETRY_MODE=DISABLED` 禁止 OTel 投递;`FULL` 被拒绝,任何非空的 `DSH_TELEMETRY_DISABLED` 都会禁用其配置行。`DSH_TELEMETRY_OTLP_URL` 选择采集端。交接尽力而为,不代表采集端接受;不提供持久化 outbox 或重试保证。这些 OTel 设置不会开启或关闭 DeepSeek 贡献。两条路径都不改变模型输入,但导出可能包含消息文本、工具参数和结果,以及工作区路径。
通过 `dsh plugin --profile <name> add <package-or-git-spec>` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent 沙箱之外的受信任可执行代码。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-desktop-host",
"description": "Private Node-mode host process for the Electron desktop application",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"private": true,
"license": "MIT",
"type": "module",
+2 -2
View File
@@ -15,8 +15,8 @@
en: 31cbb7452be8e1ad
zh: 502ec556e2ec301e
/deepseek-harness-desktop/installation-ownership:
en: 8d686d07b0809596
zh: 9ca7658daf583a66
en: 863ebc136313b73b
zh: 6aa9a9925ceaa80a
/deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation:
en: 0d11af8639977a6d
zh: 86b086b12d2210d3
+2
View File
@@ -92,6 +92,8 @@ Update copy preserves the complete version, including prerelease suffixes, witho
Electron chooses typed English or Chinese shell copy from its application locale and falls back to English. The macOS application bundle declares English and Simplified Chinese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources.
Windows and macOS relay native fullscreen transitions and the state after each page load to the renderer, so overlays can adapt their caption clearance.
Windows uses a 40-DIP caption with native window controls and colors synchronized from the application palette. Localized Application and Edit entries beside the sidebar toggle open native popup menus. They mount only after the application frame publishes its shell overlay seat, and remain absent during startup loading. Application provides Check for Updates and Exit; Edit provides undo, redo, cut, copy, paste, delete, and select all by sending the corresponding keys to the focused editor, independently of custom shortcut bindings. Plugin management uses the main application's Plugins page. No separate native menu row appears on Alt. Other platforms retain their native menus. Editable fields retain keyboard commands and a context menu without shortcut labels; Chromium supplies command availability, and selected read-only text offers Copy.
On macOS the custom menu retains Electron's standard Window menu and application hide commands, including Minimize (⌘M) and Hide (⌘H). Linux keeps the application and Edit menus.
+2
View File
@@ -94,6 +94,8 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm
Electron 根据应用语言选择类型化的英文或中文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语和简体中文,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。
Windows 和 macOS 会在原生全屏切换及每次页面加载后向渲染器同步全屏状态,让浮层调整顶栏避让距离。
Windows 使用 40 DIP 顶栏,保留原生窗口按钮,颜色随应用调色板同步。侧栏开关旁的本地化“应用”和“编辑”入口打开原生弹出菜单。仅当应用框架发布 shell overlay 席位后才挂载菜单,启动加载期间不显示。“应用”提供检查更新和退出;“编辑”向当前编辑器发送对应按键,提供撤销、重做、剪切、复制、粘贴、删除和全选,不受自定义快捷键绑定影响。插件管理使用主应用的“插件”页面。按 Alt 不会出现额外的原生菜单行。其他平台保留原生菜单。可编辑区域保留快捷键和不带快捷键标注的右键菜单;命令可用状态由 Chromium 提供,选中的只读文本提供“复制”命令。
macOS 上自定义菜单保留 Electron 的标准 Window 菜单及应用隐藏命令,包括 Minimize(⌘M)和 Hide(⌘H)。Linux 保留应用菜单和 Edit 菜单。
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-desktop",
"description": "Electron desktop shell for a bundled dsh runtime and external plugins",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"private": true,
"license": "MIT",
"type": "module",
+4 -3
View File
@@ -238,9 +238,8 @@ function createWindow(preload: string, show = false, primary = false): BrowserWi
if (['http:', 'https:'].includes(new URL(url).protocol)) void shell.openExternal(url)
return { action: 'deny' }
})
if (process.platform === 'darwin') {
// macOS hides the traffic lights in fullscreen; the page drops its
// clearance for them off the html[data-fullscreen] flag this feeds.
if (process.platform === 'darwin' || process.platform === 'win32') {
// Fullscreen hides native window controls; overlays drop their caption clearance.
const sendFullscreen = (): void => {
if (!window.isDestroyed()) window.webContents.send(DESKTOP_IPC.windowFullscreen, window.isFullScreen())
}
@@ -249,6 +248,8 @@ function createWindow(preload: string, show = false, primary = false): BrowserWi
// Reloads and navigations re-register the preload listener; resend the
// current state so a fullscreen reload does not fall back to windowed CSS.
window.webContents.on('did-finish-load', sendFullscreen)
}
if (process.platform === 'darwin') {
// Deminiaturize reattaches the NSVisualEffectView material late
// (electron/electron#25368), so a transparent window shows the desktop
// through the sidebar until then. Paint an opaque base while minimized or
+4 -3
View File
@@ -17,12 +17,13 @@ export function markDocumentPlatform(): void {
}
/**
* Mirrors the window's macOS fullscreen state onto `<html data-fullscreen>` so
* CSS drops the traffic-light clearance while the lights are hidden. The main
* Mirrors the window's macOS and Windows fullscreen state onto `<html data-fullscreen>` so
* CSS drops the clearance for hidden native window controls. The main
* process sends the state on every transition and after each load.
*/
export function syncWindowFullscreen(): void {
if (process.platform !== 'darwin') return
// The main process sends this IPC only on macOS and Windows; other platforms need no listener.
if (process.platform !== 'darwin' && process.platform !== 'win32') return
ipcRenderer.on(DESKTOP_IPC.windowFullscreen, (_event, fullscreen: boolean) => {
const root = document.documentElement as HTMLElement | null
if (root === null) return
+78
View File
@@ -0,0 +1,78 @@
/** External PowerShell must execute skill resources carried inside the Desktop archive. */
import { spawnSync } from 'node:child_process'
import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { finished } from 'node:stream/promises'
import type { WriteStream } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const builderRequire = createRequire(require.resolve('app-builder-lib/package.json'))
const { createPackageWithOptions } = builderRequire('@electron/asar') as {
createPackageWithOptions: (source: string, destination: string, options: object) => Promise<WriteStream>
}
const packageDirectory = fileURLToPath(new URL('../../../packages/sandbox/sandbox-windows-acl/', import.meta.url))
it.skipIf(process.platform !== 'win32' || process.env.DSH_EXAMPLE_MODE !== 'lib')('executes the archived ACL skill through external PowerShell and disposes its copy', { retry: 0 }, async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-acl-asar-'))
try {
const source = join(root, 'package')
// Electron reads the archived package here; full installer layout and SEA execution have separate coverage needs.
await mkdir(source)
for (const path of ['lib', 'assets', 'package.json']) await cp(join(packageDirectory, path), join(source, path), { recursive: true })
const archive = join(root, 'app.asar')
await finished(await createPackageWithOptions(source, archive, {}))
const entry = join(root, 'probe.mjs')
await writeFile(entry, `
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { createRequire, registerHooks } from 'node:module';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const [packageDirectory, archive, root] = process.argv.slice(2);
const require = createRequire(join(packageDirectory, 'package.json'));
// Only the package under test is archived; resolve its real dependencies from the built workspace.
const archiveUrl = pathToFileURL(archive).href;
registerHooks({ resolve(specifier, context, next) {
if (context.parentURL?.startsWith(archiveUrl) && !specifier.startsWith('.') && !specifier.startsWith('node:')) {
return next(pathToFileURL(require.resolve(specifier)).href, context);
}
return next(specifier, context);
} });
const { Context } = await import(pathToFileURL(require.resolve('@deepseek-ai/cordis')));
const { default: SkillRegistry } = await import(pathToFileURL(require.resolve('@deepseek-ai/dsh-skill')));
const { registerAclDiagnosisSkill, ACL_DIAGNOSIS_SKILL } = await import(pathToFileURL(join(archive, 'lib/index.js')));
const ctx = new Context();
try {
await ctx.plugin(SkillRegistry);
const fiber = await ctx.plugin({ name: 'acl-skill-archive-probe', inject: ['skills'], apply: registerAclDiagnosisSkill });
const skill = await ctx.skills.get(ACL_DIAGNOSIS_SKILL);
assert.equal(skill.resourceBase.kind, 'directory');
const directory = skill.resourceBase.path;
assert.ok(!directory.includes('.asar'));
const output = execFileSync('pwsh', ['-NoProfile', '-NonInteractive', '-File',
join(directory, 'scripts/diagnose-windows-sandbox-acl.ps1'), '-Path', root],
{ encoding: 'utf8', timeout: 30000, windowsHide: true });
assert.ok(output.includes('READABLE=True'));
await fiber.dispose();
assert.ok(!existsSync(directory));
assert.deepEqual(await ctx.skills.list(), []);
process.stdout.write('ACL_SKILL_ASAR_OK');
} finally { await ctx.fiber.dispose(); }
`)
const electron = require('electron') as string
const result = spawnSync(electron, [entry, packageDirectory, archive, root], {
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', TEMP: root, TMP: root }, encoding: 'utf8', timeout: 60_000, windowsHide: true,
})
expect(result.error).toBeUndefined()
expect(result.signal).toBeNull()
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toBe('ACL_SKILL_ASAR_OK')
} finally {
await rm(root, { recursive: true, force: true })
}
})
+3 -3
View File
@@ -717,8 +717,8 @@ describe('desktop main startup', () => {
expect((await handler(new Request('dsh-app://foreign/index.html'))).status).toBe(404)
})
it('relays the macOS fullscreen state on transitions and after each load', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
it.each(['darwin', 'win32'] as const)('relays the %s fullscreen state on transitions and after each load', async (platform) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
await import('../src/main.ts')
await harness.preparing.promise
const window = harness.windows[0]!
@@ -740,7 +740,7 @@ describe('desktop main startup', () => {
expect(sent()).toHaveLength(relayed)
})
it.each(['win32', 'linux'] as const)('registers no fullscreen relay on %s', async (platform) => {
it.each(['linux'] as const)('registers no fullscreen relay on %s', async (platform) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
await import('../src/main.ts')
await harness.preparing.promise
@@ -12,14 +12,14 @@ afterEach(() => {
on.mockClear()
})
it.each(['win32', 'linux'] as const)('subscribes no fullscreen channel on %s', (platform) => {
it.each(['linux'] as const)('subscribes no fullscreen channel on %s', (platform) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
syncWindowFullscreen()
expect(on).not.toHaveBeenCalled()
})
it('mirrors the sent fullscreen state onto html[data-fullscreen] on darwin', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
it.each(['darwin', 'win32'] as const)('mirrors the sent fullscreen state onto html[data-fullscreen] on %s', (platform) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
syncWindowFullscreen()
expect(on).toHaveBeenCalledExactlyOnceWith(DESKTOP_IPC.windowFullscreen, expect.any(Function))
const handler = on.mock.calls[0]?.[1] as (event: unknown, fullscreen: boolean) => void
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-frontend",
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -33,6 +33,7 @@
"@deepseek-ai/cordis-plugin-group": "workspace:~",
"@deepseek-ai/dsh-client-modules": "workspace:*",
"@deepseek-ai/dsh-client-store": "workspace:*",
"@deepseek-ai/dsh-llm-mock-server": "workspace:*",
"@deepseek-ai/dsh-client-test-runtime": "workspace:*",
"@deepseek-ai/dsh-client-ui-deliverables": "workspace:*",
"@deepseek-ai/dsh-client-ui-dockkit": "workspace:*",
+94
View File
@@ -0,0 +1,94 @@
/** Browser geometry for settings, expanded plugin dialogs, and their menus under Windows caption markers. */
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright'
import { expect, it } from 'vitest'
import { launchWebScaffold, watchConsole } from './scaffold.ts'
import { openSettings, ZH_BROWSER_LOCALE } from './support.ts'
it('keeps settings and expanded plugin dialogs clear of the Windows caption across fullscreen transitions', async () => {
const scaffold = await launchWebScaffold({
profile: { packages: [] },
extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
})
try {
const browser = await chromium.launch()
try {
const page = await browser.newPage({ viewport: { width: 520, height: 600 }, locale: ZH_BROWSER_LOCALE })
const console = watchConsole(page)
await page.goto(scaffold.authenticatedUrl)
await page.waitForSelector('[class*="frame"]')
await page.evaluate(() => {
document.documentElement.setAttribute('data-windows-titlebar', '')
document.documentElement.style.setProperty('--dsh-windows-titlebar-height', '40px')
})
const reopenSidebar = page.getByRole('button', { name: '打开侧边栏', exact: true })
if (await reopenSidebar.isVisible()) await reopenSidebar.click()
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置', exact: true })
const settingsMask = settings.locator('..').locator(':scope > [aria-hidden="true"]')
for (const fullscreen of [false, true, false]) {
await page.evaluate(value => document.documentElement.toggleAttribute('data-fullscreen', value), fullscreen)
const margin = fullscreen ? 24 : 60
await expect.poll(async () => (await settings.boundingBox())?.y).toBe(margin)
expect((await settings.boundingBox())?.height).toBe(600 - 2 * margin)
expect((await settingsMask.boundingBox())?.y).toBe(fullscreen ? 0 : 40)
expect((await settings.locator('..').boundingBox())?.height).toBe(600)
}
expect(await page.evaluate(() => document.elementFromPoint(300, 20)?.getAttribute('role'))).toBe('presentation')
await page.mouse.click(300, 20)
expect(await settings.isVisible()).toBe(true)
await settings.getByRole('button', { name: '工作区内修改', exact: true }).click()
const menu = page.getByRole('menu')
await menu.waitFor()
const menuRect = await menu.boundingBox()
expect(menuRect?.y).toBeGreaterThanOrEqual(60)
expect(menuRect!.y + menuRect!.height).toBeLessThanOrEqual(588)
await page.keyboard.press('Escape')
await menu.waitFor({ state: 'hidden' })
expect(await settings.isVisible()).toBe(true)
await settings.getByRole('button', { name: '关闭', exact: true }).click()
if (await reopenSidebar.isVisible()) await reopenSidebar.click()
await page.getByRole('navigation', { name: '全局面板' }).getByRole('button', { name: '插件', exact: true }).click()
await page.locator('[data-plugin-panel]').getByRole('button', { name: '添加插件', exact: true }).click()
const plugin = page.getByRole('dialog', { name: '添加插件', exact: true })
await plugin.getByRole('button', { name: '插件安装引导和示例', exact: true }).click()
await expect.poll(() => plugin.locator('button[aria-expanded="true"]').count()).toBe(1)
for (const fullscreen of [false, true, false]) {
await page.evaluate(value => document.documentElement.toggleAttribute('data-fullscreen', value), fullscreen)
const margin = fullscreen ? 24 : 60
await expect.poll(async () => (await plugin.boundingBox())?.y).toBe(margin)
expect((await plugin.boundingBox())?.height).toBe(600 - 2 * margin)
expect((await plugin.locator('..').locator(':scope > [aria-hidden="true"]').boundingBox())?.y).toBe(fullscreen ? 0 : 40)
expect(await plugin.getByRole('button', { name: '关闭', exact: true }).isVisible()).toBe(true)
expect(await plugin.getByRole('button', { name: '安装', exact: true }).isVisible()).toBe(true)
}
await plugin.getByRole('button', { name: /^安装源/ }).click()
const registry = page.locator('[data-install-registry]')
await registry.waitFor()
const registryRect = await registry.boundingBox()
expect(registryRect!.y).toBeGreaterThanOrEqual(60)
expect(registryRect!.y + registryRect!.height).toBeLessThanOrEqual(588)
await page.keyboard.press('Escape')
await registry.waitFor({ state: 'hidden' })
expect(await plugin.isVisible()).toBe(true)
await page.keyboard.press('Escape')
await plugin.waitFor({ state: 'hidden' })
for (const platform of ['darwin', 'browser']) {
await page.evaluate((value) => {
document.documentElement.removeAttribute('data-windows-titlebar')
document.documentElement.removeAttribute('data-fullscreen')
if (value === 'darwin') document.documentElement.setAttribute('data-platform', value)
else document.documentElement.removeAttribute('data-platform')
}, platform)
if (await reopenSidebar.isVisible()) await reopenSidebar.click()
await openSettings(page, 'zh')
await expect.poll(async () => (await settings.boundingBox())?.y).toBe(platform === 'darwin' ? 68 : 24)
expect((await settingsMask.boundingBox())?.y).toBe(0)
await page.keyboard.press('Escape')
await settings.waitFor({ state: 'hidden' })
}
expect(console.pageErrors).toEqual([])
expect(console.warnings).toEqual([])
} finally { await browser.close() }
} finally { await scaffold.close() }
})
+10 -2
View File
@@ -271,9 +271,17 @@ it.skipIf(MODE === 'record').each(['en-US', 'zh-CN'])('fills the spreadsheet pan
const meetingCanvas = await canvas.elementHandle()
if (meetingCanvas === null) throw new Error('meeting spreadsheet canvas is unavailable')
await expectExcelLayout(excel)
await excel.locator('.fortune-sheet-overlay').click({ position: { x: 60, y: 40 } })
await expect.poll(() => formula.innerText()).toBe('会议纪要')
// Arrow keys pick the cell from committed state, so a later render cannot re-resolve it
// against the reflowed grid the way it re-resolves a pointer press.
const overlay = excel.locator('.fortune-sheet-overlay')
await overlay.focus()
await expect.poll(() => overlay.evaluate(node => document.activeElement === node)).toBe(true)
await page.keyboard.press('ArrowRight')
await expect.poll(() => selection.innerText()).toBe('B1')
await expect.poll(() => formula.innerText()).toBe('')
await page.keyboard.press('ArrowLeft')
await expect.poll(() => selection.innerText()).toBe('A1')
await expect.poll(() => formula.innerText()).toBe('会议纪要')
for (const width of [1000, 360, 1000, 360]) {
await layout.evaluate((node, width) => { node.textContent = `[data-sidebar-right-panel] { width: ${width}px !important; }` }, width)
await expect.poll(async () => Math.round((await panel.boundingBox())!.width)).toBe(width)
@@ -29,4 +29,6 @@
- button "编辑快捷键"
- text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为
- button "排队发送"
- text: 在使用官方模型 API 时上传 Session Log 帮助改进 DeepSeek 模型与产品
- switch "在使用官方模型 API 时上传 Session Log"
- text: 当前版本:{{version}}
@@ -29,4 +29,6 @@
- button "Edit shortcuts"
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
- button "Queue"
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
- switch "Upload Session Log when using the official model API"
- text: "Current version: {{version}}"
@@ -1,5 +1,5 @@
- dialog "内测声明":
- heading "内测声明" [level=2]
- paragraph: DeepSeek Harness 目前的 0.1 版本仍处在面向 Harness 开发者进行测试的阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者的反馈建议。预计 DeepSeek Harness 的核心插件以及基础 API 都会在接下来的一段时间内快速迭代、持续演化。
- paragraph: 我们期待与全球开发者一起,在开源、开放、可复用、可组合的基础设施之上,共同探索智能上限。欢迎全球 Harness 开发者加入 DSH 插件生态。
- dialog "预览版说明":
- heading "预览版说明" [level=2]
- paragraph: DeepSeek Harness 目前的 0.2 版本仍处于预览阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者和用户的反馈建议。现在,新的桌面端面向广泛用户,开发者相关的进阶功能可在配置中开启使用。预计 DeepSeek Harness 的产品功能以及插件 API 都会继续快速迭代、持续演化,并逐渐趋于稳定。
- paragraph: 我们期待与全球用户和开发者一起,在开源、可复用、可组合的基础设施之上,共同探索智能上限。欢迎大家用 DeepSeek Harness 将想法变成现实,与社区一起丰富插件生态。
- button "继续"
@@ -29,4 +29,6 @@
- button "Edit shortcuts"
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
- button "Queue"
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
- switch "Upload Session Log when using the official model API"
- text: "Current version: {{version}}"
@@ -29,4 +29,6 @@
- button "Edit shortcuts"
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
- button "Queue"
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
- switch "Upload Session Log when using the official model API"
- text: "Current version: {{version}}"
@@ -29,4 +29,6 @@
- button "编辑快捷键"
- text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为
- button "排队发送"
- text: 在使用官方模型 API 时上传 Session Log 帮助改进 DeepSeek 模型与产品
- switch "在使用官方模型 API 时上传 Session Log"
- text: 当前版本:{{version}}
+1 -1
View File
@@ -344,7 +344,7 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
await reasoningComplete.promise
expect(await thinking.getAttribute('data-preview')).toBeNull()
expect(await thinking.getByRole('button').getAttribute('aria-expanded')).toBe('false')
expect(await thinking.locator('[data-streaming]').isVisible()).toBe(false)
expect(await thinking.locator('[data-streaming]:not([inert] *)').isVisible()).toBe(false)
}
observedReasoning.resolve(undefined)
return await settled
@@ -157,7 +157,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
;(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings = sightings
setInterval(() => {
if (document.querySelector(
'[role="dialog"][aria-label="内测声明"], '
'[role="dialog"][aria-label="预览版说明"], '
+ '[role="dialog"][aria-label="开始你的创作"], '
+ '[role="dialog"][aria-label="添加一个 API Key 开始使用"]',
) !== null) {
+1 -1
View File
@@ -260,7 +260,7 @@ fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify({ path, action, con
openFontSize: getComputedStyle(open).fontSize,
}
})
expect(geometry.answerToPresented).toBeCloseTo(20, 1)
expect(geometry.answerToPresented).toBeCloseTo(16, 1)
expect(geometry.presentedToActions).toBeCloseTo(20, 1)
expect(geometry.cardHeight).toBeCloseTo(60, 1)
expect(geometry.cardColumnGap).toBeCloseTo(10, 1)
+1 -1
View File
@@ -102,7 +102,7 @@ it('shows completed paragraph first lines across blank lines with a right-edge f
first.proceed.resolve(undefined)
await second.arrived.promise
const preview = reasoning.locator('[data-streaming]')
const preview = reasoning.locator('[data-streaming]:not([inert] *)')
await expect.poll(() => preview.textContent()).toBe('First paragraph')
expect(await preview.isVisible()).toBe(true)
await preview.evaluate((element) => { element.setAttribute('data-retained-preview', 'true') })
+4 -4
View File
@@ -113,11 +113,11 @@ export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-settings-general'
/** The installed bundle carrying the scaffold's deployment defaults; the plugin manager lists it beside fixture bundles. */
export const SCAFFOLD_DEFAULTS_BUNDLE = 'dsh-web-scaffold-defaults'
export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
export const WELCOME_NOTICE_VERSION = '2026-08-13.1'
export const WELCOME_NOTICE_VERSION = '2026-09-28.1'
export const WELCOME_NOTICE_COPY = {
zh: {
title: '内测声明',
body: 'DeepSeek Harness 目前的 0.1 版本仍处在面向 Harness 开发者进行测试的阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者的反馈建议。预计 DeepSeek Harness 的核心插件以及基础 API 都会在接下来的一段时间内快速迭代、持续演化。\n\n我们期待与全球开发者一起,在开源、开放、可复用、可组合的基础设施之上,共同探索智能上限。欢迎全球 Harness 开发者加入 DSH 插件生态。',
title: '预览版说明',
body: 'DeepSeek Harness 目前的 0.2 版本仍处于预览阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者和用户的反馈建议。现在,新的桌面端面向广泛用户,开发者相关的进阶功能可在配置中开启使用。预计 DeepSeek Harness 的产品功能以及插件 API 都会继续快速迭代、持续演化,并逐渐趋于稳定。\n\n我们期待与全球用户和开发者一起,在开源、可复用、可组合的基础设施之上,共同探索智能上限。欢迎大家用 DeepSeek Harness 将想法变成现实,与社区一起丰富插件生态。',
continueLabel: '继续',
},
} as const
@@ -682,7 +682,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
// Live fields use a shared deployment layer; process-specific ports and roots stay in CLI overlays.
const formEntries = new Set(['agent-default-model', 'agent-preset-registry', 'llm-deepseek', 'llm-pi-ai',
'web-search-deepseek', 'agent-loop', 'subagent', 'bash-sandbox', 'pwsh-sandbox',
'ui-theme', 'locale', 'ui-chat', 'ui-conversation', 'ui-settings', 'ui-settings-general', 'permission'])
'session-log-deepseek', 'ui-theme', 'locale', 'ui-chat', 'ui-conversation', 'ui-settings', 'ui-settings-general', 'permission'])
const formDefaults: PatchOptions[] = []
const processOverlays = overlayPatches.map((patch) => {
if (patch.id === undefined || !formEntries.has(patch.id) || patch.config === undefined) return patch
+72
View File
@@ -0,0 +1,72 @@
/** Browser saves control the session-log field on real DeepSeek HTTP requests. */
import { mkdtemp, rm, writeFile, readFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { chromium } from 'playwright'
import yaml from 'js-yaml'
import { expect, it, onTestFinished } from 'vitest'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
import { launchWebScaffold, webSnapshotMode } from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, openSettings } from './support.ts'
it.skipIf(webSnapshotMode() === 'record')('persists the upload switch and omits or resumes session logs on subsequent API requests', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-upload-browser-'))
onTestFinished(() => rm(root, { recursive: true, force: true }))
const server = await startMockLlmServer({ sequence: ['success', 'success', 'success'], successText: 'UPLOAD_SETTING_DONE' })
onTestFinished(() => server.close())
const key = credentialRef('DSH_UPLOAD_BROWSER_KEY')
const overlay = join(root, 'upload.yml')
await writeFile(overlay, JSON.stringify([
{ id: 'llm-deepseek', config: { baseURL: server.baseURL, apiKeyEnv: key } },
{ id: 'session-log-deepseek', config: { enabled: true } },
{ id: 'agent-default-model', config: { provider: 'deepseek-official', model: 'deepseek-v4-flash' } },
]))
const scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, extraOverlayPath: overlay })
onTestFinished(() => scaffold.close())
await scaffold.ctx.credentials.set(key, 'upload-browser-test-key')
const browser = await chromium.launch()
onTestFinished(() => browser.close())
const page = await newEnglishPage(browser)
await page.goto(scaffold.authenticatedUrl)
await connectFreshWorkspace(page, scaffold.workspaceCwd)
const input = page.locator('[data-composer-input]').first()
const send = async (text: string) => {
const settled = scaffold.whenTurnSettled()
await input.fill(text)
await input.press('Enter')
return settled
}
const setUpload = async (enabled: boolean) => {
await openSettings(page, 'en')
const toggle = page.getByRole('switch', { name: 'Upload Session Log when using the official model API' })
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe(String(!enabled))
await toggle.click()
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe(String(enabled))
await expect.poll(async () => {
const patches = yaml.load(await readFile(join(scaffold.harnessHome, 'profiles/scaffold/cordis.patch.yml'), 'utf8')) as { id: string; config?: { enabled?: boolean } }[]
return patches.find(patch => patch.id === 'session-log-deepseek')?.config?.enabled ?? true
}).toBe(enabled)
await page.keyboard.press('Escape')
}
const sessionId = await send('upload-enabled-marker')
expect(server.requests).toHaveLength(1)
expect(server.requests[0]!.body).toHaveProperty('dsh_session_log')
const first = server.requests[0]!.body as { dsh_session_log: { throughSeq: number } }
await setUpload(false)
await page.reload()
await openSettings(page, 'en')
await expect.poll(() => page.getByRole('switch', { name: 'Upload Session Log when using the official model API' }).getAttribute('aria-checked')).toBe('false')
await page.keyboard.press('Escape')
expect(await send('upload-disabled-marker')).toBe(sessionId)
expect(server.requests).toHaveLength(2)
expect(server.requests[1]!.body).not.toHaveProperty('dsh_session_log')
await setUpload(true)
expect(await send('upload-resumed-marker')).toBe(sessionId)
expect(server.requests).toHaveLength(3)
const resumed = server.requests[2]!.body as { dsh_session_log: { events: unknown[] } }
expect(resumed).toHaveProperty('dsh_session_log.afterSeq', first.dsh_session_log.throughSeq)
expect(JSON.stringify(resumed.dsh_session_log.events)).toContain('"upload-disabled-marker"')
expect(server.requests.map(request => request.outcome)).toEqual(['completed', 'completed', 'completed'])
})
+1 -1
View File
@@ -251,7 +251,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
element.closest<HTMLElement>('[data-chat-flow-kind="turn-process"]')?.getBoundingClientRect().bottom)
const answerTop = await page.getByText('DONE', { exact: true }).evaluate(element =>
element.closest<HTMLElement>('[data-chat-flow-kind="assistant-step"]')?.getBoundingClientRect().top)
expect(answerTop).toBe((processBottom ?? 0) + 8)
expect(answerTop).toBe((processBottom ?? 0) + 16)
await process.focus()
const completed = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
await compareOrRefreshGolden(COMPLETED_EXPECTED, completed, MODE)
+1 -1
View File
@@ -134,7 +134,7 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
const disclosures = element.querySelectorAll('[data-disclosure-row]')
const runHeader = disclosures[0]
const phaseHeader = disclosures[1]
const phaseTitle = phaseHeader?.children.item(1) as HTMLElement | null
const phaseTitle = phaseHeader?.querySelector('[class*="phaseTitle"]') as HTMLElement | null
const phaseStatus = element.querySelector('[data-phase-status-text]')
const originalPhaseTitle = phaseTitle?.textContent ?? ''
if (phaseTitle !== null) phaseTitle.textContent = 'A phase name long enough to require ellipsis in the narrow layout'
+2
View File
@@ -74,6 +74,8 @@
"tests/plugin-install-github.e2e.ts",
"tests/client-plugin-live.e2e.ts",
"tests/settings-chrome.e2e.ts",
"tests/session-log-upload.e2e.ts",
"tests/caption-overlays.e2e.ts",
"tests/settings-appearance.e2e.ts",
"tests/menu-material.e2e.ts",
"tests/desktop-locale.e2e.ts",
+2 -2
View File
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 5ef3da69b4de1d8a
/capability-seams-and-core-services:
en: 00ee1236e43f5538
zh: aed1f351bf657eea
en: 890a2217279b7c70
zh: a5c64adca1fb5a40
+3 -1
View File
@@ -167,6 +167,7 @@ flowchart LR
svc_sessionProjectionCache["ctx.sessionProjectionCache<br/>Persisted projection cache"]
pkg_skill["skill"]
svc_skills["ctx.skills<br/>Skill provider registry"]
pkg_sandbox_windows_acl["sandbox-windows-acl"]
pkg_skill_badge["skill-badge"]
pkg_skill_filesystem["skill-filesystem"]
pkg_skill_office["skill-office"]
@@ -365,6 +366,7 @@ flowchart LR
pkg_sandbox_local --> svc_sandbox
pkg_sandbox_policy --> svc_sandboxPolicy
pkg_sandbox_ssh --> svc_sandbox
pkg_sandbox_windows_acl --> svc_skills
pkg_schedule --> svc_schedule
pkg_session --> svc_sessions
pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions
@@ -634,7 +636,7 @@ flowchart LR
| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. |
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`api-session-controller`](../packages/api/session-controller), [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session/session-title) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and the Session controller serves baselines and pushes changed values. |
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points), serves cached projection views, and accelerates prepared-Session projection hydration. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
+3 -1
View File
@@ -169,6 +169,7 @@ flowchart LR
svc_sessionProjectionCache["ctx.sessionProjectionCache<br/>Persisted projection cache"]
pkg_skill["skill"]
svc_skills["ctx.skills<br/>Skill provider registry"]
pkg_sandbox_windows_acl["sandbox-windows-acl"]
pkg_skill_badge["skill-badge"]
pkg_skill_filesystem["skill-filesystem"]
pkg_skill_office["skill-office"]
@@ -367,6 +368,7 @@ flowchart LR
pkg_sandbox_local --> svc_sandbox
pkg_sandbox_policy --> svc_sandboxPolicy
pkg_sandbox_ssh --> svc_sandbox
pkg_sandbox_windows_acl --> svc_skills
pkg_schedule --> svc_schedule
pkg_session --> svc_sessions
pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions
@@ -636,7 +638,7 @@ flowchart LR
| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | 插件注册直接面向人的命令,而不会把调用发送给模型。 |
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session/session-projection) | - | [`api-session-controller`](../packages/api/session-controller), [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session/session-title) | - | 各领域注册由状态驱动的折叠单元;主动驱动过程维护每个会话的水位状态,Session controller 提供 baseline 并推送发生变化的值。 |
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),提供缓存投影视图,并加速 prepared Session 的投影恢复。 |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 |
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 |
+4 -2
View File
@@ -2557,19 +2557,20 @@ export interface JsonRpcConfig {
## `@deepseek-ai/dsh-session-log-deepseek`
- `inject`: `deepseekLlmApiExtensions` · `sessions`
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
- `source`: [`packages/session/session-log-deepseek/src/index.ts:39`](../packages/session/session-log-deepseek/src/index.ts)
```ts config-catalog
/** Session-log request contribution configuration. */
export interface Config {
/** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `true`. */
enabled?: boolean
enabled: Volatile<boolean>
/**
* Largest serialized `dsh_session_log` field, in UTF-8 bytes, that one request carries.
* A request uploads the longest pending event prefix that fits; later requests continue
* after its acceptance. Defaults to 8 MiB.
*/
maxBytes?: number
maxBytes: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-session-log-deepseek -->
@@ -4346,6 +4347,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-client-ui-settings-general` | — | [`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-plugin-inventory` | — | [`packages/client/ui-settings-plugin-inventory/src/index.ts`](../packages/client/ui-settings-plugin-inventory/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-plugins` | — | [`packages/client/ui-settings-plugins/src/index.ts`](../packages/client/ui-settings-plugins/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-session-log` | — | [`packages/client/ui-settings-session-log/src/index.ts`](../packages/client/ui-settings-session-log/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-shell` | — | [`packages/client/ui-settings-shell/src/index.ts`](../packages/client/ui-settings-shell/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-subagent` | — | [`packages/client/ui-settings-subagent/src/index.ts`](../packages/client/ui-settings-subagent/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-web-search` | — | [`packages/client/ui-settings-web-search/src/index.ts`](../packages/client/ui-settings-web-search/src/index.ts) |
+4 -2
View File
@@ -2559,19 +2559,20 @@ export interface JsonRpcConfig {
## `@deepseek-ai/dsh-session-log-deepseek`
- `inject`: `deepseekLlmApiExtensions` · `sessions`
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
- `source`: [`packages/session/session-log-deepseek/src/index.ts:39`](../packages/session/session-log-deepseek/src/index.ts)
```ts config-catalog
/** Session-log request contribution configuration. */
export interface Config {
/** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `true`. */
enabled?: boolean
enabled: Volatile<boolean>
/**
* Largest serialized `dsh_session_log` field, in UTF-8 bytes, that one request carries.
* A request uploads the longest pending event prefix that fits; later requests continue
* after its acceptance. Defaults to 8 MiB.
*/
maxBytes?: number
maxBytes: number
}
```
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-session-log-deepseek -->
@@ -4348,6 +4349,7 @@ export interface Config {
| `@deepseek-ai/dsh-client-ui-settings-general` | — | [`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-plugin-inventory` | — | [`packages/client/ui-settings-plugin-inventory/src/index.ts`](../packages/client/ui-settings-plugin-inventory/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-plugins` | — | [`packages/client/ui-settings-plugins/src/index.ts`](../packages/client/ui-settings-plugins/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-session-log` | — | [`packages/client/ui-settings-session-log/src/index.ts`](../packages/client/ui-settings-session-log/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-shell` | — | [`packages/client/ui-settings-shell/src/index.ts`](../packages/client/ui-settings-shell/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-subagent` | — | [`packages/client/ui-settings-subagent/src/index.ts`](../packages/client/ui-settings-subagent/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-settings-web-search` | — | [`packages/client/ui-settings-web-search/src/index.ts`](../packages/client/ui-settings-web-search/src/index.ts) |
+5 -2
View File
@@ -191,6 +191,7 @@ flowchart TD
pkg_client_ui_settings_models["client-ui-settings-models"]
pkg_client_ui_settings_plugin_inventory["client-ui-settings-plugin-inventory"]
pkg_client_ui_settings_plugins["client-ui-settings-plugins"]
pkg_client_ui_settings_session_log["client-ui-settings-session-log"]
pkg_client_ui_settings_shell["client-ui-settings-shell"]
pkg_client_ui_settings_subagent["client-ui-settings-subagent"]
pkg_client_ui_settings_web_search["client-ui-settings-web-search"]
@@ -483,7 +484,6 @@ flowchart TD
pkg_credentials_local --> pkg_home_paths
pkg_credentials_local --> pkg_launch_environment
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_sandbox_windows_acl --> pkg_subprocess
pkg_subprocess_local --> pkg_subprocess
pkg_subprocess_local --> pkg_timeout
pkg_skill_badge --> pkg_skill
@@ -503,6 +503,8 @@ flowchart TD
pkg_persona --> pkg_system_prompt
pkg_sandbox --> pkg_llm
pkg_sandbox --> pkg_session
pkg_sandbox_windows_acl --> pkg_skill
pkg_sandbox_windows_acl --> pkg_subprocess
pkg_session_format_catalog --> pkg_session
pkg_session_log_deepseek --> pkg_deepseek_llm_api_extensions
pkg_session_log_deepseek --> pkg_invariants
@@ -1440,6 +1442,7 @@ flowchart TD
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | — |
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | — |
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | — |
| [`client-ui-settings-session-log`](../packages/client/ui-settings-session-log) | `client` | — |
| [`client-ui-settings-shell`](../packages/client/ui-settings-shell) | `client` | — |
| [`client-ui-settings-subagent`](../packages/client/ui-settings-subagent) | `client` | — |
| [`client-ui-settings-web-search`](../packages/client/ui-settings-web-search) | `client` | — |
@@ -1514,7 +1517,6 @@ flowchart TD
| [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`subprocess`](../packages/subprocess/subprocess) |
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) |
@@ -1525,6 +1527,7 @@ flowchart TD
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) |
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess) |
| [`session-format-catalog`](../packages/session/session-format-catalog) | `session` | [`session`](../packages/core/session) |
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
+5 -2
View File
@@ -193,6 +193,7 @@ flowchart TD
pkg_client_ui_settings_models["client-ui-settings-models"]
pkg_client_ui_settings_plugin_inventory["client-ui-settings-plugin-inventory"]
pkg_client_ui_settings_plugins["client-ui-settings-plugins"]
pkg_client_ui_settings_session_log["client-ui-settings-session-log"]
pkg_client_ui_settings_shell["client-ui-settings-shell"]
pkg_client_ui_settings_subagent["client-ui-settings-subagent"]
pkg_client_ui_settings_web_search["client-ui-settings-web-search"]
@@ -485,7 +486,6 @@ flowchart TD
pkg_credentials_local --> pkg_home_paths
pkg_credentials_local --> pkg_launch_environment
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_sandbox_windows_acl --> pkg_subprocess
pkg_subprocess_local --> pkg_subprocess
pkg_subprocess_local --> pkg_timeout
pkg_skill_badge --> pkg_skill
@@ -505,6 +505,8 @@ flowchart TD
pkg_persona --> pkg_system_prompt
pkg_sandbox --> pkg_llm
pkg_sandbox --> pkg_session
pkg_sandbox_windows_acl --> pkg_skill
pkg_sandbox_windows_acl --> pkg_subprocess
pkg_session_format_catalog --> pkg_session
pkg_session_log_deepseek --> pkg_deepseek_llm_api_extensions
pkg_session_log_deepseek --> pkg_invariants
@@ -1442,6 +1444,7 @@ flowchart TD
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | — |
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | — |
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | — |
| [`client-ui-settings-session-log`](../packages/client/ui-settings-session-log) | `client` | — |
| [`client-ui-settings-shell`](../packages/client/ui-settings-shell) | `client` | — |
| [`client-ui-settings-subagent`](../packages/client/ui-settings-subagent) | `client` | — |
| [`client-ui-settings-web-search`](../packages/client/ui-settings-web-search) | `client` | — |
@@ -1516,7 +1519,6 @@ flowchart TD
| [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`subprocess`](../packages/subprocess/subprocess) |
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
| [`skill-office`](../packages/skill/skill-office) | `skill` | [`skill`](../packages/skill/skill) |
@@ -1527,6 +1529,7 @@ flowchart TD
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) |
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess) |
| [`session-format-catalog`](../packages/session/session-format-catalog) | `session` | [`session`](../packages/core/session) |
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
+2 -2
View File
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/skills.md
/skills:
en: dfadaef222efffb5
zh: 309c88debfe92ce6
en: 047c73a023dcdcd1
zh: 052f42fbfd202f32
/skills/provider-registry:
en: abe338b6c6d1ebd1
zh: c604db2de10a23c4
+2 -2
View File
@@ -2,9 +2,9 @@
English | [中文](skills.zh.md)
The [skill capability family](../../packages/skill) includes the Service Definition ([dsh-skill](../../packages/skill/skill), `ctx.skills`), the local Service Provider ([dsh-skill-filesystem](../../packages/skill/skill-filesystem)), optional packaged providers ([dsh-skill-badge](../../packages/skill/skill-badge) and [dsh-skill-office](../../packages/skill/skill-office)), and the Consumer ([dsh-tool-skill](../../packages/skill/tool-skill)). The registry merges provider catalogs across its host and per-scope layers; providers contribute local or packaged skills; the Consumer owns the initial and replacement catalogs plus the model-facing `skill` tool. Skills are optional instructions, not session events, so their vocabulary lives here rather than in [core.md](core.md).
The [skill capability family](../../packages/skill) includes the Service Definition ([dsh-skill](../../packages/skill/skill), `ctx.skills`), the local Service Provider ([dsh-skill-filesystem](../../packages/skill/skill-filesystem)), optional packaged providers ([dsh-skill-badge](../../packages/skill/skill-badge), [dsh-skill-office](../../packages/skill/skill-office), and the Windows ACL diagnosis provider in [dsh-sandbox-windows-acl](../../packages/sandbox/sandbox-windows-acl)), and the Consumer ([dsh-tool-skill](../../packages/skill/tool-skill)). The registry merges provider catalogs across its host and per-scope layers; providers contribute local or packaged skills; the Consumer owns the initial and replacement catalogs plus the model-facing `skill` tool. Skills are optional instructions, not session events, so their vocabulary lives here rather than in [core.md](core.md).
Source: [`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts), [`packages/skill/skill-filesystem/src/index.ts`](../../packages/skill/skill-filesystem/src/index.ts), [`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts), [`packages/skill/skill-office/src/index.ts`](../../packages/skill/skill-office/src/index.ts), and [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts).
Source: [`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts), [`packages/skill/skill-filesystem/src/index.ts`](../../packages/skill/skill-filesystem/src/index.ts), [`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts), [`packages/skill/skill-office/src/index.ts`](../../packages/skill/skill-office/src/index.ts), [`packages/sandbox/sandbox-windows-acl/src/acl-skill.ts`](../../packages/sandbox/sandbox-windows-acl/src/acl-skill.ts), and [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts).
## Provider registry
+2 -2
View File
@@ -2,9 +2,9 @@
[English](skills.md) | 中文
[skill(技能)能力族](../../packages/skill) 包含 Service Definition([dsh-skill](../../packages/skill/skill),`ctx.skills`)、本地 Service Provider([dsh-skill-filesystem](../../packages/skill/skill-filesystem))、可选的随包提供方([dsh-skill-badge](../../packages/skill/skill-badge) 与 [dsh-skill-office](../../packages/skill/skill-office))和 Consumer([dsh-tool-skill](../../packages/skill/tool-skill))。注册表在其宿主层与各 scope 层之间合并各提供方的目录;提供方贡献本地或随包 skill;Consumer 拥有初始目录和替换目录,以及面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.zh.md)。
[skill(技能)能力族](../../packages/skill) 包含 Service Definition([dsh-skill](../../packages/skill/skill),`ctx.skills`)、本地 Service Provider([dsh-skill-filesystem](../../packages/skill/skill-filesystem))、可选的随包提供方([dsh-skill-badge](../../packages/skill/skill-badge)、[dsh-skill-office](../../packages/skill/skill-office) 及 [dsh-sandbox-windows-acl](../../packages/sandbox/sandbox-windows-acl) 中的 Windows ACL 诊断提供方)和 Consumer([dsh-tool-skill](../../packages/skill/tool-skill))。注册表在其宿主层与各 scope 层之间合并各提供方的目录;提供方贡献本地或随包 skill;Consumer 拥有初始目录和替换目录,以及面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.zh.md)。
源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-filesystem/src/index.ts`](../../packages/skill/skill-filesystem/src/index.ts)、[`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts)、[`packages/skill/skill-office/src/index.ts`](../../packages/skill/skill-office/src/index.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。
源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-filesystem/src/index.ts`](../../packages/skill/skill-filesystem/src/index.ts)、[`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts)、[`packages/skill/skill-office/src/index.ts`](../../packages/skill/skill-office/src/index.ts)、[`packages/sandbox/sandbox-windows-acl/src/acl-skill.ts`](../../packages/sandbox/sandbox-windows-acl/src/acl-skill.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。
## 提供方注册表
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-root",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"license": "MIT",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp",
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-account-controller",
"description": "Expose safe account operations over authenticated Remote",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-gateway",
"description": "Typert Remote Host dispatcher and Client API endpoint",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-job-controller",
"description": "Job Remote observation stream and the reference-counted client job-output service",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-remotes",
"description": "Remote BFF assembly for application-selected Host capabilities",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-session-controller",
"description": "Session Remote commands, cold reads, and live control transport",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-settings-controller",
"description": "Remote owner for the configuration surfaces over the settings-domain seams",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-terminal-controller",
"description": "Session-owned interactive terminals with shell discovery, screen recovery and typed Remote control",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-workspace-controller",
"description": "Workspace Remote commands and reconnect-safe state transport",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-workspace-files",
"description": "Workspace file service and Client resource provider: bounded reads, directory listing, and live metadata over the workspaceFiles Remote namespace",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment-local",
"description": "Private content-addressed DSH_HOME attachment storage",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment",
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-app-boot",
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-cmdline",
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-config-editor",
"description": "Persist plugin configuration through profile patches and Loader reconciliation",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-hmr",
"description": "Coordinated module and profile configuration hot reload",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-plugin-manager",
"description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-browser-use",
"description": "Exclusive named browser-use provider registration",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp-app",
"description": "The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-base",
"description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-headless",
"description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-sdk-app",
"description": "The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-sdk-minimal",
"description": "The standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessions",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+3
View File
@@ -393,6 +393,9 @@
- id: ui-agent-preset
name: '@deepseek-ai/dsh-client-ui-agent-preset'
- id: ui-settings-session-log
name: '@deepseek-ai/dsh-client-ui-settings-session-log'
# The Built-in plugins settings section: the navigation entry and the tab
# row the inventory tab registers into. The official configuration pages
# are the companion rows below.
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-app",
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -175,7 +175,8 @@
"@deepseek-ai/dsh-client-shortcuts": "workspace:*",
"@deepseek-ai/dsh-client-ui-shortcuts": "workspace:*",
"@deepseek-ai/dsh-client-product-analytics": "workspace:*",
"@deepseek-ai/dsh-host-product-telemetry-otel": "workspace:*"
"@deepseek-ai/dsh-host-product-telemetry-otel": "workspace:*",
"@deepseek-ai/dsh-client-ui-settings-session-log": "workspace:*"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
+2 -2
View File
@@ -15,8 +15,8 @@
en: 1eb35b015a6c423f
zh: a8a3dceee2c9e943
/client-web-gui-browser-half/packages:
en: 2a964f7daf3586d5
zh: 2ca0adf9479a1ac5
en: 1aa93b8088fb1962
zh: 10feaed78bc6408c
/client-web-gui-browser-half/related-documentation:
en: 598d07b07449128f
zh: e8c40708ac0e603e
+1
View File
@@ -77,6 +77,7 @@ The kernel packages boot and serve the page; the UI feature packages present it.
| [`ui-settings-models/`](ui-settings-models/README.md) | Provides model-provider configuration and DeepSeek onboarding | — |
| [`ui-settings-shell/`](ui-settings-shell/README.md) | Provides the shell settings page on the Plugins page | — |
| [`ui-settings-agent-loop/`](ui-settings-agent-loop/README.md) | Provides the agent-loop settings page on the Plugins page | — |
| [`ui-settings-session-log/`](ui-settings-session-log/README.md) | Controls API Session-log upload in General settings | — |
| [`ui-settings-subagent/`](ui-settings-subagent/README.md) | Provides the Subagent settings page on the Plugins page | — |
| [`ui-settings-web-search/`](ui-settings-web-search/README.md) | Provides the web-search settings page on the Plugins page | — |
| [`ui-plugin-manager/`](ui-plugin-manager/README.md) | Contributes the sidebar Plugins panel: install, enable, disable, retry, and compose installed packages | — |
+1
View File
@@ -77,6 +77,7 @@ kind: "package-group"
| [`ui-settings-models/`](ui-settings-models/README.zh.md) | 提供模型提供方配置与 DeepSeek 引导 | — |
| [`ui-settings-shell/`](ui-settings-shell/README.zh.md) | 在插件页提供终端设置页 | — |
| [`ui-settings-agent-loop/`](ui-settings-agent-loop/README.zh.md) | 在插件页提供 Agent 循环设置页 | — |
| [`ui-settings-session-log/`](ui-settings-session-log/README.zh.md) | 在通用设置中控制随 API 上传会话日志 | — |
| [`ui-settings-subagent/`](ui-settings-subagent/README.zh.md) | 在插件页提供子智能体设置页 | — |
| [`ui-settings-web-search/`](ui-settings-web-search/README.zh.md) | 在插件页提供网页搜索设置页 | — |
| [`ui-plugin-manager/`](ui-plugin-manager/README.zh.md) | 贡献侧栏的“插件”面板:安装、启用、停用、重试与组合已安装的包 | — |
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-connection",
"description": "Authenticated RPC transport and generation lifecycle",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-file-upload",
"description": "Agent-scoped browser file upload, streaming intake, and staged receipt service",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-hmr",
"description": "Web client graph synchronization and rebuilt-bundle reload transport",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-locale",
"description": "Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-modules",
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-product-analytics",
"description": "Desktop product event collection and authenticated Host reporting",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-resources",
"description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-shortcuts",
"description": "Application keyboard command registry and physical-key routing",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-store",
"description": "React-free observable and snapshot-store contracts with the shared Zustand/Immer engine",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-agent-preset",
"description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-approval",
"description": "Approval composer takeover over the scoped Remote Event waterfall",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-attachment",
"description": "Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-brand-official",
"description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots",
"version": "0.1.7-rc.2",
"version": "0.2.0-rc.1",
"publishConfig": {
"access": "public"
},
+2 -2
View File
@@ -33,8 +33,8 @@
en: 0ca3e8c4eab1ca78
zh: 1a1c71bab151eb7d
/deepseek-ai-dsh-client-ui-chat/grouped-rendering:
en: cc6718a9ff761114
zh: d734fc73c1b77074
en: f7b5260bea372361
zh: 2daa3ff650ec9246
/deepseek-ai-dsh-client-ui-chat/scroll-ownership:
en: 69b021c5a5d8b5a7
zh: 091ae7986c7a8a20
Loaded 100 of 527 files, more files were not shown because too many files have changed in this diff. Show more