diff --git a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml index 76754c3084..c92c021186 100644 --- a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md index 232ab867b1..42327124fd 100644 --- a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md +++ b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.md @@ -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. diff --git a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md index 0fd8210e46..74bce2350d 100644 --- a/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-14-session-log-upload-default.zh.md @@ -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 录制省略该设置,验证产品默认值及持久接受事件。 diff --git a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml index cfa5b6c2ad..7c1c12f8bd 100644 --- a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md index 65780b9bba..ecb2125d33 100644 --- a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md +++ b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md @@ -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 confining executor. +## 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. diff --git a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md index fe9aadb97b..4c1e24bb6d 100644 --- a/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md +++ b/.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md @@ -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) 交付。 + +## 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),但受限读的令牌需要逐路径的读授权——恰好重新引入身份路线付出的代价——而沙盒词汇表从不要求读隔离。 diff --git a/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.i18n.yaml b/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.i18n.yaml new file mode 100644 index 0000000000..c8ae9617c4 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.md b/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.md new file mode 100644 index 0000000000..24e2ae8fe7 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.md @@ -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:/` 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. diff --git a/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.zh.md b/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.zh.md new file mode 100644 index 0000000000..03380bca7a --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-28-dual-release-install-layout-work-budget.zh.md @@ -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:/`,持久化的 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 秒。若图的增长改变了形状却没有改变「包数乘边数」,它仍留在预算内,只有在挂起保护到期时才被发现。 diff --git a/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.i18n.yaml b/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.i18n.yaml new file mode 100644 index 0000000000..f50d88bce9 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.md b/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.md new file mode 100644 index 0000000000..fc4e47fdf8 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.md @@ -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. diff --git a/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.zh.md b/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.zh.md new file mode 100644 index 0000000000..2b04cfe058 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-27-oxlint-contract-spawn-case-budgets.zh.md @@ -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`,改动后在上限内通过。 diff --git a/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.i18n.yaml b/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.i18n.yaml new file mode 100644 index 0000000000..2297d06fee --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.md b/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.md new file mode 100644 index 0000000000..73b3f9e219 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.md @@ -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. diff --git a/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.zh.md b/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.zh.md new file mode 100644 index 0000000000..b2d6a3c5d7 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-09-27-spreadsheet-selection-by-keyboard-before-reflow.zh.md @@ -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 未改动,所以产品中一次真实的指针按下若在一次延迟渲染内紧跟着侧栏重排,仍可能移动选区。 diff --git a/apps/cli/package.json b/apps/cli/package.json index be98d17216..32a6fa67d1 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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" }, diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 96646ef29a..7019d0bc1e 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -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 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 094d8ba5bf..ae492f22c8 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -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 add `. 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. diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index 2bd6abde1c..e1262f1410 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -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 add ` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent 沙箱之外的受信任可执行代码。 diff --git a/apps/desktop-host/package.json b/apps/desktop-host/package.json index a5b679bfff..f239df4cb8 100644 --- a/apps/desktop-host/package.json +++ b/apps/desktop-host/package.json @@ -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", diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index d417b4dfbb..20819d01f5 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -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 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index bb2295986b..7d61a39a0d 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -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. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 40683dfd85..12a7d53fea 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -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 菜单。 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index c46368a2ac..8c8211d55a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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", diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 53bc447011..fcc1f545a2 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -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 diff --git a/apps/desktop/src/preload-platform.ts b/apps/desktop/src/preload-platform.ts index 0ce5c82ef8..1e197b0c76 100644 --- a/apps/desktop/src/preload-platform.ts +++ b/apps/desktop/src/preload-platform.ts @@ -17,12 +17,13 @@ export function markDocumentPlatform(): void { } /** - * Mirrors the window's macOS fullscreen state onto `` so - * CSS drops the traffic-light clearance while the lights are hidden. The main + * Mirrors the window's macOS and Windows fullscreen state onto `` 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 diff --git a/apps/desktop/tests/acl-skill.built.e2e.ts b/apps/desktop/tests/acl-skill.built.e2e.ts new file mode 100644 index 0000000000..b0d84d3a41 --- /dev/null +++ b/apps/desktop/tests/acl-skill.built.e2e.ts @@ -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 +} +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 }) + } +}) diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index 1b54c9fa46..a6d6e97843 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -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 diff --git a/apps/desktop/tests/preload-platform.client.spec.ts b/apps/desktop/tests/preload-platform.client.spec.ts index fbf6902db0..5b876ab343 100644 --- a/apps/desktop/tests/preload-platform.client.spec.ts +++ b/apps/desktop/tests/preload-platform.client.spec.ts @@ -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 diff --git a/apps/web/package.json b/apps/web/package.json index 87654ff312..420c46b93e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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:*", diff --git a/apps/web/tests/caption-overlays.e2e.ts b/apps/web/tests/caption-overlays.e2e.ts new file mode 100644 index 0000000000..9a28468a2f --- /dev/null +++ b/apps/web/tests/caption-overlays.e2e.ts @@ -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() } +}) diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index fa20d41683..ecb3aba9e5 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -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) diff --git a/apps/web/tests/expected/desktop-locale/automatic.expected.md b/apps/web/tests/expected/desktop-locale/automatic.expected.md index 5eb07e6be3..a243bc300c 100644 --- a/apps/web/tests/expected/desktop-locale/automatic.expected.md +++ b/apps/web/tests/expected/desktop-locale/automatic.expected.md @@ -29,4 +29,6 @@ - button "编辑快捷键" - text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为 - button "排队发送" + - text: 在使用官方模型 API 时上传 Session Log 帮助改进 DeepSeek 模型与产品 + - switch "在使用官方模型 API 时上传 Session Log" - text: 当前版本:{{version}} diff --git a/apps/web/tests/expected/desktop-locale/selected.expected.md b/apps/web/tests/expected/desktop-locale/selected.expected.md index 1d68d15d1f..25e93094dd 100644 --- a/apps/web/tests/expected/desktop-locale/selected.expected.md +++ b/apps/web/tests/expected/desktop-locale/selected.expected.md @@ -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}}" diff --git a/apps/web/tests/expected/onboarding-deepseek-config/welcome.expected.md b/apps/web/tests/expected/onboarding-deepseek-config/welcome.expected.md index de65e02fc7..9ddc222acd 100644 --- a/apps/web/tests/expected/onboarding-deepseek-config/welcome.expected.md +++ b/apps/web/tests/expected/onboarding-deepseek-config/welcome.expected.md @@ -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 "继续" diff --git a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md index a31abbd9a6..bebeced11e 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md @@ -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}}" diff --git a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md index a31abbd9a6..bebeced11e 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md @@ -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}}" diff --git a/apps/web/tests/expected/settings-chrome/dialog.expected.md b/apps/web/tests/expected/settings-chrome/dialog.expected.md index 75c367d690..6bbb8835aa 100644 --- a/apps/web/tests/expected/settings-chrome/dialog.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog.expected.md @@ -29,4 +29,6 @@ - button "编辑快捷键" - text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为 - button "排队发送" + - text: 在使用官方模型 API 时上传 Session Log 帮助改进 DeepSeek 模型与产品 + - switch "在使用官方模型 API 时上传 Session Log" - text: 当前版本:{{version}} diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index b40cc2d30b..4f9fba2f43 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/apps/web/tests/lifecycle-chrome.e2e.ts @@ -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 diff --git a/apps/web/tests/onboarding-deepseek-config.e2e.ts b/apps/web/tests/onboarding-deepseek-config.e2e.ts index f3b3b48542..3d403300bd 100644 --- a/apps/web/tests/onboarding-deepseek-config.e2e.ts +++ b/apps/web/tests/onboarding-deepseek-config.e2e.ts @@ -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) { diff --git a/apps/web/tests/present.e2e.ts b/apps/web/tests/present.e2e.ts index 73d7bcd40c..1a3cd9f2ba 100644 --- a/apps/web/tests/present.e2e.ts +++ b/apps/web/tests/present.e2e.ts @@ -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) diff --git a/apps/web/tests/reasoning-preview.e2e.ts b/apps/web/tests/reasoning-preview.e2e.ts index 44300a4ca6..6ae2425e41 100644 --- a/apps/web/tests/reasoning-preview.e2e.ts +++ b/apps/web/tests/reasoning-preview.e2e.ts @@ -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') }) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 974d742353..9459865b13 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -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 { if (patch.id === undefined || !formEntries.has(patch.id) || patch.config === undefined) return patch diff --git a/apps/web/tests/session-log-upload.e2e.ts b/apps/web/tests/session-log-upload.e2e.ts new file mode 100644 index 0000000000..09a3c4444c --- /dev/null +++ b/apps/web/tests/session-log-upload.e2e.ts @@ -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']) +}) diff --git a/apps/web/tests/turn-tail-actions.e2e.ts b/apps/web/tests/turn-tail-actions.e2e.ts index f1b7b3684f..0f3128d92a 100644 --- a/apps/web/tests/turn-tail-actions.e2e.ts +++ b/apps/web/tests/turn-tail-actions.e2e.ts @@ -251,7 +251,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => { element.closest('[data-chat-flow-kind="turn-process"]')?.getBoundingClientRect().bottom) const answerTop = await page.getByText('DONE', { exact: true }).evaluate(element => element.closest('[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) diff --git a/apps/web/tests/workflow-run.e2e.ts b/apps/web/tests/workflow-run.e2e.ts index 42c5b1d8fc..220ecd7274 100644 --- a/apps/web/tests/workflow-run.e2e.ts +++ b/apps/web/tests/workflow-run.e2e.ts @@ -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' diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 2f2bd905ef..f5f0300f04 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -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", diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index abf47a1663..bdc6ab959f 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -6,5 +6,5 @@ en: 41d772192075e133 zh: 5ef3da69b4de1d8a /capability-seams-and-core-services: - en: 00ee1236e43f5538 - zh: aed1f351bf657eea + en: 890a2217279b7c70 + zh: a5c64adca1fb5a40 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 4d6fd35fa2..5cc25f88f2 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -167,6 +167,7 @@ flowchart LR svc_sessionProjectionCache["ctx.sessionProjectionCache
Persisted projection cache"] pkg_skill["skill"] svc_skills["ctx.skills
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. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 2068e7f369..03c62ceeca 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -169,6 +169,7 @@ flowchart LR svc_sessionProjectionCache["ctx.sessionProjectionCache
Persisted projection cache"] pkg_skill["skill"] svc_skills["ctx.skills
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 的事件和服务,而不依赖此包。 | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index ba9634e909..162fff2e57 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -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 /** * 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 } ``` @@ -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) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index a8adba160d..72bab78412 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -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 /** * 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 } ``` @@ -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) | diff --git a/docs/module-graph.md b/docs/module-graph.md index 0da940458a..de08e4f0e6 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -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) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index e023865c6b..5bd4996a34 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -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) | diff --git a/docs/subsystems/skills.i18n.yaml b/docs/subsystems/skills.i18n.yaml index 503112592f..d95e69dd63 100644 --- a/docs/subsystems/skills.i18n.yaml +++ b/docs/subsystems/skills.i18n.yaml @@ -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 diff --git a/docs/subsystems/skills.md b/docs/subsystems/skills.md index 7e45418b8b..e330edd2c5 100644 --- a/docs/subsystems/skills.md +++ b/docs/subsystems/skills.md @@ -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 diff --git a/docs/subsystems/skills.zh.md b/docs/subsystems/skills.zh.md index ead07c09fe..03a6a06f4e 100644 --- a/docs/subsystems/skills.zh.md +++ b/docs/subsystems/skills.zh.md @@ -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)。 ## 提供方注册表 diff --git a/package.json b/package.json index 4317f7dc94..591fbb4a12 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json index 039aeca6e0..6003be4d3a 100644 --- a/packages/acp/acp/package.json +++ b/packages/acp/acp/package.json @@ -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" }, diff --git a/packages/api/account-controller/package.json b/packages/api/account-controller/package.json index 0d6ca4a08f..eeadb6cec0 100644 --- a/packages/api/account-controller/package.json +++ b/packages/api/account-controller/package.json @@ -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" }, diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index ee277b989c..ac42c71b33 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -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" }, diff --git a/packages/api/job-controller/package.json b/packages/api/job-controller/package.json index e9644eb115..8634b97a89 100644 --- a/packages/api/job-controller/package.json +++ b/packages/api/job-controller/package.json @@ -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" }, diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 09b64bebe7..d8ff08519a 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -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" }, diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index 7b1808168d..c4e3422209 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -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" }, diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 140320b162..db335d48e8 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -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" }, diff --git a/packages/api/terminal-controller/package.json b/packages/api/terminal-controller/package.json index 99fb5b4757..d24392cd78 100644 --- a/packages/api/terminal-controller/package.json +++ b/packages/api/terminal-controller/package.json @@ -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" }, diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index df1549e04c..39c4556f1a 100644 --- a/packages/api/workspace-controller/package.json +++ b/packages/api/workspace-controller/package.json @@ -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" }, diff --git a/packages/api/workspace-files/package.json b/packages/api/workspace-files/package.json index b5035d87c7..8da719086e 100644 --- a/packages/api/workspace-files/package.json +++ b/packages/api/workspace-files/package.json @@ -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" }, diff --git a/packages/attachment/attachment-local/package.json b/packages/attachment/attachment-local/package.json index f2a040cf8e..7d217e3427 100644 --- a/packages/attachment/attachment-local/package.json +++ b/packages/attachment/attachment-local/package.json @@ -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" }, diff --git a/packages/attachment/attachment/package.json b/packages/attachment/attachment/package.json index d2e31eed59..e8ec0a6b26 100644 --- a/packages/attachment/attachment/package.json +++ b/packages/attachment/attachment/package.json @@ -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" }, diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index b4113ee163..77c16a8ac3 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -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" }, diff --git a/packages/boot/cmdline/package.json b/packages/boot/cmdline/package.json index cde209a30e..eaa7d4ba5c 100644 --- a/packages/boot/cmdline/package.json +++ b/packages/boot/cmdline/package.json @@ -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" }, diff --git a/packages/boot/config-editor/package.json b/packages/boot/config-editor/package.json index 177b03df26..e69b6d9268 100644 --- a/packages/boot/config-editor/package.json +++ b/packages/boot/config-editor/package.json @@ -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" }, diff --git a/packages/boot/hmr/package.json b/packages/boot/hmr/package.json index 74a1638898..7b3665dddf 100644 --- a/packages/boot/hmr/package.json +++ b/packages/boot/hmr/package.json @@ -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" }, diff --git a/packages/boot/plugin-manager/package.json b/packages/boot/plugin-manager/package.json index e6057f3d6a..a27b83dd61 100644 --- a/packages/boot/plugin-manager/package.json +++ b/packages/boot/plugin-manager/package.json @@ -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" }, diff --git a/packages/browser-use/browser-use/package.json b/packages/browser-use/browser-use/package.json index 21883bff77..3154e8c235 100644 --- a/packages/browser-use/browser-use/package.json +++ b/packages/browser-use/browser-use/package.json @@ -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" }, diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json index 08246cecad..cec7bb3c69 100644 --- a/packages/bundle/acp-app/package.json +++ b/packages/bundle/acp-app/package.json @@ -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" }, diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 51b37cf282..294db07e8e 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -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" }, diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index 2ead4bf6f8..afc5c5c531 100644 --- a/packages/bundle/headless/package.json +++ b/packages/bundle/headless/package.json @@ -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" }, diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index 9e564b7637..15a38034e7 100644 --- a/packages/bundle/sdk-app/package.json +++ b/packages/bundle/sdk-app/package.json @@ -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" }, diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index e5bb9b6e2c..767d2dd38b 100644 --- a/packages/bundle/sdk-minimal/package.json +++ b/packages/bundle/sdk-minimal/package.json @@ -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" }, diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 6a70826264..f9c2abfe9f 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -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. diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index accaa7132e..74ecfd9bdc 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -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:~", diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index 8e3015ad7c..0b76133bf7 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/README.i18n.yaml @@ -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 diff --git a/packages/client/README.md b/packages/client/README.md index a7e4228fac..bb89a761f2 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -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 | — | diff --git a/packages/client/README.zh.md b/packages/client/README.zh.md index da9ee4d12b..83f0a7d0f1 100644 --- a/packages/client/README.zh.md +++ b/packages/client/README.zh.md @@ -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) | 贡献侧栏的“插件”面板:安装、启用、停用、重试与组合已安装的包 | — | diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index 44d0bf9d24..8fc268496c 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -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" }, diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index 1d6539bfbd..d70e4c4230 100644 --- a/packages/client/file-upload/package.json +++ b/packages/client/file-upload/package.json @@ -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" }, diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json index 92f9f22d92..51b7230eff 100644 --- a/packages/client/hmr/package.json +++ b/packages/client/hmr/package.json @@ -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" }, diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index df7b06db8a..4be6830903 100644 --- a/packages/client/locale/package.json +++ b/packages/client/locale/package.json @@ -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" }, diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json index 689ebfcc67..8283c8f174 100644 --- a/packages/client/modules/package.json +++ b/packages/client/modules/package.json @@ -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" }, diff --git a/packages/client/product-analytics/package.json b/packages/client/product-analytics/package.json index b3e2fffa7d..9a3266ecf6 100644 --- a/packages/client/product-analytics/package.json +++ b/packages/client/product-analytics/package.json @@ -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" }, diff --git a/packages/client/resources/package.json b/packages/client/resources/package.json index d5febb5eb1..925bf5c348 100644 --- a/packages/client/resources/package.json +++ b/packages/client/resources/package.json @@ -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" }, diff --git a/packages/client/shortcuts/package.json b/packages/client/shortcuts/package.json index 79a13d595a..36671f5d2c 100644 --- a/packages/client/shortcuts/package.json +++ b/packages/client/shortcuts/package.json @@ -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" }, diff --git a/packages/client/store/package.json b/packages/client/store/package.json index f240542739..c77aa6f2af 100644 --- a/packages/client/store/package.json +++ b/packages/client/store/package.json @@ -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" }, diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json index a08b9d75ef..c9b33e94f4 100644 --- a/packages/client/ui-agent-preset/package.json +++ b/packages/client/ui-agent-preset/package.json @@ -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" }, diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json index 3d89009fe3..b1d6dd1624 100644 --- a/packages/client/ui-approval/package.json +++ b/packages/client/ui-approval/package.json @@ -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" }, diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index d092d49e60..577fff583a 100644 --- a/packages/client/ui-attachment/package.json +++ b/packages/client/ui-attachment/package.json @@ -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" }, diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index e5605fcff0..b490ac91e4 100644 --- a/packages/client/ui-brand-official/package.json +++ b/packages/client/ui-brand-official/package.json @@ -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" }, diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index f7077d438f..ace10677b3 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index 297b1b3d69..88c95680e6 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -95,6 +95,8 @@ Live tool deltas share reasoning's frame-batched publication; durable calls and The process group uses a stable `div` layout box, a scroll body, and an uncapped content box that reports growth inside the body. Business styles must adapt spacing within and across groups, including hidden or empty members and the answer-spacing exception. CSS variables do not belong in the Group Definition. +Running group titles, reasoning previews, tool and command rows, the active retry line, and the bottom running status use the shared [TextShimmer](../ui-primitives/README.md#component-catalog). Each row’s text shares one left-to-right highlight, including collapsed group titles; icons and hover chevrons retain their base appearance. Tool and process rows sit 6px apart. An expanded group title has 8px before its content, Assistant responses have 12px from adjacent process rows, and completed-Turn duration/status controls have 16px around them. Hidden and empty rows, including retained empty slot anchors, add no gap. The running “Deep diving” text retains its blue base while its theme-specific highlight sweeps across it. + Scroll-edge fades initialize when `ResizeObserver` reports the open group's layout; opening the group performs no immediate scroll-dimension read in a layout effect. Each group owns local `useDisclosure` state that survives mode changes while its component stays mounted. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index d2582913b3..55d8e30d70 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -95,6 +95,8 @@ Chat 通过 `uiConversation.groups` 注册过程 Group Definition。React 通过 过程组使用稳定的 `div` 布局盒子、滚动正文及不限高的内容盒子,后者报告正文内部的内容增长。业务样式必须适配组内及组边界间距,处理隐藏或空成员以及回答前的间距特例。CSS 变量不属于 Group Definition。 +运行中的分组标题、思考预览、工具与命令行、当前重试行以及底部运行状态使用共享的 [TextShimmer](../ui-primitives/README.zh.md#component-catalog)。每行的文字共用一道从左到右的高光,包括折叠的分组标题;图标和悬停箭头保留底色。工具和过程行之间相隔 6px;展开的分组标题与内容之间相隔 8px;Assistant 回复与相邻过程行之间相隔 12px;每轮完成/耗时控件周围保留 16px。隐藏行与空行(包括保留的空 slot 锚点)不产生间距。“深度求索”运行文字保留蓝色底色,并使用主题专用高光扫过文字。 + 滚动边缘渐隐在 `ResizeObserver` 报告已展开组的布局后初始化;展开组时不会在 layout effect 中立即读取滚动尺寸。 每个组拥有本地 `useDisclosure` 状态,组件保持挂载时,模式切换保留该状态。 diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index bd51186a1b..934289989b 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-chat", "description": "Chat Conversation target, node definitions, renderers, and details surface", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css index 56687cd84e..3fd38dc21a 100644 --- a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css @@ -68,11 +68,11 @@ } .title[aria-expanded="true"] { - padding-bottom: 16px; + padding-bottom: 8px; } .body { - --dsh-chat-flow-gap: 8px; + --dsh-chat-flow-gap: 6px; max-height: min(400px, 50vh); overflow-y: auto; overscroll-behavior-y: auto; @@ -118,10 +118,9 @@ } .content > * { flex-shrink: 0; } .content > :not([hidden]):not(:empty) ~ :not([hidden]):not(:empty) { - margin-top: var(--dsh-chat-flow-gap, 8px); + margin-top: var(--dsh-chat-flow-gap, 6px); } .expandedBody { - --dsh-chat-flow-gap: 16px; max-height: none; overflow: visible; scrollbar-gutter: auto; diff --git a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx index e09a0d9504..443cb0407a 100644 --- a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx @@ -120,7 +120,9 @@ const ProcessGroupHeader = memo(function ProcessGroupHeader({ groupKey, useChatG {open ? : } - {title} + + {title} + ) }) diff --git a/packages/client/ui-chat/src/client/chat/ChatView.module.css b/packages/client/ui-chat/src/client/chat/ChatView.module.css index cc4cc3222d..6513822263 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatView.module.css @@ -65,9 +65,20 @@ /* `hidden="until-found"` retains a zero-height box so browser find can reveal its subtree. Direct Chat Node Seats and auxiliary rows share one flow; hidden and empty Seats do not contribute spacing. */ -.column > :not([hidden]):not(.flowItem:empty) - ~ :not([hidden]):not(.flowItem:empty) { - margin-top: var(--dsh-chat-flow-gap, 16px); +.column > :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))) + ~ :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))) { + margin-top: var(--dsh-chat-flow-gap, 6px); +} + +/* Process rows sit 6px apart; an Assistant response is separated from its + neighbours by 12px. */ +.flowItem[data-chat-group-part="response"] { + --dsh-chat-flow-gap: 12px; +} + +.column > [data-chat-group-part="response"]:not([hidden]):not(:empty) + + :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))):not(.running) { + --dsh-chat-flow-gap: 12px; } /* Settled-flow identity boundary. It is neutral until it becomes the natural @@ -76,15 +87,16 @@ min-width: 0; } -/* A closed process reads as one summary immediately followed by its answer. - Expanded process rows return to the ordinary 16px rhythm. */ -.flowItem[data-turn-process-answer] { - --dsh-chat-flow-gap: 8px; +/* The Turn header has 16px clearance from its input and following content. */ +.flowItem[data-chat-flow-kind="turn-process"], +.flowItem[data-turn-process-answer], +.column > [data-chat-flow-kind="turn-process"] + :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))):not(.running) { + --dsh-chat-flow-gap: 16px; } -/* Empty Seats retain ordered layout positions for Turn lookup without - contributing height or the sibling gap. */ -.flowItem:empty { +/* Empty Seats, including retained empty slot anchors, preserve Turn lookup + without contributing height or the sibling gap. */ +.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)) { height: 0; } @@ -106,6 +118,7 @@ flex-direction: column; align-items: flex-start; --dsh-chat-flow-gap: 12px; + --dsw-alias-label-shimmer: var(--dsw-alias-label-deep-diving-shimmer); color: var(--dsw-alias-label-deep-diving); font-size: calc(var(--dsh-content-font-size, 14px) - 2px); line-height: calc(22px + var(--dsh-content-font-delta, 0px)); diff --git a/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx b/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx index 0b0749881b..db4a1f4082 100644 --- a/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx +++ b/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx @@ -42,9 +42,9 @@ export const GenericCommandCard = memo(function GenericCommandCard({ node, t, ru const toggle = useCallback(() => { setExpanded(value => !value) }, []) const collapsedContent = useMemo(() => ( <> - + - {summary} + {summary} ), [running, state, summary]) diff --git a/packages/client/ui-chat/src/client/chat/MessageItem.module.css b/packages/client/ui-chat/src/client/chat/MessageItem.module.css index abedf6985e..2b7a85a255 100644 --- a/packages/client/ui-chat/src/client/chat/MessageItem.module.css +++ b/packages/client/ui-chat/src/client/chat/MessageItem.module.css @@ -201,6 +201,7 @@ display: inline-flex; align-items: center; width: fit-content; + max-width: 100%; padding: 2px 0; gap: 7px; border-radius: var(--dsw-radius-sm); @@ -215,8 +216,10 @@ } .retrySummary::after { + flex: none; width: 6px; height: 6px; + margin-right: 2px; border-right: 1.5px solid currentcolor; border-bottom: 1.5px solid currentcolor; content: ''; @@ -235,24 +238,8 @@ } .retryText { - color: inherit; -} - -.retryRow[data-active] .retryText { - background: - linear-gradient( - 90deg, - var(--dsw-alias-label-tertiary) 0%, - var(--dsw-alias-label-tertiary) 40%, - var(--dsw-alias-label-secondary) 50%, - var(--dsw-alias-label-tertiary) 60%, - var(--dsw-alias-label-tertiary) 100% - ); - background-position: 100% 50%; - background-size: 200% 100%; - background-clip: text; - color: transparent; - animation: retry-shimmer 1.6s ease-in-out infinite; + min-width: 0; + overflow-wrap: anywhere; } .retryRow[open] .retrySummary::after { @@ -313,24 +300,6 @@ font-weight: 600; } -@keyframes retry-shimmer { - from { - background-position: 100% 50%; - } - - to { - background-position: 0 50%; - } -} - -@media (prefers-reduced-motion: reduce) { - .retryRow[data-active] .retryText { - background: none; - color: inherit; - animation: none; - } -} - .attachmentRow { display: flex; flex-wrap: wrap; diff --git a/packages/client/ui-chat/src/client/chat/MessageItem.tsx b/packages/client/ui-chat/src/client/chat/MessageItem.tsx index d84a819f3c..9005150af0 100644 --- a/packages/client/ui-chat/src/client/chat/MessageItem.tsx +++ b/packages/client/ui-chat/src/client/chat/MessageItem.tsx @@ -2,7 +2,7 @@ import { Fragment, memo, useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' import type { PendingSubmission } from '@deepseek-ai/dsh-api-session-controller/client' import type { MessageImageSource } from '@deepseek-ai/dsh-client-ui-conversation/client' -import { fileExtension, FileTypeIcon, fileSizeText, JsonBlock, projectUserText, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' +import { fileExtension, FileTypeIcon, fileSizeText, JsonBlock, projectUserText, StateDot, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives' import type { ChatNodeOwnerProps, ChatNodeViewProps, ChatViewSlotProps } from '../contract/slots.ts' import type { ModelRetryNode, TurnErrorNode, UserMessageNode } from '../contract/snapshot.ts' import { CompactionItem } from './CompactionItem.tsx' @@ -107,7 +107,9 @@ function ModelRetryItem({ node, active, t }: {
- {t('message.retry.status', { label, retry: node.retry, maximum, seconds })} + + {t('message.retry.status', { label, retry: node.retry, maximum, seconds })} +
diff --git a/packages/client/ui-chat/src/client/chat/ReasoningRow.module.css b/packages/client/ui-chat/src/client/chat/ReasoningRow.module.css index f59c04e404..c63704a7da 100644 --- a/packages/client/ui-chat/src/client/chat/ReasoningRow.module.css +++ b/packages/client/ui-chat/src/client/chat/ReasoningRow.module.css @@ -22,27 +22,6 @@ background: var(--dsw-alias-bg-base); } -.root[data-state='running'] .row::after { - content: ''; - position: absolute; - inset-block: 0; - left: 0; - width: 300px; - background: linear-gradient( - 90deg, - transparent 0%, - color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, - transparent 100% - ); - animation: dsh-reasoning-row-sweep 2.6s ease-out infinite; - pointer-events: none; -} - -@keyframes dsh-reasoning-row-sweep { - 0% { left: -300px; } - 90%, 100% { left: 100%; } -} - .leading { flex-shrink: 0; } @@ -102,9 +81,3 @@ padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px)); min-width: 0; } - -@media (prefers-reduced-motion: reduce) { - .root[data-state='running'] .row::after { - animation: none; - } -} diff --git a/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx b/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx index 74a801d6dd..53a255cd26 100644 --- a/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx +++ b/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx @@ -1,6 +1,6 @@ /** Assistant reasoning disclosure, independent of Tool-call presentation. */ import { memo, useMemo } from 'react' -import { DisclosureRow, IconThinkOutlineRegular, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' +import { DisclosureRow, IconThinkOutlineRegular, MarkdownText, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives' import type { ChatViewSlotProps, UseDisclosure, UsePresentation } from '../contract/slots.ts' import { markdownLabels } from '../markdown-labels.ts' import a11yCss from './accessibility.module.css' @@ -59,9 +59,11 @@ export const ReasoningRow = memo(function ReasoningRow({ text, running, usePrese && (running || policy.settledReasoningPreview)) const collapsedContent = useMemo(() => ( <> - + - {summary} + + {summary} + ), [running, summary]) @@ -87,6 +89,7 @@ export const ReasoningRow = memo(function ReasoningRow({ text, running, usePrese chevronClassName={css.chevron} icon={THINK_ICON} title={t('message.think')} + running={running} open={expanded} expandable expandOnRowClick diff --git a/packages/client/ui-chat/src/client/chat/TurnProcessNodeView.module.css b/packages/client/ui-chat/src/client/chat/TurnProcessNodeView.module.css index dbc29ec5c1..aedb0b6a4f 100644 --- a/packages/client/ui-chat/src/client/chat/TurnProcessNodeView.module.css +++ b/packages/client/ui-chat/src/client/chat/TurnProcessNodeView.module.css @@ -23,10 +23,6 @@ color: var(--dsw-alias-label-primary); } -.root:not([data-open]) { - margin-bottom: 8px; -} - .chevron { flex: none; width: 14px; diff --git a/packages/client/ui-chat/tests/chat-font-axis-styles.client.spec.ts b/packages/client/ui-chat/tests/chat-font-axis-styles.client.spec.ts index d19290a60a..cf192daa49 100644 --- a/packages/client/ui-chat/tests/chat-font-axis-styles.client.spec.ts +++ b/packages/client/ui-chat/tests/chat-font-axis-styles.client.spec.ts @@ -157,11 +157,12 @@ describe('chat flow font-size axis', () => { it('keeps empty outer Seats in flow without adding height or sibling gaps', () => { const css = read('ChatView.module.css') - const empty = declarationsFrom(css, '.flowItem:empty') + const emptySelector = '.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))' + const empty = declarationsFrom(css, emptySelector) expect(empty).toContain('height: 0') expect(empty).not.toContain('display: none') - expect(css).toContain('.column > :not([hidden]):not(.flowItem:empty)') - expect(css).toContain('~ :not([hidden]):not(.flowItem:empty)') + expect(css).toContain(`.column > :not([hidden]):not(${emptySelector})`) + expect(css).toContain(`~ :not([hidden]):not(${emptySelector})`) }) it('the interrupted-turn tag stays fixed like the dense token variants', () => { diff --git a/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx b/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx index 5a875f31cb..14388245a6 100644 --- a/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx +++ b/packages/client/ui-chat/tests/reasoning-row.client.spec.tsx @@ -126,7 +126,7 @@ describe('ReasoningRow', () => { ) expect(view.getByText('运行中')).toBeTruthy() expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false') - expect(view.getByText('Newest reasoning tokens').parentElement?.getAttribute('data-streaming')) + expect(view.getByText('Newest reasoning tokens').closest('[data-streaming]')?.getAttribute('data-streaming')) .toBe('true') view.rerender( @@ -138,7 +138,7 @@ describe('ReasoningRow', () => { renderMessageImages={renderMessageImages} />, ) - expect(view.getByText('Newest reasoning tokens').parentElement + expect(view.getByText('Newest reasoning tokens').closest('[data-streaming]') ?.getAttribute('data-streaming')).toBe('true') expect(view.queryByText('Checking boundaries')).toBeNull() @@ -167,7 +167,7 @@ describe('ReasoningRow', () => { const settledSummary = view.getByText('Inspect the session') expect(view.getByRole('button').getAttribute('aria-expanded')).toBe('false') expect(view.queryByText('运行中')).toBeNull() - expect(settledSummary.parentElement?.hasAttribute('data-streaming')).toBe(false) + expect(settledSummary.closest('[data-streaming]')).toBeNull() }) it('expands from either Think or the reasoning summary', () => { diff --git a/packages/client/ui-chat/tests/running-status.client.spec.tsx b/packages/client/ui-chat/tests/running-status.client.spec.tsx index b206fd26d2..58b648e5c1 100644 --- a/packages/client/ui-chat/tests/running-status.client.spec.tsx +++ b/packages/client/ui-chat/tests/running-status.client.spec.tsx @@ -28,7 +28,7 @@ describe('RunningStatus', () => { expect(vi.getTimerCount()).toBe(0) view.set(1_000) expect(view.content()?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/) - expect(view.container.querySelectorAll('[data-text-shimmer]')).toHaveLength(1) + expect(view.content()?.querySelectorAll('[data-shimmer="true"]')).toHaveLength(1) expect(vi.getTimerCount()).toBe(1) view.set() expect(view.content()?.textContent).toBe('深度求索中...') diff --git a/packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts b/packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts index 1955fd41cd..e2523c86d9 100644 --- a/packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts +++ b/packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts @@ -1,4 +1,4 @@ -/** CSS checks for the completed-turn footer's 20px content spacing. */ +/** CSS checks for the completed-turn footer's content spacing. */ import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' @@ -8,7 +8,7 @@ const read = (name: string): string => describe('completed-turn spacing', () => { it('combines the flow and footer offsets around turn-tail content', () => { - expect(read('ChatView.module.css')).toMatch(/margin-top:\s*var\(--dsh-chat-flow-gap, 16px\)/) + expect(read('ChatView.module.css')).toMatch(/margin-top:\s*var\(--dsh-chat-flow-gap, 6px\)/) const tail = read('TurnTailNodeView.module.css') expect(tail).toMatch(/\.root\s*\{[^}]*gap:\s*16px/s) expect(tail).toMatch(/\.actions\s*\{[^}]*margin-top:\s*4px/s) diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json index 1470bb3b19..6e103c37b2 100644 --- a/packages/client/ui-commands/package.json +++ b/packages/client/ui-commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-commands", "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index c30f69d0b9..347d53070a 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-conversation", "description": "Target-neutral Conversation assembly, shell, composer, queue, and view navigation", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index f0e64e3727..884b8197ac 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-deliverables", "description": "Changed-files card with per-file comparison tabs, delivery cards, and clickable final-response file references for Web", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json index 21165d7968..f6e30299f9 100644 --- a/packages/client/ui-directory-picker-browse/package.json +++ b/packages/client/ui-directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-browse", "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-directory-picker-native/package.json b/packages/client/ui-directory-picker-native/package.json index 05b8248bd2..44c19b2d66 100644 --- a/packages/client/ui-directory-picker-native/package.json +++ b/packages/client/ui-directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-directory-picker-native", "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the local Desktop or Host OS chooser", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-dockkit/README.i18n.yaml b/packages/client/ui-dockkit/README.i18n.yaml index 243bad0c64..c92fb30883 100644 --- a/packages/client/ui-dockkit/README.i18n.yaml +++ b/packages/client/ui-dockkit/README.i18n.yaml @@ -18,8 +18,8 @@ en: ea872ff2c2089037 zh: b3ec3a218eacd5c8 /deepseek-ai-dsh-client-ui-dockkit/embedding-it: - en: 789c879dd83f463a - zh: 37f2dc45f9588810 + en: 47b9852393643b6a + zh: ce5f2ab60eec9564 /deepseek-ai-dsh-client-ui-dockkit/interaction-rules-worth-keeping: en: 7e5c1e9a9f9f244f zh: 7e3a8fcef86bc7dd diff --git a/packages/client/ui-dockkit/README.md b/packages/client/ui-dockkit/README.md index 3c3b77151b..df84a12abf 100644 --- a/packages/client/ui-dockkit/README.md +++ b/packages/client/ui-dockkit/README.md @@ -60,7 +60,7 @@ A tab's `kind` is an opaque string. Seeded tabs are factories (`DockControllerOp `DockLayout`, used by the Sidebar, keeps each tab in a stable DOM cell across selection, pane moves and floating. It accepts one docked pane or two horizontal panes; CSS Grid resolves their widths. `keepMounted(tab)` retains visited bodies and `active` controls Session visibility without reparenting content. -`DockSurface` is the docked area for the separate recursive renderer. Chrome around it — a rail, a collapsed presentation, any history controls — belongs to the embedder, which reads `state.expanded` and decides; the kit ships no undo/redo control of its own. Surface-wide controls the embedder does want on the surface go through the `chrome` prop, which the kit places at the far end of the top-right pane's tab strip (the last child of every row split, the first of every column split), so a surface needs no header row of its own. `FloatLayer` owns its own gestures and positions panels in viewport coordinates, so it may be mounted anywhere, including a portal. +`DockSurface` is the docked area for the separate recursive renderer. Chrome around it — a rail, a collapsed presentation, any history controls — belongs to the embedder, which reads `state.expanded` and decides; the kit ships no undo/redo control of its own. Surface-wide controls the embedder does want on the surface go through the `chrome` prop, which the kit places at the far end of the top-right pane's tab strip (the last child of every row split, the first of every column split), so a surface needs no header row of its own. `FloatLayer` owns its own gestures and positions panels in viewport coordinates, so it may be mounted anywhere, including a portal. Both float renderers keep the header at least 20px below the Windows caption (20px below the viewport top in native fullscreen), including newly opened and restored panels. Dragging and resizing start at the displayed position. ## Interaction rules worth keeping diff --git a/packages/client/ui-dockkit/README.zh.md b/packages/client/ui-dockkit/README.zh.md index fb1692d168..64df0ad36b 100644 --- a/packages/client/ui-dockkit/README.zh.md +++ b/packages/client/ui-dockkit/README.zh.md @@ -60,7 +60,7 @@ tab 的 `kind` 是不透明字符串。种子 tab 是工厂(`DockControllerOpt Sidebar 使用的 `DockLayout` 在选中项变化、跨格移动与浮动期间,把每个 tab 保留在稳定的 DOM 容器中。它接受一个停靠格或左右两个格,由 CSS Grid 计算宽度。`keepMounted(tab)` 保留已访问正文,`active` 控制 Session 可见性,不改变内容的父子关系。 -`DockSurface` 是独立递归渲染器的停靠区。它周围的 chrome——轨道、折叠形态、任何历史控件——属于嵌入方,由嵌入方读取 `state.expanded` 后自行决定;套件不自带撤销/重做控件。嵌入方确实想放到面上的整面控件通过 `chrome` prop 传入,套件把它放在右上格 tab 条的最末端(每个横向分裂的最后一个子节点、每个纵向分裂的第一个子节点),因此停靠面不需要自己的标题行。`FloatLayer` 拥有自己的手势并以视口坐标定位浮窗,因此可以挂在任何位置,包括 portal 里。 +`DockSurface` 是独立递归渲染器的停靠区。它周围的 chrome——轨道、折叠形态、任何历史控件——属于嵌入方,由嵌入方读取 `state.expanded` 后自行决定;套件不自带撤销/重做控件。嵌入方确实想放到面上的整面控件通过 `chrome` prop 传入,套件把它放在右上格 tab 条的最末端(每个横向分裂的最后一个子节点、每个纵向分裂的第一个子节点),因此停靠面不需要自己的标题行。`FloatLayer` 拥有自己的手势并以视口坐标定位浮窗,因此可以挂在任何位置,包括 portal 里。 两种浮窗渲染器都让标题栏与 Windows 顶栏至少间隔 20px(原生全屏时距视口顶部 20px),新建和恢复的浮窗均适用。拖动和调整大小从实际显示位置开始。 ## 值得保留的交互规则 diff --git a/packages/client/ui-dockkit/package.json b/packages/client/ui-dockkit/package.json index 01a081bf70..0f869a1fd2 100644 --- a/packages/client/ui-dockkit/package.json +++ b/packages/client/ui-dockkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-dockkit", "description": "Docking layout kit: split-tree engine with invertible operations, and the React components that render and drive it (zero cordis)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-dockkit/src/components/FloatLayer.tsx b/packages/client/ui-dockkit/src/components/FloatLayer.tsx index 612adf89cd..dfe028993b 100644 --- a/packages/client/ui-dockkit/src/components/FloatLayer.tsx +++ b/packages/client/ui-dockkit/src/components/FloatLayer.tsx @@ -86,7 +86,14 @@ export function useFloatGestures(state: LayoutState, intents: DockIntents) { // The press stops here: the panel's own press-to-focus would record a focus // entry before the drag's, and the release below decides which one it is. event.stopPropagation() - const start: FloatDrag = { mode, originX: event.clientX, originY: event.clientY, rect: floatRect(getPane(state, paneId)) } + const root = document.documentElement + const windows = root.hasAttribute('data-windows-titlebar') + // Desktop publishes the caption height in pixels on the root's inline style. + const caption = windows && !root.hasAttribute('data-fullscreen') + ? Number.parseFloat(root.style.getPropertyValue('--dsh-windows-titlebar-height')) : 0 + const saved = floatRect(getPane(state, paneId)) + const rect = { ...saved, y: Math.max(saved.y, windows ? caption + 20 : 0) } + const start: FloatDrag = { mode, originX: event.clientX, originY: event.clientY, rect } begin(event.currentTarget, event.pointerId, { move: (moved) => { setPreview({ paneId, rect: draggedRect(start, moved.clientX, moved.clientY) }) }, up: (released) => { @@ -164,7 +171,7 @@ export function FloatLayer({ state, intents, labels, renderTab, renderTabTitle, data-dockkit-float-active={state.activePaneId === paneId || undefined} style={{ left: live.x, - top: live.y, + top: `max(var(--dsh-dockkit-float-top, 0px), ${live.y}px)`, width: live.width, height: live.height, zIndex: lifted === undefined ? depth + 1 : state.floats.length + 1, diff --git a/packages/client/ui-dockkit/src/components/TabLayout.tsx b/packages/client/ui-dockkit/src/components/TabLayout.tsx index 4511d64e68..f3a2081870 100644 --- a/packages/client/ui-dockkit/src/components/TabLayout.tsx +++ b/packages/client/ui-dockkit/src/components/TabLayout.tsx @@ -88,7 +88,7 @@ function TabHost({ state, callbacks, intents, tab, pane, column, floats, focusRe data-dockkit-float-active={floating && state.activePaneId === pane.id || undefined} data-dockkit-column={!floating ? column : undefined} style={rect === undefined ? undefined : { - left: rect.x, top: rect.y, width: rect.width, height: rect.height, + left: rect.x, top: `max(var(--dsh-dockkit-float-top, 0px), ${rect.y}px)`, width: rect.width, height: rect.height, }} onPointerDown={() => { if (floating) floats.raise(pane.id) }} onClick={() => { if (!floating && state.activePaneId !== pane.id) callbacks.onFocusPane(pane.id) }}> diff --git a/packages/client/ui-dockkit/src/components/dockkit.module.css b/packages/client/ui-dockkit/src/components/dockkit.module.css index 0c69f3d0be..4896d2f5d1 100644 --- a/packages/client/ui-dockkit/src/components/dockkit.module.css +++ b/packages/client/ui-dockkit/src/components/dockkit.module.css @@ -739,6 +739,14 @@ height: 40%; } +:global(html[data-windows-titlebar]) .float { + --dsh-dockkit-float-top: calc(var(--dsh-windows-titlebar-height) + 20px); +} + +:global(html[data-windows-titlebar][data-fullscreen]) .float { + --dsh-dockkit-float-top: 20px; +} + .float { position: fixed; display: flex; diff --git a/packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts b/packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts index 7bf28fdcbf..f0e1428afc 100644 --- a/packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts +++ b/packages/client/ui-dockkit/tests/app-region-styles.client.spec.ts @@ -25,6 +25,12 @@ const regionRules = [...css.matchAll(/(?[^{}]+)\{(?[^{}]*)\}/g)] /-webkit-app-region:\s*([^;]+);/.exec(body)?.[1]?.trim(), ] as const) +it('keeps Windows floating headers 20px below the caption, including restored positions', () => { + const captionRule = css.split(':global(html[data-windows-titlebar]) .float')[1]?.split('}')[0] + expect(captionRule).toContain('--dsh-dockkit-float-top: calc(var(--dsh-windows-titlebar-height) + 20px);') + expect(css).toMatch(/:global\(html\[data-windows-titlebar\]\[data-fullscreen\]\) \.float\s*\{\s*--dsh-dockkit-float-top: 20px;/) +}) + describe('macOS app-region ownership', () => { it('subtracts the chip run, strip-end chrome, pane body, and divider, and declares no drag itself', () => { // The strip row's drag is the markup mark's job: a sheet that declares it diff --git a/packages/client/ui-dockkit/tests/components.client.spec.tsx b/packages/client/ui-dockkit/tests/components.client.spec.tsx index 45ee2672ca..dccbe129a4 100644 --- a/packages/client/ui-dockkit/tests/components.client.spec.tsx +++ b/packages/client/ui-dockkit/tests/components.client.spec.tsx @@ -1304,6 +1304,34 @@ describe('divider drags', () => { }) describe('FloatLayer', () => { + it.each([ + { fullscreen: false, savedY: 0, displayedY: 60 }, + { fullscreen: true, savedY: 0, displayedY: 20 }, + { fullscreen: false, savedY: 80, displayedY: 80 }, + { fullscreen: true, savedY: 80, displayedY: 80 }, + ])('starts move and resize at $displayedY for saved y=$savedY, fullscreen=$fullscreen without layout reads', ({ fullscreen, savedY, displayedY }) => { + const root = document.documentElement + const attributes = ['data-windows-titlebar', 'data-fullscreen', 'style'].map(name => [name, root.getAttribute(name)] as const) + onTestFinished(() => { + for (const [name, value] of attributes) { + if (value === null) root.removeAttribute(name) + else root.setAttribute(name, value) + } + }) + root.setAttribute('data-windows-titlebar', '') + root.toggleAttribute('data-fullscreen', fullscreen) + root.style.setProperty('--dsh-windows-titlebar-height', '40px') + const { intents, paneId, panel } = floating(undefined, savedY) + const measure = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect') + const computedStyle = vi.spyOn(window, 'getComputedStyle') + drag(panel.querySelector('[data-dockkit-float-grip]')!, [150, displayedY + 10], [170, displayedY + 30]) + expect(intents.moveFloat).toHaveBeenCalledWith(paneId, 120, displayedY + 20) + drag(panel.querySelector('[data-dockkit-float-resize]')!, [400, displayedY + 200], [420, displayedY + 220]) + expect(intents.resizeFloat).toHaveBeenCalledWith(paneId, { x: 100, y: displayedY, width: 320, height: 220 }) + expect(measure).not.toHaveBeenCalled() + expect(computedStyle).not.toHaveBeenCalled() + }) + /** Two floating panels, `lower` under `upper`, with `upper` active. */ function twoFloats(): { intents: ReturnType @@ -1333,7 +1361,7 @@ describe('FloatLayer', () => { } /** One floating panel over a spied intent set. */ - function floating(canCloseTab?: FloatLayerProps['canCloseTab']): { + function floating(canCloseTab?: FloatLayerProps['canCloseTab'], y = 80): { intents: ReturnType paneId: PaneId tabId: TabId @@ -1341,7 +1369,7 @@ describe('FloatLayer', () => { } { const controller = seededController() const tabId = controller.openContent({ contentId: 'dsh-resource://file/session/s/a.txt', title: 'a.txt', kind: 'file' }) - const paneId = controller.floatTab(tabId, { x: 100, y: 80, width: 300, height: 200 }) + const paneId = controller.floatTab(tabId, { x: 100, y, width: 300, height: 200 }) const intents = spyIntents() render( { drag(grip, [150, 90], [180, 110], false) expect(panel.style.left).toBe('130px') - expect(panel.style.top).toBe('100px') + expect(panel.style.top).toBe('max(var(--dsh-dockkit-float-top, 0px), 100px)') // Mid-gesture the panel draws on top, as the move it records will leave it. expect(panel.style.zIndex).toBe('3') fireEvent.pointerUp(window, { pointerId: 7, clientX: 180, clientY: 110 }) diff --git a/packages/client/ui-goal/package.json b/packages/client/ui-goal/package.json index 73fb7e6f30..b95764ef02 100644 --- a/packages/client/ui-goal/package.json +++ b/packages/client/ui-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-goal", "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json index e9747ae8fd..ce5131d38b 100644 --- a/packages/client/ui-input-trigger/package.json +++ b/packages/client/ui-input-trigger/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-input-trigger", "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-jobs/package.json b/packages/client/ui-jobs/package.json index d0e4912c72..8b4605961f 100644 --- a/packages/client/ui-jobs/package.json +++ b/packages/client/ui-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-jobs", "description": "Session-header background-job list with on-demand streaming record panels", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-layout/README.i18n.yaml b/packages/client/ui-layout/README.i18n.yaml index 24a9220917..aab49e22e5 100644 --- a/packages/client/ui-layout/README.i18n.yaml +++ b/packages/client/ui-layout/README.i18n.yaml @@ -15,11 +15,11 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-layout/use-this-package: - en: 7d135dd3dbd7ff96 - zh: 43ac4d9c5a12f741 + en: 66f419ab23119ba6 + zh: ae0bba69aff6eead /deepseek-ai-dsh-client-ui-layout/use-this-package/window-chrome-seat: - en: e55c831b460b40e4 - zh: 8670e583bec8c50b + en: 7eac53b38d0777fe + zh: 7f9422dff671ba50 /deepseek-ai-dsh-client-ui-layout/use-this-package/theme-presentation: en: 1e1d3361dac7d8b0 zh: b6b3d210d65738f0 diff --git a/packages/client/ui-layout/README.md b/packages/client/ui-layout/README.md index e1fab07fff..e32e9261d4 100644 --- a/packages/client/ui-layout/README.md +++ b/packages/client/ui-layout/README.md @@ -31,12 +31,15 @@ The root slot composes the sidebar, main content, and right column. The sidebar Global panels occupy the root-scoped `main` keyed slot; `conversation` is the reserved key for the Conversation. `ctx.layout.selectPanel(id)` selects a registered panel, and `null` selects the Conversation without changing the current Session. No global panel is registered by the shipped composition. + ### Window-chrome seat -On macOS desktop (`html[data-platform='darwin']`, set only by the desktop preload) a collapsed sidebar hides its column entirely instead of keeping the rail, and the frame mounts the single root-scoped `shell.leading` seat at its top-left — beside the hiddenInset traffic lights, over every main panel; ui-sidebar occupies it with the reopen and New Session controls. While the seat is mounted the frame publishes `--dsh-frame-leading-clearance`, the inline band the window chrome occupies measured from the frame's left edge; a main panel whose content reaches the top-left corner pads by it so nothing lands under the lights or the controls. The frame also always publishes `--dsh-frame-top-clearance` (48px) on the root element, the constant step below the window's top strip; entry pages in the main panel (plugin manager and similar, not the conversation) pad their top by it, and overlay primitives (portal menus, bottom-anchored overlays, the settings panel) keep it as their top viewport margin — the root-element home lets overlays portalled to `document.body` read it too. The frame declares no darwin drag of its own: every chrome row marks itself `data-window-drag`, ui-web base.css turns that mark into the one darwin drag rule, and the row's own box is therefore the window's draggable geometry — a row's blank runs drag while its controls stay clickable. The frame's remaining drag rule is the Windows caption row, which belongs to that platform. +On macOS desktop (`html[data-platform='darwin']`, set only by the desktop preload) a collapsed sidebar hides its column entirely instead of keeping the rail, and the frame mounts the single root-scoped `shell.leading` seat at its top-left — beside the hiddenInset traffic lights, over every main panel; ui-sidebar occupies it with the reopen and New Session controls. While the seat is mounted the frame publishes `--dsh-frame-leading-clearance`, the inline band the window chrome occupies measured from the frame's left edge; a main panel whose content reaches the top-left corner pads by it so nothing lands under the lights or the controls. The frame also always publishes `--dsh-frame-top-clearance` (48px) on the root element, the constant step below the window's top strip; entry pages in the main panel (plugin manager and similar, not the conversation) pad their top by it. The frame declares no darwin drag of its own: every chrome row marks itself `data-window-drag`, ui-web base.css turns that mark into the one darwin drag rule, and the row's own box is therefore the window's draggable geometry — a row's blank runs drag while its controls stay clickable. The frame's remaining drag rule is the Windows caption row, which belongs to that platform. Windows Electron's `data-windows-titlebar` marker reserves the caption height above all columns and removes the collapsed sidebar rail. Only the content area's top-left corner has a 16px radius; the other corners and the internal divider remain square. The frame publishes `--dsh-windows-content-radius` and `--dsh-windows-sidebar-width` for ui-sidebar-right's fullscreen corner and sidebar clearance. Ordinary Web documents do not receive the marker; macOS retains its separate layout. +The root publishes `--dsh-frame-top-clearance` for the reserved strip (48px on macOS, the caption height on Windows), and `--dsh-frame-overlay-top` adds 20px for dialogs and menus. Native fullscreen reduces the overlay inset to 20px; dialogs retain their own 24px minimum. `--dsh-frame-chrome-top` excludes the Windows caption from modal mask painting and becomes zero in fullscreen. The full-viewport modal layer still blocks background clicks. Plain browser documents publish none of these values. + ### Theme presentation The presenter consumes resolved theme snapshots and projects them onto the document: `html { color-scheme }` for native UA chrome, `body[data-ds-dark-theme]` from the active color scheme, the theme's alias tokens and `--dsh-content-font-size` as inline variables on body, and one owned `` whose content follows the computed body background. Disposing the presenter removes its metadata node with its other global writes. diff --git a/packages/client/ui-layout/README.zh.md b/packages/client/ui-layout/README.zh.md index 29d0633b7f..83b29e59a9 100644 --- a/packages/client/ui-layout/README.zh.md +++ b/packages/client/ui-layout/README.zh.md @@ -31,12 +31,15 @@ kind: "package-reference" 全局面板占据 root 作用域的 `main` keyed slot;`conversation` 是为会话界面保留的 key。`ctx.layout.selectPanel(id)` 选中已注册面板,`null` 则选中会话界面,但不改变当前会话。默认组合不注册任何全局面板。 + ### 窗口 chrome 座 -在 macOS 桌面(`html[data-platform='darwin']`,仅由桌面 preload 设置)下,收起的侧边栏整列隐藏而非保留控制栏,框架在左上角挂载 root 作用域的单一 `shell.leading` 座——位于 hiddenInset 红绿灯旁,覆盖所有主面板;ui-sidebar 以重新打开与 New Session 控件占据该座。座挂载期间框架发布 `--dsh-frame-leading-clearance`:窗口 chrome 占据的行内带宽,自框架左边缘起量;内容抵达左上角的主面板以它做内边距,避免落在红绿灯或控件之下。框架还始终在根元素上发布 `--dsh-frame-top-clearance`(48px):窗口顶带之下的固定下沉量;主面板中的入口型页面(插件管理器等类似页面,非对话)以它做顶部内边距,浮层原语(portal 菜单、底部锚定浮层、设置面板)以它做视口顶部安全边距——放在根元素上让 portal 到 document.body 的浮层也能读到。框架自己不声明任何 darwin drag:每个 chrome 行给自身打上 `data-window-drag`,ui-web base.css 把该标记变成唯一那条 darwin drag 规则,于是行自己的盒子就是窗口的可拖几何——行的空白段可拖,控件保持可点。框架仅剩的那条 drag 规则属于 Windows 标题栏行。 +在 macOS 桌面(`html[data-platform='darwin']`,仅由桌面 preload 设置)下,收起的侧边栏整列隐藏而非保留控制栏,框架在左上角挂载 root 作用域的单一 `shell.leading` 座——位于 hiddenInset 红绿灯旁,覆盖所有主面板;ui-sidebar 以重新打开与 New Session 控件占据该座。座挂载期间框架发布 `--dsh-frame-leading-clearance`:窗口 chrome 占据的行内带宽,自框架左边缘起量;内容抵达左上角的主面板以它做内边距,避免落在红绿灯或控件之下。框架还始终在根元素上发布 `--dsh-frame-top-clearance`(48px):窗口顶带之下的固定下沉量;主面板中的入口型页面(插件管理器等类似页面,非对话)以它做顶部内边距。框架自己不声明任何 darwin drag:每个 chrome 行给自身打上 `data-window-drag`,ui-web base.css 把该标记变成唯一那条 darwin drag 规则,于是行自己的盒子就是窗口的可拖几何——行的空白段可拖,控件保持可点。框架仅剩的那条 drag 规则属于 Windows 标题栏行。 Windows Electron 的 `data-windows-titlebar` 标记在所有列上方预留顶栏高度,并移除收起后的侧栏轨道。内容区仅左上角保留 16px 圆角,其余角和内部交界处保持直角。框架发布 `--dsh-windows-content-radius` 和 `--dsh-windows-sidebar-width`,供 ui-sidebar-right 的全屏圆角及侧栏避让使用。普通 Web 文档不会获得该标记;macOS 保留其独立布局。 +根元素通过 `--dsh-frame-top-clearance` 发布顶栏占用量(macOS 为 48px,Windows 为标题栏高度),通过 `--dsh-frame-overlay-top` 为弹窗和菜单再留出 20px。原生全屏时浮层起点降为 20px;弹窗仍保留自身 24px 的最小边距。`--dsh-frame-chrome-top` 让模态遮罩不绘制 Windows 顶栏区域,全屏时归零。覆盖整个视口的模态层仍阻挡背景点击。普通浏览器文档不发布这些值。 + ### 主题呈现 呈现器消费解析后的主题快照,并投影到 document:`html { color-scheme }` 驱动原生 UA 控件,依据当前配色方案设置 `body[data-ds-dark-theme]`,把主题的别名 token 与 `--dsh-content-font-size` 设为 body 上的内联变量,并持有一个 ``,其内容随计算后的 body 背景色更新。对呈现器执行 dispose(资源释放)时,它会连同其他全局写入一起移除自己的元数据节点。 diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index 7ce271c5c1..95da5aa715 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-layout", "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-layout/src/client/AppFrame.module.css b/packages/client/ui-layout/src/client/AppFrame.module.css index 4f450df3c9..56494a90ee 100644 --- a/packages/client/ui-layout/src/client/AppFrame.module.css +++ b/packages/client/ui-layout/src/client/AppFrame.module.css @@ -87,13 +87,31 @@ toggle, shell.leading controls), the same in both sidebar states. Entry pages in the main panel (plugin manager, settings-like pages — not the conversation, which owns its header) pad their top by it so their leading - content clears the strip, and overlay primitives keep it as their top - viewport margin. Declared on the root element rather than the frame so + content clears the strip, and overlay primitives add their own safe inset. + Declared on the root element rather than the frame so overlays portalled to document.body inherit it too. */ :global(html[data-platform='darwin']) { --dsh-frame-top-clearance: 48px; } +/* In-frame Windows content already clears the caption through .frame padding. */ +:global(html[data-windows-titlebar]) { + --dsh-frame-top-clearance: var(--dsh-windows-titlebar-height); + --dsh-frame-chrome-top: var(--dsh-windows-titlebar-height); +} + +/* Portalled overlays share a viewport inset without adding space to entry pages. */ +:global(html[data-platform='darwin']), +:global(html[data-windows-titlebar]) { + --dsh-frame-overlay-top: calc(var(--dsh-frame-top-clearance) + 20px); +} + +:global(html[data-platform='darwin'][data-fullscreen]), +:global(html[data-windows-titlebar][data-fullscreen]) { + --dsh-frame-overlay-top: 20px; + --dsh-frame-chrome-top: 0px; +} + :global([data-platform='darwin']) .sidebarCol { /* A blue wash over the top of the translucent tint, gone by a third of the column; a faint blue-grey-violet rises through the bottom third; and a diff --git a/packages/client/ui-message-feedback/package.json b/packages/client/ui-message-feedback/package.json index dc76fd2474..6b73febab7 100644 --- a/packages/client/ui-message-feedback/package.json +++ b/packages/client/ui-message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-message-feedback", "description": "The Web feedback surface: per-message Like/Dislike in the assistant-message action strip and the feedback dialog behind both ratings and /feedback, backed by the messageFeedback and sessionFeedback Host Remotes", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index 821c345ab9..5586549360 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-model-selection", "description": "Model selection over the shared model catalog, Session projection, and session.selectModel", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-open-in-app/package.json b/packages/client/ui-open-in-app/package.json index efc4ac1a13..f722f7b47a 100644 --- a/packages/client/ui-open-in-app/package.json +++ b/packages/client/ui-open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-open-in-app", "description": "Web \"Open In...\" controls: the Session-header split button opening the workspace directory in an installed application, and the document preview's default-application controls for one file", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-permission-presets/package.json b/packages/client/ui-permission-presets/package.json index d2683b0c86..7d1acf1ee3 100644 --- a/packages/client/ui-permission-presets/package.json +++ b/packages/client/ui-permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-permission-presets", "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-plan/package.json b/packages/client/ui-plan/package.json index d1d4870ec8..6b237a2e4d 100644 --- a/packages/client/ui-plan/package.json +++ b/packages/client/ui-plan/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plan", "description": "Plan mode controls, persistent transcript plan cards, and sidebar Markdown previews", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-plugin-manager/package.json b/packages/client/ui-plugin-manager/package.json index 7620948e98..b5a444a0e0 100644 --- a/packages/client/ui-plugin-manager/package.json +++ b/packages/client/ui-plugin-manager/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-plugin-manager", "description": "Plugin management for the dsh web client: the sidebar Plugins panel installs, enables, disables, retries, and composes installed plugin packages", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 6863bd624f..5a737bad28 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -15,14 +15,14 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-primitives/use-this-package: - en: ddec1f6f9b018747 - zh: 9555f132daf8901c + en: 7bae72c97d0449f5 + zh: 4589ecc2b216edc5 /deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog: - en: d6d33ad4bfedf06f - zh: e131294a2d40af28 + en: 91d4b79159aab5df + zh: 3a2d8461e836bb66 /deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons: - en: f78bd63b167a7574 - zh: 472eb7791f080ac2 + en: b4cd33720a6cf6b8 + zh: 8db75bf633789c6d /deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output: en: d79e20306593c4bf zh: 5c0f1100e2cd6fcc diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 83a27e6d9f..6fdbce8a2a 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -25,7 +25,7 @@ Use `dsh-client-ui-primitives` to build web-client controls and render agent out ## Use this package -`Toast` uses the theme’s toast background and label colors in both light and dark modes. `Menu` data rows and `MenuItemButton` component rows accept owner-provided effective shortcuts and align their keys at the trailing edge as muted text without a background, including submenus. `ShortcutKeys` defaults to unboxed keys for menu and inline hints. Its `tooltip` variant uses lighter keycaps over the dark bubble and groups plus-separated combinations into one filled block. `Tooltip.shortcutKeys` vertically centers keycaps beside optional localized action text, or shows only keys when the label is empty. The optional `className` supplies owner interaction styles. `Modal` and the settings shell share top-layer Escape and Tab handling through `useModalLayer`, restoring the previous focus on close. `closeTopModal(document)` requests the foreground modal’s current close action; newer menus or unregistered dialogs block dismissal of the modal behind them. Components use `isBehindModal` to avoid moving focus behind a nested dialog. Menus consume their local Escape before modal dismissal. `observeComposition` supplies the same composition-end and legacy IME guard to local modal and recording handlers; callers dispose its document listeners with their interaction lifetime. +`Toast` uses the theme’s toast background and label colors in both light and dark modes. `Menu` data rows and `MenuItemButton` component rows accept owner-provided effective shortcuts and align their keys at the trailing edge as muted text without a background, including submenus. `ShortcutKeys` defaults to unboxed keys for menu and inline hints. Its `tooltip` variant uses lighter keycaps over the dark bubble and groups plus-separated combinations into one filled block. `Tooltip.shortcutKeys` vertically centers keycaps beside optional localized action text, or shows only keys when the label is empty. The optional `className` supplies owner interaction styles. `Modal` uses the frame’s shared overlay inset, with a minimum 24px viewport margin. Its mask leaves the Windows caption unpainted while the full-viewport layer blocks background clicks. `Modal` and the settings shell share top-layer Escape and Tab handling through `useModalLayer`, restoring the previous focus on close. `closeTopModal(document)` requests the foreground modal’s current close action; newer menus or unregistered dialogs block dismissal of the modal behind them. Components use `isBehindModal` to avoid moving focus behind a nested dialog. Menus consume their local Escape before modal dismissal. `observeComposition` supplies the same composition-end and legacy IME guard to local modal and recording handlers; callers dispose its document listeners with their interaction lifetime. Automatic modal entry and modal/menu return focus, including after Escape and application close shortcuts, use `focusWithoutRing(element, options?)` to suppress the outline until Tab or directional navigation resumes normal focus styling. Dialog containers keep their outline-free styling. Existing borders, shadows, and error states remain intact. Mark a dialog's initial control with `data-modal-autofocus` so the modal captures its invoking control before moving focus. Controls mounted with the dialog must not use React `autoFocus`, which runs before that capture. Tab and Shift+Tab from the dialog container enter its first and last focusable controls. @@ -53,6 +53,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `StateDot` | Solid green `done`, amber `warning`, red `error`, and neutral-grey `idle` marks in a 10px slot, plus a tertiary-grey 14px rotating `ongoing` loader whose animations pin to document time zero so every visible loader rotates in phase. `aria-hidden`, so the render site owns the name. `appearance="step"` shows a filled check for completion and a hollow pending circle. | | `ConnectionIndicator` | Inline connection-recovery control across outage, retry, and recovered states. | | `DisclosureRow` | 24px compact disclosure that lays title and content side by side. Memoized with shallow prop comparison; keep callbacks and React-node props stable when their content is unchanged. | +| `TextShimmer` | One left-to-right highlight across a row’s title, separators, summary, and suffix, with a 300ms initial delay, a one-second sweep and a 500ms rest. The mask spans the content width, capped by the visible row and tilts 15° from vertical, with a flat peak and soft edges. The moving decoration stays clipped to the row without enlarging its scrollable area. Base text inherits the caller’s color, including hover changes; the theme supplies the translucent highlight. Place icons and chevrons outside `TextShimmer` so only text and separators receive the highlight. A string child uses the same retained text interface; nested `TextShimmer` instances share the outer animation. Composite children render twice while active and must have no effects or element ids. Mark separators with `data-shimmer-decoration` so their fill receives the highlight. `DisclosureRow` forwards `contentClassName` for the text area and `contentLayoutClassName` for its inner layout. The inert decorative copy is excluded from interaction, selection, and accessibility; text updates retain the animation, and reduced-motion mode keeps the static base. | | `Modal` | Centered dialog over a page mask. A nested dialog can intercept keys with `onKeyDownCapture` before document Escape handlers. The tint and dialog fade in while backdrop blur stays fully applied, honoring reduced motion. Set `backdropBlur={false}` when the caller already blurs the source page. | | `RiskConfirmation` | Sensitive action gated behind an explicit checkbox. | | `Tooltip` | Hover and keyboard-focus text anchored to a cloned child; optional `delayMs` controls hover delay and `focusDelayMs` controls keyboard-focus delay, both defaulting to 0 ms. Optional `portal` rendering escapes clipping containers and ancestor stacking contexts that cap the bubble's z-index. | @@ -82,7 +83,7 @@ Writing your own component in your own package is fine when the need is genuinel ### Controls and icons -The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions ([decision](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md)). Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic code glyph uses angle brackets and a slash at the supplied 1.35px stroke; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader in the same warning color as its label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; `useAnchoredMaxHeight` and portalled `Menu` widen their 12px top viewport margin to the frame's published `--dsh-frame-top-clearance`. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance, caps its height at 420px, and follows content and anchor resizing; Escape or pointer and keyboard activation of the anchor dismisses it. The whole preview fades in and out over 100ms; a closing preview stops receiving pointer input and unmounts after the fade. Re-entering the anchor during the fade restores it. Reduced-motion preferences suppress the transition. Copy labels are required only when copying is enabled. A `Tooltip` nested in the anchor hides that preview while its own hover or focus label is visible; releasing, disabling, or unmounting the nested tooltip restores a still-open preview. `Toast` uses the owner's `holdMs` for both the fade delay and its hold-and-fade lifetime. Rerenders with unchanged `holdMs` do not restart that lifetime; completion uses the latest callback, and fully faded actions cannot receive input. A new component key restarts the banner. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. Portalled `Menu` lists follow their anchor during dragging and CSS transforms, and stop tracking when closed. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus. +The catalog above lists what each export is for; this section covers the behavior that props alone do not show. Product icon names omit artboard sizes and end in `Regular` for the supplied one-pixel artwork or `Medium` for the same geometry at a 1.3px stroke; the `size` prop controls rendered dimensions ([decision](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.md)). Each product, reference, link, and permission glyph intentionally keeps both weight exports even when the current product uses only one, so callers can choose emphasis without adding another API later; fill-only pairs render identically. `IconWarningOutlineRegular`/`Medium` use a circle; `IconWarningTriangleOutlineRegular`/`Medium` use a rounded triangle. `FishLogo` and `BrandWordmark` fill brand slots. `FileTypeIcon` renders the traditional 28px spreadsheet, folder, HTML, image, Markdown, generic, PDF, PPT, video, and Word glyphs, and uses the imported square technology artwork for the established 48 code and configuration categories. The import replaces artwork only: archive-only categories do not extend `CodeFileType`. `classifyFileType` applies exact filename, prefix, suffix, optional project-context, and extension rules in that order; React names win over TypeScript/JavaScript, Angular suffixes win over their base extension, and a Dart file becomes Flutter only when the supplied project files contain a `pubspec.yaml` with `flutter:`. Markdown and SVG remain traditional Markdown and image files. Spreadsheet mappings include CSV, TSV, Excel workbooks and templates, OpenDocument spreadsheets, and Numbers; KEY maps to slides, while RTF/ODT/Pages map to documents. `fileExtension` exposes the same basename and final-dot parsing for adjacent metadata labels. Traditional glyphs use a solid category-colored sheet with a white mark and translucent white corner; the generic code glyph uses angle brackets and a slash at the supplied 1.35px stroke; the generic file uses a grey sheet and darker grey corner. Callers may override the sheet color through `--dsh-file-type-icon-color`. The full-color technology artwork is the deliberate exception and retains its embedded palette. All glyphs are decorative and carry no label. `LinkIconMedium` is the leading glyph for clickable artifact links — globe, folder, code, image, document, or plain paper, and for a `url` link the mark of a well-known site named by its `href` — the developer sites a transcript usually cites (GitHub, GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube, X, Bilibili, Zhihu, Juejin, CSDN) and mainstream search, video, social, shopping, and reference sites (Google, Baidu, DuckDuckGo, TikTok, Netflix, Spotify, Facebook, Instagram, Reddit, Telegram, WhatsApp, WeChat, QQ, Weibo, Taobao, AliExpress, eBay, Quora, V2EX, Apple) — while `classifyLinkPath` folds the shared file types into that existing six-category vocabulary. `ConnectionIndicator` renders a warning-colored disconnected action whose permanent retry glyph marks the retry action beside the owner-supplied outage label, the shared ongoing loader in the same warning color as its label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Clicking either warning state requests an immediate reconnect; no hover interaction changes the copy. The pill fades in on appearance, fades out for 150ms before unmounting, and sizes to its current label. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor; anchored overlays and portalled `Menu` add 20px to the frame's published top clearance, or keep 20px in native fullscreen, while preserving their own minimum margins in ordinary browsers. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. Its `preview` variant uses the anchor or `widthAnchorRef` element’s width minus 48px, centers the panel with 24px side insets, and places it above or below the row within the viewport. The panel keeps the frame’s top clearance, caps its height at 420px, and follows content and anchor resizing; Escape or pointer and keyboard activation of the anchor dismisses it. The whole preview fades in and out over 100ms; a closing preview stops receiving pointer input and unmounts after the fade. Re-entering the anchor during the fade restores it. Reduced-motion preferences suppress the transition. Copy labels are required only when copying is enabled. A `Tooltip` nested in the anchor hides that preview while its own hover or focus label is visible; releasing, disabling, or unmounting the nested tooltip restores a still-open preview. `Toast` uses the owner's `holdMs` for both the fade delay and its hold-and-fade lifetime. Rerenders with unchanged `holdMs` do not restart that lifetime; completion uses the latest callback, and fully faded actions cannot receive input. A new component key restarts the banner. `rankByName` is the `/` menu's shared candidate ranker for the command and skill sources: the query must be a case-insensitive ordered subsequence of the name; prefix hits rank first, then alignment score, then source order. Portalled `Menu` lists follow their anchor during dragging and CSS transforms, and stop tracking when closed. `Menu.autoFocus` focuses its first enabled item, supports Arrow Up/Down and Home/End navigation, and focuses the first button in the anchor on Escape; it is opt-in for action menus. `Tooltip` reads the anchor on hover or keyboard focus and fits its bubble from `ResizeObserver` border-box sizes. The bubble stays hidden until its first fit, slides inside the horizontal viewport margin, and flips above or below only when the opposite side fits. Label-size and viewport changes reuse the anchor coordinates; fitting does not synchronously measure the bubble or trigger a React render. Opt-in `openOnClick` also lets an informational button pin the same bubble for reading and associates it with the anchor description. Another click, Escape, Tab, or an outside pointerdown closes it; ordinary action tooltips still dismiss on click. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 01bc406e60..029f271387 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -25,7 +25,7 @@ kind: "package-library" ## 使用本包 -`Toast` 在浅色和深色模式下均使用主题的系统提示背景与文字颜色。`Menu` 数据条目与 `MenuItemButton` 组件条目接收功能 owner 提供的有效快捷键,并在末端以无背景的浅灰色文字对齐显示,子菜单也采用同一呈现。`ShortcutKeys` 默认以无方块的按键文字显示菜单和行内提示。`tooltip` 变体在深色气泡上使用稍浅的键帽,以加号连接的组合则共用一个填充色块。`Tooltip.shortcutKeys` 将键帽与可选的本地化操作文本垂直居中排列,标签为空时只显示按键。可选的 `className` 供调用方设置交互状态样式。`Modal` 与设置外壳通过 `useModalLayer` 共用顶层 Esc 和 Tab 处理,关闭时恢复先前焦点。`closeTopModal(document)` 请求前台弹窗当前的关闭操作;上层菜单或未注册的对话框会阻止关闭其后方弹窗。组件通过 `isBehindModal` 避免将焦点移到嵌套弹窗后方。菜单先消费自己的 Esc,再由模态层处理关闭。 `observeComposition` 为局部弹层和录键处理提供相同的 composition-end 与旧版 IME 保护;调用方随交互生命周期释放其 document 监听。 +`Toast` 在浅色和深色模式下均使用主题的系统提示背景与文字颜色。`Menu` 数据条目与 `MenuItemButton` 组件条目接收功能 owner 提供的有效快捷键,并在末端以无背景的浅灰色文字对齐显示,子菜单也采用同一呈现。`ShortcutKeys` 默认以无方块的按键文字显示菜单和行内提示。`tooltip` 变体在深色气泡上使用稍浅的键帽,以加号连接的组合则共用一个填充色块。`Tooltip.shortcutKeys` 将键帽与可选的本地化操作文本垂直居中排列,标签为空时只显示按键。可选的 `className` 供调用方设置交互状态样式。`Modal` 使用框架共享的浮层起点,视口边距至少为 24px。其遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击。`Modal` 与设置外壳通过 `useModalLayer` 共用顶层 Esc 和 Tab 处理,关闭时恢复先前焦点。`closeTopModal(document)` 请求前台弹窗当前的关闭操作;上层菜单或未注册的对话框会阻止关闭其后方弹窗。组件通过 `isBehindModal` 避免将焦点移到嵌套弹窗后方。菜单先消费自己的 Esc,再由模态层处理关闭。 `observeComposition` 为局部弹层和录键处理提供相同的 composition-end 与旧版 IME 保护;调用方随交互生命周期释放其 document 监听。 弹窗自动进入及弹窗、菜单回焦,包括通过 Esc 和应用关闭快捷键触发的回焦,均使用 `focusWithoutRing(element, options?)`,在 Tab 或方向键导航恢复正常焦点样式前抑制外轮廓线。弹窗容器仍不绘制焦点外框。原有边框、阴影和错误状态保持不变。用 `data-modal-autofocus` 标记弹窗的初始控件,让模态层先保存触发控件,再移动焦点。随弹窗挂载的控件不得使用 React `autoFocus`,因为它会在保存触发控件前执行。弹窗容器获得焦点时,Tab 和 Shift+Tab 分别进入第一个和最后一个可聚焦控件。 @@ -53,6 +53,7 @@ kind: "package-library" | `StateDot` | 10px 槽内的绿色 `done`、琥珀色 `warning`、红色 `error`、中性灰色 `idle` 圆点,以及 tertiary 灰色 14px 旋转 `ongoing` loading,其动画固定到文档时间零点,所以所有可见 loading 同相旋转。它是 `aria-hidden` 的,名称由渲染点提供。 `appearance="step"` 以实心勾表示完成、空心圆表示等待。 | | `ConnectionIndicator` | 行内连接恢复控件,覆盖断线、重试与已恢复三种状态。 | | `DisclosureRow` | 24px 紧凑折叠行,标题与内容左右排列。使用浅层 prop 比较进行 memo;内容未变时,保持回调与 React 节点 prop 的引用稳定。 | +| `TextShimmer` | 同一道高光从左到右扫过一行的标题、分隔符、摘要与后缀:首次等待 300ms,扫动一秒,再静止 500ms。遮罩覆盖内容宽度,并限制在可见行宽内,相对垂直方向倾斜 15°,峰值区域平坦,两侧柔和渐隐。移动的装饰层始终裁剪在行内,不会扩大可滚动区域。底色继承调用方的颜色,包括悬停变化;主题提供半透明高光。图标和箭头放在 `TextShimmer` 外,只有文字和分隔符参与扫光。字符串子节点沿用稳定的文字接口;嵌套的 `TextShimmer` 共用外层动画。活动期间组合子节点渲染两次,因此不得带副作用或元素 id。分隔符标记 `data-shimmer-decoration`,让填色参与扫光。`DisclosureRow` 将 `contentClassName` 传给文字区域,将 `contentLayoutClassName` 传给内部布局。惰性装饰副本不参与交互、选区和无障碍访问;文字更新保留动画,减少动态效果模式保留静态底色。 | | `Modal` | 页面遮罩之上的居中对话框。嵌套对话框可通过 `onKeyDownCapture` 在文档级 Escape 处理器之前拦截按键。 色层与弹窗淡入,背景模糊始终完整生效,并遵循减少动态效果偏好。调用方已模糊源页面时设置 `backdropBlur={false}`。 | | `RiskConfirmation` | 以显式复选框把关的敏感操作确认。 | | `Tooltip` | 锚定在克隆子元素上的悬停与键盘聚焦提示;可选的 `delayMs` 控制悬停延迟,`focusDelayMs` 控制键盘聚焦延迟,均默认为 0 毫秒。可通过 `portal` 渲染到外层,避免被容器裁剪,或受祖先层叠上下文限制其 z-index。 | @@ -82,7 +83,7 @@ kind: "package-library" ### 控件与图标 -上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸([决定](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md))。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用代码图形使用尖括号与斜线,保留原图 1.35px 描边;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、与文案使用相同警告色的共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;`useAnchoredMaxHeight` 与 portal 模式的 `Menu` 会把 12px 的视口顶部边距加宽到框架发布的 `--dsh-frame-top-clearance`。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区,高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。锚点中嵌套的 `Tooltip` 在显示悬停或焦点标签时隐藏该预览;嵌套提示释放、禁用或卸载后,仍处于打开状态的预览会恢复。`Toast` 使用调用方的 `holdMs` 同时控制淡出延迟与停留加淡出的总时长。`holdMs` 未变时,父组件重渲染不会重启该生命周期;完成时调用最新回调,完全淡出的操作不能接收输入。新的组件 key 会重新开始横幅周期。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 portal 模式的 `Menu` 列表会在拖动和 CSS 变换期间跟随锚点,并在关闭时停止跟踪。`Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。 +上面的目录说明每个导出的用途;本节讲 props 本身看不出来的行为。产品图标名称不含画板尺寸,以 `Regular` 表示原始 1px 图形,以 `Medium` 表示同一几何的 1.3px 描边;`size` prop 控制渲染尺寸([决定](../../../.agents/notes/implemented/architecture/2026-09-16-size-neutral-product-icon-weights.zh.md))。每个产品、引用、链接与权限图形都会有意保留两种线重导出,即使当前产品只使用其中一种,也让调用方无需再次扩展 API 就能选择强调程度;仅填充的成对图形外观相同。`IconWarningOutlineRegular`/`Medium` 使用圆形;`IconWarningTriangleOutlineRegular`/`Medium` 使用圆角三角形。`FishLogo` 与 `BrandWordmark` 填充品牌 slot。`FileTypeIcon` 渲染传统的 28px spreadsheet、folder、HTML、image、Markdown、generic、PDF、PPT、video 与 Word 图形,并为现有 48 个代码和配置类别使用导入的方形技术图形。该导入只替换图形:资源包中额外的类别不会扩展 `CodeFileType`。`classifyFileType` 按完整文件名、前缀、后缀、可选项目上下文、扩展名的顺序匹配;React 文件名优先于 TypeScript/JavaScript,Angular 后缀优先于基础扩展名,只有传入的项目文件包含带 `flutter:` 的 `pubspec.yaml` 时 Dart 文件才使用 Flutter。Markdown 与 SVG 仍分别使用传统 Markdown 与图片图形。表格映射包括 CSV、TSV、Excel 工作簿与模板、OpenDocument 表格和 Numbers;KEY 映射为幻灯片,RTF/ODT/Pages 映射为文档。`fileExtension` 为相邻元数据 label 暴露同一套 basename 与最终点号解析。传统图形使用实色分类底板、白色标记和半透明白色折角;通用代码图形使用尖括号与斜线,保留原图 1.35px 描边;通用文件使用灰色底板与较深灰色折角。调用方可通过 `--dsh-file-type-icon-color` 覆盖底板颜色。全彩技术图形是明确例外,会保留其内嵌调色板。所有图形都是装饰性的,不自带 label。`LinkIconMedium` 是可点击产物链接的前置图形——地球、文件夹、代码、图片、文档或纸张,`url` 链接的 `href` 指向已知站点时则改用该站点自己的标记——转写内容常引用的开发者站点(GitHub、GitLab、npm、PyPI、Stack Overflow、MDN、Wikipedia、Hacker News、YouTube、X、Bilibili、知乎、掘金、CSDN),以及主流搜索、视频、社交、购物与参考资料站点(Google、百度、DuckDuckGo、TikTok、Netflix、Spotify、Facebook、Instagram、Reddit、Telegram、WhatsApp、微信、QQ、微博、淘宝、速卖通、eBay、Quora、V2EX、Apple)——`classifyLinkPath` 把共享文件类型折叠进原有六类词汇。`ConnectionIndicator` 可渲染警告色的断联操作(常驻重试图形指明重试动作,断联文案由持有方提供)、与文案使用相同警告色的共享 ongoing loading 加一至三个点以独立于 retry 时序的 500ms 节奏推进的连接中状态,或成功色的恢复状态。点击任一警告状态都会请求立即重连;没有任何悬停交互会改变文案。药丸出现时淡入、卸载前淡出 150ms,宽度随当前 label 自适应。它的持有方提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点;锚定浮层与 portal 模式的 `Menu` 在框架发布的顶部占用量上增加 20px,原生全屏时保留 20px,普通浏览器中沿用自身的最小边距。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。其 `preview` 变体使用 anchor 或 `widthAnchorRef` 元素的宽度减去 48px,左右各内缩 24px,并在视口内放置于行的上方或下方。浮层避开框架顶部保留区,高度最多 420px,会跟随内容及 anchor 尺寸变化;Escape 或通过鼠标和键盘激活锚点可将其关闭。整个浮层的淡入和淡出各持续 100ms;关闭中的浮层停止接收指针输入,淡出后卸载。在淡出期间移回锚点可恢复显示。减少动态效果偏好会禁用过渡动画。只有启用复制时才必须提供复制标签。锚点中嵌套的 `Tooltip` 在显示悬停或焦点标签时隐藏该预览;嵌套提示释放、禁用或卸载后,仍处于打开状态的预览会恢复。`Toast` 使用调用方的 `holdMs` 同时控制淡出延迟与停留加淡出的总时长。`holdMs` 未变时,父组件重渲染不会重启该生命周期;完成时调用最新回调,完全淡出的操作不能接收输入。新的组件 key 会重新开始横幅周期。`rankByName` 是 `/` 菜单命令源与 skill(技能)源共享的候选排序器:查询必须是名字的不区分大小写的有序子序列;前缀命中排最前,其次按对齐分数,再按来源顺序。 portal 模式的 `Menu` 列表会在拖动和 CSS 变换期间跟随锚点,并在关闭时停止跟踪。`Menu.autoFocus` 聚焦首个启用项,支持上下方向键与 Home/End 导航,并在 Escape 时聚焦 anchor 内的第一个按钮;操作菜单可显式启用。 `Tooltip` 在悬停或键盘聚焦时读取锚点位置,再根据 `ResizeObserver` 提供的边框盒尺寸调整气泡。首次定位前气泡保持隐藏;横向移入视口留白,仅在另一侧容得下时上下翻转。标签尺寸与视口变化复用锚点坐标;定位不会同步测量气泡,也不会触发 React 渲染。 信息按钮可显式启用 `openOnClick`,点击后保持同一个气泡供阅读,并将内容关联为锚点的无障碍描述;再次点击、Escape、Tab 或外部 pointerdown 会关闭。普通操作按钮的 tooltip 仍在点击时关闭。 diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json index 60f85be721..5acd0163e2 100644 --- a/packages/client/ui-primitives/package.json +++ b/packages/client/ui-primitives/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-primitives", "description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-primitives/src/DisclosureRow.tsx b/packages/client/ui-primitives/src/DisclosureRow.tsx index a4fa7a2124..c429547ecf 100644 --- a/packages/client/ui-primitives/src/DisclosureRow.tsx +++ b/packages/client/ui-primitives/src/DisclosureRow.tsx @@ -11,7 +11,7 @@ export interface DisclosureRowProps { open: boolean expandable: boolean onToggle: () => void - /** Animate the title while its owning operation is running. */ + /** Animate the complete header while its owning operation is running. */ running?: boolean | undefined /** Makes the complete title row the disclosure target. */ expandOnRowClick?: boolean | undefined @@ -23,6 +23,10 @@ export interface DisclosureRowProps { children?: ReactNode className?: string | undefined rowClassName?: string | undefined + /** Sizing class for the header text area, beside the leading icon. */ + contentClassName?: string | undefined + /** Layout class shared by the header text and its decorative copy. */ + contentLayoutClassName?: string | undefined leadingClassName?: string | undefined chevronClassName?: string | undefined titleClassName?: string | undefined @@ -48,6 +52,8 @@ export const DisclosureRow = memo(function DisclosureRow({ children, className, rowClassName, + contentClassName, + contentLayoutClassName, leadingClassName, chevronClassName, titleClassName, @@ -100,8 +106,10 @@ export const DisclosureRow = memo(function DisclosureRow({ {leading} )} - {title} - {(keepContentWhenOpen || !open) && collapsedContent} + + {title} + {(keepContentWhenOpen || !open) && collapsedContent} +
{open && children} diff --git a/packages/client/ui-primitives/src/Menu.module.css b/packages/client/ui-primitives/src/Menu.module.css index f9f00fa8d6..a6d6714783 100644 --- a/packages/client/ui-primitives/src/Menu.module.css +++ b/packages/client/ui-primitives/src/Menu.module.css @@ -58,12 +58,12 @@ /* Viewport fit: the card stops 12px short of the viewport's top/bottom edges * (the portal MARGIN in Menu.tsx; the top side widens to the frame's published - * top clearance, the macOS window strip) and taller content scrolls inside + * overlay inset) and taller content scrolls inside * .viewport, so a pinned .footer stays visible. Menus with submenu rows skip * this class — the overflow clip would crop the side card, so they rely on * staying short. */ .scrollable { - max-height: calc(100vh - 12px - max(12px, var(--dsh-frame-top-clearance, 12px))); + max-height: calc(100vh - 12px - max(12px, var(--dsh-frame-overlay-top, 12px))); } .viewport { diff --git a/packages/client/ui-primitives/src/Modal.module.css b/packages/client/ui-primitives/src/Modal.module.css index 8f7dc22fff..be4b832d23 100644 --- a/packages/client/ui-primitives/src/Modal.module.css +++ b/packages/client/ui-primitives/src/Modal.module.css @@ -1,6 +1,6 @@ /* Full-viewport layer (figma Mask + Dialog 451:18655): mask + centered card. 24px of air on every side; the top/bottom widen to the frame's published - top clearance (the macOS window strip) so a tall card stays clear of it. + overlay inset so a tall card stays clear of it. Cards size against this padding box: consumers cap growth with `max-height: 100%`, never their own viewport calc. */ .root { @@ -11,13 +11,13 @@ display: flex; align-items: center; justify-content: center; - padding: max(24px, var(--dsh-frame-top-clearance, 24px)) 24px; + padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px; } -/* The mask dims the page; dark theme raises its opacity. */ +/* Only paint excludes the native caption; the root still blocks background clicks. */ .mask { position: absolute; - inset: 0; + inset: var(--dsh-frame-chrome-top, 0px) 0 0; backdrop-filter: var(--dsw-mask-blur); } @@ -31,6 +31,7 @@ /* Dialogs use the prominent elevation on the secondary layer. */ .dialog { + box-sizing: border-box; position: relative; z-index: 1; display: flex; diff --git a/packages/client/ui-primitives/src/TextShimmer.module.css b/packages/client/ui-primitives/src/TextShimmer.module.css index bdc2de9a44..5131ecd653 100644 --- a/packages/client/ui-primitives/src/TextShimmer.module.css +++ b/packages/client/ui-primitives/src/TextShimmer.module.css @@ -1,27 +1,88 @@ -.root[data-text-shimmer] { - background-image: linear-gradient( - 90deg, - currentColor calc(50% - var(--dsh-text-shimmer-spread)), - color-mix(in oklab, currentColor 50%, transparent), - currentColor calc(50% + var(--dsh-text-shimmer-spread)) - ); - background-position: 100% center; - background-repeat: no-repeat; - background-size: 250% 100%; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: dsh-text-shimmer 1.5s cubic-bezier(0.33, 0, 0.67, 1) infinite; +.root { + position: relative; + display: inline-grid; + width: max-content; + max-width: 100%; + min-width: 0; + vertical-align: top; + color: inherit; } -@keyframes dsh-text-shimmer { - 66.6667%, 100% { background-position: 0% center; } +.content { + display: flex; + align-items: center; + min-width: 0; +} + +.text { + color: inherit; +} + +.text[data-shimmer-text]::after { + content: attr(data-shimmer-text); +} + +.decoration { + position: absolute; + inset: 0; + /* The moving mask must not enlarge any ancestor's scrollable area. */ + overflow: clip; + pointer-events: none; + user-select: none; +} + +.sweep { + position: absolute; + inset: 0; + overflow: hidden; + color: var(--dsw-alias-label-shimmer); + mask-image: linear-gradient(105deg, transparent 0%, black 40% 60%, transparent 100%); + transform: translateX(-100%); + animation-name: dsh-row-shimmer-sweep; +} + +/* Semantic base colors stay on the real content; the overlay supplies one tint. */ +.sweep * { + color: inherit !important; +} + +.sweep [data-shimmer-decoration] { + background: currentColor; +} + +/* Opposite transforms keep every glyph aligned while their shared mask moves. */ +.highlight { + width: 100%; + height: 100%; + transform: translateX(100%); + animation-name: dsh-row-shimmer-highlight; +} + +.sweep, +.highlight { + animation-duration: 1.5s; + animation-delay: 0.3s; + animation-timing-function: steps(48, end); + animation-iteration-count: infinite; +} + +@keyframes dsh-row-shimmer-sweep { + 0% { transform: translateX(-100%); } + 66.6667%, 100% { transform: translateX(100%); } +} + +@keyframes dsh-row-shimmer-highlight { + 0% { transform: translateX(100%); } + 66.6667%, 100% { transform: translateX(-100%); } } @media (prefers-reduced-motion: reduce) { - .root[data-text-shimmer] { - background-image: none; - -webkit-text-fill-color: currentColor; + .decoration { + display: none; + } + + .sweep, + .highlight { animation: none; } } diff --git a/packages/client/ui-primitives/src/TextShimmer.tsx b/packages/client/ui-primitives/src/TextShimmer.tsx index 8249626435..020073a8ae 100644 --- a/packages/client/ui-primitives/src/TextShimmer.tsx +++ b/packages/client/ui-primitives/src/TextShimmer.tsx @@ -1,23 +1,58 @@ -/** Text-only activity animation with a stable span across lifecycle changes. */ -import { memo, useMemo, type CSSProperties } from 'react' +/** Text activity animation shared by a row and its nested text fragments. */ +import { createContext, memo, useContext, type ReactNode } from 'react' import clsx from 'clsx' import css from './TextShimmer.module.css' +const DecorativeCopy = createContext(undefined) + /** Text and activity supplied by the owning row. */ export interface TextShimmerProps { - children: string - active: boolean + /** Text or presentational children with text in nested TextShimmer instances. No effects or element ids. */ + children: ReactNode + /** Whether to animate; nested instances share the containing row's activity. */ + active?: boolean | undefined className?: string | undefined + /** Layout class applied equally to the base and decorative content. */ + contentClassName?: string | undefined +} + +function TextContent({ children, className }: Pick) { + const decorative = useContext(DecorativeCopy) + const generated = decorative === true && typeof children === 'string' + return ( + + {generated ? null : children} + + ) } /** - * Render text with an optional moving highlight; inactive text keeps the same node. + * Render text with one shared highlight while retaining selectable, accessible content. + * Nested instances inherit the outer animation. Keep icons outside; mark decorative + * separators with data-shimmer-decoration so their background follows the highlight. + * Active children also render in an inert, clipped decoration; supply only presentation. * @param props - localized text, running state, and owner styling. - * @returns the retained text span. + * @returns retained text and its optional decorative highlight. */ -export const TextShimmer = memo(function TextShimmer({ children, active, className }: TextShimmerProps) { - const style = useMemo(() => ({ - '--dsh-text-shimmer-spread': `${children.length * 8}px`, - } as CSSProperties), [children.length]) - return {children} +export const TextShimmer = memo(function TextShimmer({ children, active = false, className, contentClassName }: TextShimmerProps) { + const decorative = useContext(DecorativeCopy) + if (decorative !== undefined) return {children} + const content = typeof children === 'string' ? {children} : children + return ( + + + {content} + + {active && ( + // React 18 forwards inert as an empty string; a boolean is dropped. + + )} + + ) }) diff --git a/packages/client/ui-primitives/src/overlay-top-margin.ts b/packages/client/ui-primitives/src/overlay-top-margin.ts index c522f2d0a4..02ed679280 100644 --- a/packages/client/ui-primitives/src/overlay-top-margin.ts +++ b/packages/client/ui-primitives/src/overlay-top-margin.ts @@ -1,19 +1,13 @@ -/** - * Overlay clearance from the window's top strip. On macOS desktop the frame - * publishes `--dsh-frame-top-clearance` on the root element — the constant - * step below the traffic-light strip, where clicks drag the window instead of - * the overlay. JS-clamped overlays keep at least that much air above them. - * Elsewhere the property is absent and the caller's own margin applies. - */ +/** Shared viewport inset for overlays, derived from the desktop frame's reserved top strip. */ /** * Resolve the top margin an overlay keeps from the viewport edge. * @param min - the overlay's own viewport margin in px, used as the floor. - * @returns the larger of `min` and the frame's published top clearance. + * @returns the larger of `min` and the frame clearance plus 20px; fullscreen keeps only the 20px gap. */ export function overlayTopMargin(min: number): number { - const clearance = Number.parseFloat( - getComputedStyle(document.documentElement).getPropertyValue('--dsh-frame-top-clearance'), - ) - return Number.isNaN(clearance) ? min : Math.max(min, clearance) + const root = document.documentElement + const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue('--dsh-frame-top-clearance')) + if (Number.isNaN(clearance)) return min + return Math.max(min, (root.hasAttribute('data-fullscreen') ? 0 : clearance) + 20) } diff --git a/packages/client/ui-primitives/src/useAnchoredMaxHeight.ts b/packages/client/ui-primitives/src/useAnchoredMaxHeight.ts index d137b8b7c8..27e3b30885 100644 --- a/packages/client/ui-primitives/src/useAnchoredMaxHeight.ts +++ b/packages/client/ui-primitives/src/useAnchoredMaxHeight.ts @@ -10,7 +10,7 @@ import { overlayTopMargin } from './overlay-top-margin.ts' /** * Safe distance kept between the overlay and the viewport top edge (mirrors - * the Menu portal margin); the frame's published top clearance widens it. + * the Menu portal margin); the frame's overlay inset widens it. */ const MARGIN = 12 @@ -20,8 +20,8 @@ const MARGIN = 12 * @param cap - design max-height in px (the clamp never exceeds it). * @param signal - re-measure trigger: pass the overlay's render state so anchor * moves (composer growth) re-fit; resize/scroll re-fit while mounted. - * @param margin - viewport top margin floor in px; the frame's published top - * clearance widens it. Callers under fixed chrome (the conversation header) + * @param margin - viewport top margin floor in px; the frame's overlay + * inset widens it. Callers under fixed chrome (the conversation header) * raise it past their chrome's height. * @returns the max-height to apply inline, in px. */ diff --git a/packages/client/ui-primitives/src/useAnchoredPosition.ts b/packages/client/ui-primitives/src/useAnchoredPosition.ts index 4482db4556..f87e4d0697 100644 --- a/packages/client/ui-primitives/src/useAnchoredPosition.ts +++ b/packages/client/ui-primitives/src/useAnchoredPosition.ts @@ -11,6 +11,7 @@ */ import { useLayoutEffect, useState, type CSSProperties, type RefObject } from 'react' +import { overlayTopMargin } from './overlay-top-margin.ts' /** Inputs for {@link useAnchoredPosition}. */ export interface AnchoredPositionOptions { @@ -26,7 +27,7 @@ export interface AnchoredPositionOptions { align?: 'start' | 'end' /** Distance kept between the anchor edge named by `side` and the panel. */ gap: number - /** Distance kept between the panel and each viewport edge. */ + /** Distance kept between the panel and each viewport edge; the frame's overlay inset widens the top margin. */ margin: number } @@ -55,7 +56,7 @@ export function useAnchoredPosition(options: AnchoredPositionOptions): CSSProper let left = align === 'end' ? rect.right - width : rect.left let top = side === 'top' ? rect.top - gap - height : rect.bottom + gap if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin) - if (height > 0) top = Math.min(Math.max(top, margin), window.innerHeight - height - margin) + if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin) /* v8 ignore stop */ setPosition({ left, top }) } diff --git a/packages/client/ui-primitives/tests/hover-card.client.spec.tsx b/packages/client/ui-primitives/tests/hover-card.client.spec.tsx index 837b5762b0..322cfcb526 100644 --- a/packages/client/ui-primitives/tests/hover-card.client.spec.tsx +++ b/packages/client/ui-primitives/tests/hover-card.client.spec.tsx @@ -220,7 +220,7 @@ describe('HoverCard', () => { it('keeps previews below the desktop frame clearance', () => { const prior = document.documentElement.style.getPropertyValue('--dsh-frame-top-clearance') const priority = document.documentElement.style.getPropertyPriority('--dsh-frame-top-clearance') - document.documentElement.style.setProperty('--dsh-frame-top-clearance', '120px') + document.documentElement.style.setProperty('--dsh-frame-top-clearance', '100px') try { const { wrapper } = mount({ variant: 'preview' }) wrapper.getBoundingClientRect = () => DOMRect.fromRect({ x: 100, y: 600, width: 600, height: 32 }) diff --git a/packages/client/ui-primitives/tests/text-shimmer-styles.client.spec.ts b/packages/client/ui-primitives/tests/text-shimmer-styles.client.spec.ts deleted file mode 100644 index b416a1705f..0000000000 --- a/packages/client/ui-primitives/tests/text-shimmer-styles.client.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** CSS activity and reduced-motion rules that jsdom does not execute. */ -import { readFileSync } from 'node:fs' -import { describe, expect, it } from 'vitest' - -const css = readFileSync(new URL('../src/TextShimmer.module.css', import.meta.url), 'utf8') - -describe('TextShimmer styles', () => { - it('animates active text and disables motion and transparent fill for reduced motion', () => { - expect(css).toMatch(/\.root\[data-text-shimmer\]\s*\{[^}]*animation: dsh-text-shimmer/s) - expect(css).toMatch(/@keyframes dsh-text-shimmer\s*\{[^}]*background-position: 0% center/s) - const reduced = /@media \(prefers-reduced-motion: reduce\)\s*\{\s*\.root\[data-text-shimmer\]\s*\{([^}]+)\}/.exec(css)?.[1] - expect(reduced).toContain('background-image: none;') - expect(reduced).toContain('-webkit-text-fill-color: currentColor;') - expect(reduced).toContain('animation: none;') - }) -}) diff --git a/packages/client/ui-primitives/tests/text-shimmer.client.spec.tsx b/packages/client/ui-primitives/tests/text-shimmer.client.spec.tsx index 5f6efdf805..c8d4af6307 100644 --- a/packages/client/ui-primitives/tests/text-shimmer.client.spec.tsx +++ b/packages/client/ui-primitives/tests/text-shimmer.client.spec.tsx @@ -1,29 +1,51 @@ // @vitest-environment jsdom -import { cleanup, render } from '@testing-library/react' -import { afterEach, describe, expect, it } from 'vitest' +import { cleanup, fireEvent, render } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' import { TextShimmer } from '../src/TextShimmer.tsx' afterEach(cleanup) describe('TextShimmer', () => { - it('retains its text span across activity, text, and owner-class changes', () => { - const view = render(Run) - const span = view.getByText('Run') - expect(span.tagName).toBe('SPAN') - expect(span.dataset.textShimmer).toBe('true') - expect(span.classList.contains('title')).toBe(true) - expect(span.style.getPropertyValue('--dsh-text-shimmer-spread')).toBe('24px') + it('retains a string child as its activity and text change', () => { + const view = render(Reading) + const text = view.getByText('Reading') + expect(view.container.textContent).toBe('Reading') + expect(view.container.querySelectorAll('[data-shimmer="true"]')).toHaveLength(1) - view.rerender(End) - expect(view.getByText('End')).toBe(span) - expect(span.hasAttribute('data-text-shimmer')).toBe(false) - expect(span.classList.contains('title')).toBe(false) - expect(span.style.getPropertyValue('--dsh-text-shimmer-spread')).toBe('24px') - - view.rerender(Running) - expect(view.getByText('Running')).toBe(span) - expect(span.dataset.textShimmer).toBe('true') - expect(span.style.getPropertyValue('--dsh-text-shimmer-spread')).toBe('56px') + view.rerender(Read) + expect(view.getByText('Read')).toBe(text) + expect(view.container.querySelector('[data-shimmer]')).toBeNull() + expect(view.container.querySelector('[inert]')).toBeNull() }) + it('retains text and controls while nested fragments share one inert decoration', () => { + const open = vi.fn() + const row = (active: boolean, title: string) => ( + + {title} + + + ) + const view = render(row(true, 'Read')) + const title = view.getByText('Read') + const button = view.getByRole('button', { name: 'file.ts' }) + const decoration = view.container.querySelector('[inert]')! + expect(decoration.getAttribute('aria-hidden')).toBe('true') + expect(view.container.querySelectorAll('[inert]')).toHaveLength(1) + expect([...decoration.querySelectorAll('[data-shimmer-text]')].map(node => node.getAttribute('data-shimmer-text'))) + .toEqual(['Read', 'file.ts']) + expect(view.container.textContent).toBe('Readfile.ts') + fireEvent.click(button) + expect(open).toHaveBeenCalledOnce() + + view.rerender(row(true, 'Reading')) + expect(view.getByText('Reading')).toBe(title) + expect(view.getByRole('button', { name: 'file.ts' })).toBe(button) + expect(view.container.querySelector('[inert]')).toBe(decoration) + + view.rerender(row(false, 'Read')) + expect(view.getByText('Read')).toBe(title) + expect(view.getByRole('button', { name: 'file.ts' })).toBe(button) + expect(view.container.querySelector('[inert]')).toBeNull() + }) }) diff --git a/packages/client/ui-primitives/tests/use-anchored-max-height.client.spec.tsx b/packages/client/ui-primitives/tests/use-anchored-max-height.client.spec.tsx index c6a243e2b8..3092be9113 100644 --- a/packages/client/ui-primitives/tests/use-anchored-max-height.client.spec.tsx +++ b/packages/client/ui-primitives/tests/use-anchored-max-height.client.spec.tsx @@ -40,7 +40,7 @@ describe('useAnchoredMaxHeight', () => { document.documentElement.style.setProperty('--dsh-frame-top-clearance', '48px') stubBottom(300) const { getByTestId } = render() - expect(getByTestId('probe').dataset.maxHeight).toBe('252') + expect(getByTestId('probe').dataset.maxHeight).toBe('232') }) it('keeps a caller-raised margin over a published smaller clearance', () => { diff --git a/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx b/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx index d173fccc98..a3083af168 100644 --- a/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx +++ b/packages/client/ui-primitives/tests/use-anchored-position.client.spec.tsx @@ -10,7 +10,7 @@ * `ResizeObserver` does not exist. */ import { useRef } from 'react' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest' import { cleanup, render } from '@testing-library/react' import { useAnchoredPosition } from '../src/useAnchoredPosition.ts' @@ -64,6 +64,26 @@ function Host({ open, align }: { open: boolean; align?: 'start' | 'end' }) { } describe('useAnchoredPosition', () => { + it.each([{ fullscreen: false, top: 60 }, { fullscreen: true, top: 20 }])('keeps an anchored panel at $top with fullscreen=$fullscreen', ({ fullscreen, top }) => { + const root = document.documentElement + const previous = root.getAttribute('style') + const priorFullscreen = root.getAttribute('data-fullscreen') + onTestFinished(() => { + if (previous === null) root.removeAttribute('style') + else root.setAttribute('style', previous) + if (priorFullscreen === null) root.removeAttribute('data-fullscreen') + else root.setAttribute('data-fullscreen', priorFullscreen) + vi.restoreAllMocks() + }) + root.style.setProperty('--dsh-frame-top-clearance', '40px') + root.toggleAttribute('data-fullscreen', fullscreen) + vi.spyOn(HTMLButtonElement.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(100, 0, 200, 10)) + vi.spyOn(HTMLDivElement.prototype, 'offsetHeight', 'get').mockReturnValue(400) + vi.stubGlobal('innerHeight', 600) + const { getByTestId } = render() + expect(getByTestId('panel').style.top).toBe(`${top}px`) + }) + it('lines the panel up with the anchor\'s right edge when aligned to the end', () => { // jsdom lays nothing out: the anchor's rect and the panel's width are stated, and the placement read back. const rect = { left: 100, right: 300, top: 10, bottom: 30, width: 200, height: 20, x: 100, y: 10, toJSON: () => ({}) } as DOMRect diff --git a/packages/client/ui-reference/package.json b/packages/client/ui-reference/package.json index 7e9579f71f..3e08b21886 100644 --- a/packages/client/ui-reference/package.json +++ b/packages/client/ui-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-reference", "description": "Unified Web @file and @session reference source", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-renderer/package.json b/packages/client/ui-renderer/package.json index d748ca474e..fbac1f7568 100644 --- a/packages/client/ui-renderer/package.json +++ b/packages/client/ui-renderer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-renderer", "description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-schedule/package.json b/packages/client/ui-schedule/package.json index 665d1bca90..2efcc60f5a 100644 --- a/packages/client/ui-schedule/package.json +++ b/packages/client/ui-schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-schedule", "description": "Host task management page and Session reminder catalog", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "type": "module", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/packages/client/ui-schedule/src/client/TaskMenu.module.css b/packages/client/ui-schedule/src/client/TaskMenu.module.css index 9b554c73f2..91fa09e354 100644 --- a/packages/client/ui-schedule/src/client/TaskMenu.module.css +++ b/packages/client/ui-schedule/src/client/TaskMenu.module.css @@ -54,7 +54,7 @@ /* Viewport fit: the card stops 12px short of the viewport's top/bottom edges * (the hook's margin) and taller content scrolls inside .viewport. */ .scrollable { - max-height: calc(100vh - 12px - max(12px, var(--dsh-frame-top-clearance, 12px))); + max-height: calc(100vh - 12px - max(12px, var(--dsh-frame-overlay-top, 12px))); } .viewport { diff --git a/packages/client/ui-session/package.json b/packages/client/ui-session/package.json index 1960b725be..f453c48ba9 100644 --- a/packages/client/ui-session/package.json +++ b/packages/client/ui-session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-session", "description": "Session Controller adapter for React and session-scoped slots", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-account/package.json b/packages/client/ui-settings-account/package.json index c895148b8a..fa0aa33215 100644 --- a/packages/client/ui-settings-account/package.json +++ b/packages/client/ui-settings-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-account", "description": "Manage DeepSeek login and open Platform billing pages", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-agent-loop/package.json b/packages/client/ui-settings-agent-loop/package.json index efc93778cd..2953bc20bb 100644 --- a/packages/client/ui-settings-agent-loop/package.json +++ b/packages/client/ui-settings-agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-agent-loop", "description": "Settings page of the agent loop on the dsh web client's Plugins page: the parallel tool-call cap of the agent-loop namespace", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 42484f8a4a..1ce0fa39c1 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/README.i18n.yaml @@ -12,11 +12,11 @@ en: cd94b4a8dae32670 zh: 333b8603e77f92d7 /deepseek-ai-dsh-client-ui-settings-general/table-of-contents: - en: aeb9c4bfdb19bc56 - zh: 45a29bea90cff239 + en: 2843d46e95311bce + zh: ec11bdeb5d3ce818 /deepseek-ai-dsh-client-ui-settings-general/use-this-package: - en: 473e1f0843f7a896 - zh: acfbc21efc458bbf + en: 3be9bf992c10bc6b + zh: b270083651e5dc43 /deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section: en: 8b8a7c6287bb303b zh: 19535112531aedd5 diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md index 29a1a7d544..4df26c7df0 100644 --- a/packages/client/ui-settings-general/README.md +++ b/packages/client/ui-settings-general/README.md @@ -22,13 +22,15 @@ Use this package to give the dsh web client a Settings panel, connection-recover ----- -The Settings panel uses a shared 760 × 500 layout, bounded by the viewport. Longer sections scroll inside the content column; the Account entry uses the account icon. The panel portals beside `#root` rather than inside it, so a macOS window drag region a chrome row declares later in document order cannot swallow its controls. +The Settings panel uses a shared 800 × 800 layout, bounded by the viewport. Longer sections scroll inside the content column; the Account entry uses the account icon. The panel portals beside `#root` rather than inside it, so a macOS window drag region a chrome row declares later in document order cannot swallow its controls. ## Use this package Users reach the shell through the sidebar's bottom Settings control; feature plugins contribute their pages and onboarding steps through the slot ledgers this shell projects. In both the expanded sidebar and collapsed rail, the control exposes the localized Settings label as its accessible name. A pale-yellow **Disconnected** action beside Settings indicates browser offline suspension; its permanent retry glyph marks the retry action, which the Chinese outage copy also names (连接异常,刷新重试). Every recovery attempt shows the shared ongoing loader beside **Reconnecting** with one to three dots advancing every 500ms, and an attempt stays visible for at least 800ms so brief retries do not flicker. Selecting either yellow state starts an immediate retry; press feedback stays within the warning palette. Recovery changes the region to pale-green **Connected** for two seconds from the moment the green pill becomes visible. The pill fades in on appearance, fades out over 150ms on removal, and sizes to its current label. Initial startup and uninterrupted healthy operation remain silent. The shell renders the modal panel, the navigation built from `settings.section` entries, and exactly one mounted onboarding step at a time. +Settings uses the [frame’s shared overlay inset](../ui-layout/README.md#window-chrome-seat) and keeps at least 24px of viewport margin. Its mask leaves the Windows caption unpainted; the full-viewport layer still blocks background clicks. + When the section navigation exceeds the panel's available height, the list scrolls independently of the settings content and keeps the Settings title fixed. In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. Checking, verifying, and preparing-to-restart tooltips show only the supplied version; without a version, they retain the state label. Desktop update tooltips preserve the complete version and prerelease suffix without adding v or V; their error summaries match the shell update dialogs. diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md index f43966253f..dcbba9257d 100644 --- a/packages/client/ui-settings-general/README.zh.md +++ b/packages/client/ui-settings-general/README.zh.md @@ -22,13 +22,15 @@ kind: "package-reference" ----- -设置面板采用各页面共享的 760 × 500 布局,并受视口大小约束。较长的页面在内容栏内部滚动;账号入口使用账号图标。面板挂载在 `#root` 之外而不是其内部,这样在文档序上更靠后的 chrome 行所声明的 macOS 窗口拖拽区不会吞掉它的控件。 +设置面板采用各页面共享的 800 × 800 布局,并受视口大小约束。较长的页面在内容栏内部滚动;账号入口使用账号图标。面板挂载在 `#root` 之外而不是其内部,这样在文档序上更靠后的 chrome 行所声明的 macOS 窗口拖拽区不会吞掉它的控件。 ## 使用本包 用户通过侧边栏底部的 Settings 控件进入外壳;功能插件通过本外壳所投影的 slot 账本贡献自己的页面与引导步骤。在展开侧边栏和收起轨道中,该控件都会把本地化的 Settings 文案作为其可访问名称。Settings 右侧浅黄色的**连接异常**操作表示浏览器离线暂停;其常驻重试图形与中文文案「连接异常,刷新重试」都指明重试动作。每次恢复尝试都显示共享 ongoing loading 加**重新连接中**,其后一至三个点每 500ms 前进一次,且每次尝试至少可见 800ms,短暂重试不会闪动。选中任一黄色状态都会立即发起重试;按压反馈留在黄色色阶内。恢复后该区域变为浅绿色的**连接成功**,从绿色药丸可见起驻留 2 秒再消失。药丸出现时淡入、移除时以 150ms 淡出,宽度随当前文案自适应。首次启动与未曾中断的健康连接保持静默。外壳渲染模态面板、由 `settings.section` 条目构建的导航,以及每次只挂载一个的引导步骤。 +设置面板使用[框架共享的浮层起点](../ui-layout/README.zh.md#window-chrome-seat),视口边距至少为 24px。其遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击。 + 当分区导航超出面板可用高度时,列表独立于设置内容滚动,并保持「设置」标题固定。 在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致。 diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json index 43400c8c30..df3de834d4 100644 --- a/packages/client/ui-settings-general/package.json +++ b/packages/client/ui-settings-general/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-general", "description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-general/src/client/SettingsRoot.module.css b/packages/client/ui-settings-general/src/client/SettingsRoot.module.css index 74502b5cb3..0f695de58a 100644 --- a/packages/client/ui-settings-general/src/client/SettingsRoot.module.css +++ b/packages/client/ui-settings-general/src/client/SettingsRoot.module.css @@ -67,9 +67,10 @@ justify-content: center; } +/* The full-viewport overlay blocks background clicks above the painted mask too. */ .mask { position: absolute; - inset: 0; + inset: var(--dsh-frame-chrome-top, 0px) 0 0; background: var(--dsw-alias-bg-mask-1); backdrop-filter: var(--dsw-mask-blur); } @@ -79,15 +80,14 @@ every section, taken from the viewport rather than the content, because a content-sized panel would resize under the pointer on every nav click. The viewport height leaves 24px of air above and below, widened to the - frame's published top clearance (the macOS window strip, where clicks drag - the window) on both sides, so the panel sits centered and clear of the - strip. */ + frame's published overlay inset on both sides, so the panel sits centered + and clear of the strip. */ .panel { position: relative; z-index: 1; display: flex; width: 800px; - height: min(800px, calc(100vh - 2 * max(24px, var(--dsh-frame-top-clearance, 24px)))); + height: min(800px, calc(100vh - 2 * max(24px, var(--dsh-frame-overlay-top, 24px)))); max-width: calc(100vw - 48px); border-radius: var(--dsw-radius-panel); overflow: hidden; diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts index 59209b8169..08e0802b98 100644 --- a/packages/client/ui-settings-general/src/client/index.ts +++ b/packages/client/ui-settings-general/src/client/index.ts @@ -80,7 +80,7 @@ export function apply(ctx: ClientContext): void { setEnabled: enabled => ctx.configForms.developerTools.setEnabled(enabled), }), }, DeveloperToolsRow)) - // Last row: every feature-registered preference row orders below 100. + // Version information follows the core preferences. ctx.slots.inject('settings.general.item', () => ctx.slots.register({ name: 'settings.general.item', id: 'current-version', order: 100, locale: NS, }, CurrentVersionRow)) diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index dbd67087e4..d7af2df2ea 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -9,8 +9,8 @@ en: 0db6fa836e1ff511 zh: 0e2968763e1fb7a5 /deepseek-ai-dsh-client-ui-settings-models/summary: - en: 53301a5d29936f69 - zh: 9ac408a33571dbdd + en: 51252c1e9fa6da0a + zh: 227b54d7898417fa /deepseek-ai-dsh-client-ui-settings-models/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -42,8 +42,8 @@ en: 670b8c066aed9b7a zh: 2bedba9a2e0e3fee /deepseek-ai-dsh-client-ui-settings-models/understand-the-implementation/onboarding-coordinator: - en: 5b18ec5319a96cfa - zh: 0eaa3078e7d87cad + en: 46dea666dffe8813 + zh: 6f56fdc365fa3642 /deepseek-ai-dsh-client-ui-settings-models/further-exploration: en: 20abd8b98b6c20e2 zh: 6dfda7955ce5f33f diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index c974239fed..ff2a5bd48f 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -11,7 +11,7 @@ Desktop product events use the optional [product analytics service](../product-a ## Summary -`dsh-client-ui-settings-models` is the Models settings page of the dsh web client: users configure API keys (stored write-only under the profile's credential reference), edit each provider's model list, and hand-declare custom pi-ai routes, with provider rows and one editor card at a time. The page joins the provider directory, the settings document, and the credential descriptions into one shared snapshot, so a row's state stays consistent across all three. It also walks first-run users through two ordered dialogs — a versioned internal-testing notice and the conditional official-DeepSeek credential step. +`dsh-client-ui-settings-models` is the Models settings page of the dsh web client: users configure API keys (stored write-only under the profile's credential reference), edit each provider's model list, and hand-declare custom pi-ai routes, with provider rows and one editor card at a time. The page joins the provider directory, the settings document, and the credential descriptions into one shared snapshot, so a row's state stays consistent across all three. It also walks first-run users through two ordered dialogs — a versioned preview notice and the conditional official-DeepSeek credential step. ## Table of Contents @@ -85,7 +85,7 @@ Each settings write carries the card's current `revision`, so a concurrent write ### Onboarding coordinator -The notice step owns its exact copy in `src/client/locales.ts` and its acknowledgement version in `src/onboarding-copy.ts`; on loopback it compares and writes `ui-settings-general.welcomeNoticeVersion` through the shared configuration form, and only an explicit Continue records the current version. A non-loopback browser cannot use that Host-only namespace, so acknowledgement is process-local and the notice returns after reload. The DeepSeek step targets `deepseek-official` in `llm-deepseek` and renders the existing `ProviderEditor` in credential-only mode inside the shared onboarding modal; `credentials.set` stays the only secret write, and no provider settings are changed. +The notice step owns its exact copy in `src/client/locales.ts` and its acknowledgement version in `src/onboarding-copy.ts`; on loopback it compares and writes `ui-settings-general.welcomeNoticeVersion` through the shared configuration form, and only an explicit Continue records the current version. Users who acknowledged an earlier version see the current notice again. A non-loopback browser cannot use that Host-only namespace, so acknowledgement is process-local and the notice returns after reload. The DeepSeek step targets `deepseek-official` in `llm-deepseek` and renders the existing `ProviderEditor` in credential-only mode inside the shared onboarding modal; `credentials.set` stays the only secret write, and no provider settings are changed.
diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index 3ac2bb0480..e0f2e017ff 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -11,7 +11,7 @@ kind: "package-reference" ## 概述 -`dsh-client-ui-settings-models` 是 dsh Web 客户端的 Models 设置页面:用户可以配置 API 密钥(以只写方式存入 profile 的凭据引用之下)、编辑每个提供商的模型列表,并手工声明自定义 pi-ai 路由;页面以提供商行展示,一次只展开一张编辑卡片。该页面把提供商目录、设置文档与凭据描述合并为一个共享快照,因此行的状态在三个方面始终一致。它还会带首次运行的用户走两个有序弹窗——版本化内测声明,以及按条件显示的官方 DeepSeek 凭据步骤。 +`dsh-client-ui-settings-models` 是 dsh Web 客户端的 Models 设置页面:用户可以配置 API 密钥(以只写方式存入 profile 的凭据引用之下)、编辑每个提供商的模型列表,并手工声明自定义 pi-ai 路由;页面以提供商行展示,一次只展开一张编辑卡片。该页面把提供商目录、设置文档与凭据描述合并为一个共享快照,因此行的状态在三个方面始终一致。它还会带首次运行的用户走两个有序弹窗——版本化预览版说明,以及按条件显示的官方 DeepSeek 凭据步骤。 ## 目录 @@ -85,7 +85,7 @@ Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepS ### 引导协调器 -声明步骤在 `src/client/locales.ts` 中持有精确文案,并在 `src/onboarding-copy.ts` 中持有确认版本;回环时它通过既有 settings API 比较并写入 `ui-settings-general.welcomeNoticeVersion`,且只有显式点击「继续」才会记录当前版本。非回环浏览器无法使用这个仅限宿主的 namespace,因此确认只保留在进程内,刷新后声明会再次出现。DeepSeek 步骤面向 `llm-deepseek` 中的 `deepseek-official`,在共享引导模态框内以仅凭据模式渲染既有 `ProviderEditor`;`credentials.set` 仍是唯一的机密写入,且不改变任何提供商设置。 +预览版说明在 `src/client/locales.ts` 中持有精确文案,并在 `src/onboarding-copy.ts` 中持有确认版本;回环时它通过既有 settings API 比较并写入 `ui-settings-general.welcomeNoticeVersion`,且只有显式点击「继续」才会记录当前版本。已确认旧版的用户会再次看到当前说明。非回环浏览器无法使用这个仅限宿主的 namespace,因此确认只保留在进程内,刷新后说明会再次出现。DeepSeek 步骤面向 `llm-deepseek` 中的 `deepseek-official`,在共享引导模态框内以仅凭据模式渲染既有 `ProviderEditor`;`credentials.set` 仍是唯一的机密写入,且不改变任何提供商设置。 diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-settings-models/package.json index 169aac59b9..be7b3e00e3 100644 --- a/packages/client/ui-settings-models/package.json +++ b/packages/client/ui-settings-models/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-models", "description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-models/src/client/WelcomeNotice.tsx b/packages/client/ui-settings-models/src/client/WelcomeNotice.tsx index 3442ab508a..51685beb44 100644 --- a/packages/client/ui-settings-models/src/client/WelcomeNotice.tsx +++ b/packages/client/ui-settings-models/src/client/WelcomeNotice.tsx @@ -1,4 +1,4 @@ -/** Product-wide, versioned internal-testing notice. */ +/** Product-wide, versioned preview notice. */ import { useCallback, useEffect, useRef } from 'react' import type { ReactNode } from 'react' diff --git a/packages/client/ui-settings-models/src/client/index.ts b/packages/client/ui-settings-models/src/client/index.ts index c80634a2b6..bb60baabe8 100644 --- a/packages/client/ui-settings-models/src/client/index.ts +++ b/packages/client/ui-settings-models/src/client/index.ts @@ -1,6 +1,6 @@ /** * Models settings and product-onboarding plugin, browser half. It registers - * the Models page plus the ordered internal-testing and official-DeepSeek + * the Models page plus the ordered preview-notice and official-DeepSeek * onboarding dialogs, whose UI shares this package's modal wrapper. The Host * settings and credential contracts stay behind their existing wire APIs. * Export discipline: diff --git a/packages/client/ui-settings-models/src/client/locales.ts b/packages/client/ui-settings-models/src/client/locales.ts index bba0b85288..0ece7ada5e 100644 --- a/packages/client/ui-settings-models/src/client/locales.ts +++ b/packages/client/ui-settings-models/src/client/locales.ts @@ -105,8 +105,8 @@ export const en = { settingsPathUnresolvable: 'unresolvable settings path', create: 'Create provider', creating: 'Creating\u2026', - welcomeTitle: 'Internal Testing Notice', - welcomeBody: "DeepSeek Harness 0.1 remains in testing for Harness developers. Many areas need further improvement, and we welcome feedback from the developer community. DeepSeek Harness's core plugins and foundational APIs will continue to evolve rapidly over the coming months.\n\nWe look forward to exploring the limits of intelligence with developers around the world, building on open-source, open, reusable, and composable infrastructure. We welcome Harness developers everywhere to join the DSH plugin ecosystem.", + welcomeTitle: 'Preview Notice', + welcomeBody: 'DeepSeek Harness 0.2 is still in preview, and many areas need continued improvement and refinement. We welcome feedback and suggestions from all developers and users. The new desktop app now targets a broad range of users, while developer-related advanced features can be enabled in the settings. DeepSeek Harness’s product features and plugin APIs are expected to continue rapid iteration and evolution, and will gradually stabilize over time.\n\nWe look forward to exploring the limits of intelligence together with users and developers around the world, building on open-source, reusable, and composable infrastructure. We welcome everyone to bring their ideas to life with DeepSeek Harness and participate in the community to enrich the plugin ecosystem.', welcomeContinue: 'Continue', welcomeError: 'The acknowledgement could not be saved. Please try again.', onboardingTitle: 'Add an API key to get started', @@ -225,8 +225,8 @@ export const zh: { [Key in keyof typeof en]: string } = { settingsPathUnresolvable: '无法解析设置路径', create: '创建提供商', creating: '创建中\u2026', - welcomeTitle: '内测声明', - welcomeBody: 'DeepSeek Harness 目前的 0.1 版本仍处在面向 Harness 开发者进行测试的阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者的反馈建议。预计 DeepSeek Harness 的核心插件以及基础 API 都会在接下来的一段时间内快速迭代、持续演化。\n\n我们期待与全球开发者一起,在开源、开放、可复用、可组合的基础设施之上,共同探索智能上限。欢迎全球 Harness 开发者加入 DSH 插件生态。', + welcomeTitle: '预览版说明', + welcomeBody: 'DeepSeek Harness 目前的 0.2 版本仍处于预览阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者和用户的反馈建议。现在,新的桌面端面向广泛用户,开发者相关的进阶功能可在配置中开启使用。预计 DeepSeek Harness 的产品功能以及插件 API 都会继续快速迭代、持续演化,并逐渐趋于稳定。\n\n我们期待与全球用户和开发者一起,在开源、可复用、可组合的基础设施之上,共同探索智能上限。欢迎大家用 DeepSeek Harness 将想法变成现实,与社区一起丰富插件生态。', welcomeContinue: '继续', welcomeError: '暂时无法保存确认状态,请重试。', onboardingTitle: '添加一个 API Key 开始使用', diff --git a/packages/client/ui-settings-models/src/onboarding-copy.ts b/packages/client/ui-settings-models/src/onboarding-copy.ts index aad628c1bb..f50c066c46 100644 --- a/packages/client/ui-settings-models/src/onboarding-copy.ts +++ b/packages/client/ui-settings-models/src/onboarding-copy.ts @@ -8,4 +8,4 @@ export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion' * Bump only when the notice changes materially and every user should see it * again. The acknowledgement is compared for exact equality. */ -export const WELCOME_NOTICE_VERSION = '2026-08-13.1' +export const WELCOME_NOTICE_VERSION = '2026-09-28.1' diff --git a/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx b/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx index 17f5f3c922..82794a042c 100644 --- a/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/welcome-notice.client.spec.tsx @@ -106,8 +106,8 @@ function mount( describe('WelcomeNotice', () => { it('uses the exact owner copy in both GUI locales', () => { expect(WELCOME_NOTICE_COPY.en).toEqual({ - title: 'Internal Testing Notice', - body: "DeepSeek Harness 0.1 remains in testing for Harness developers. Many areas need further improvement, and we welcome feedback from the developer community. DeepSeek Harness's core plugins and foundational APIs will continue to evolve rapidly over the coming months.\n\nWe look forward to exploring the limits of intelligence with developers around the world, building on open-source, open, reusable, and composable infrastructure. We welcome Harness developers everywhere to join the DSH plugin ecosystem.", + title: 'Preview Notice', + body: 'DeepSeek Harness 0.2 is still in preview, and many areas need continued improvement and refinement. We welcome feedback and suggestions from all developers and users. The new desktop app now targets a broad range of users, while developer-related advanced features can be enabled in the settings. DeepSeek Harness’s product features and plugin APIs are expected to continue rapid iteration and evolution, and will gradually stabilize over time.\n\nWe look forward to exploring the limits of intelligence together with users and developers around the world, building on open-source, reusable, and composable infrastructure. We welcome everyone to bring their ideas to life with DeepSeek Harness and participate in the community to enrich the plugin ecosystem.', continueLabel: 'Continue', }) expect(en.welcomeBody).toBe(WELCOME_NOTICE_COPY.en.body) @@ -132,8 +132,8 @@ describe('WelcomeNotice', () => { expect(screen.getByRole('dialog')).toBeTruthy() }) - it('completes only after the acknowledgement write commits', async () => { - const h = mount() + it('requires a fresh acknowledgement after the 0.1 notice', async () => { + const h = mount('2026-08-13.1') await screen.findByRole('dialog') fireEvent.click(screen.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel })) await act(async () => { await Promise.resolve() }) diff --git a/packages/client/ui-settings-plugin-inventory/package.json b/packages/client/ui-settings-plugin-inventory/package.json index 3192c146a2..49c7c20146 100644 --- a/packages/client/ui-settings-plugin-inventory/package.json +++ b/packages/client/ui-settings-plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugin-inventory", "description": "Read-only Cordis Loader inventory tab in Web Plugins settings", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json index f12bc572a9..89ec5f7838 100644 --- a/packages/client/ui-settings-plugins/package.json +++ b/packages/client/ui-settings-plugins/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-plugins", "description": "Built-in plugins settings section for the dsh web client: the Settings navigation entry and the tab chrome feature-owned tabs register into", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-session-log/README.i18n.yaml b/packages/client/ui-settings-session-log/README.i18n.yaml new file mode 100644 index 0000000000..a247ab248b --- /dev/null +++ b/packages/client/ui-settings-session-log/README.i18n.yaml @@ -0,0 +1,37 @@ +# Bilingual-pair consistency record for README.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 packages/client/ui-settings-session-log/README.md +/: + en: 71cee35d4ba9ca79 + zh: c2aec2b4d3d7bd54 +/deepseek-ai-dsh-client-ui-settings-session-log: + en: 12fe698ae615f9ea + zh: a6331c5e6a70830b +/deepseek-ai-dsh-client-ui-settings-session-log/summary: + en: 39c3846ca9dfd3e4 + zh: 8a745b54482403f8 +/deepseek-ai-dsh-client-ui-settings-session-log/table-of-contents: + en: d152484eb41ac6b4 + zh: bda879a892f240f0 +/deepseek-ai-dsh-client-ui-settings-session-log/use-this-package: + en: 57f1a5defa6e8d89 + zh: 88525c972f18b6ac +/deepseek-ai-dsh-client-ui-settings-session-log/understand-the-implementation: + en: 573cd565e22a6f6e + zh: d214978ce1c43a86 +/deepseek-ai-dsh-client-ui-settings-session-log/further-exploration: + en: a78c5129dd79131e + zh: 6dddf577871572d6 +/deepseek-ai-dsh-client-ui-settings-session-log/model-experience: + en: dd26d7193f9ca4ce + zh: 954a7085b03beb8a +/deepseek-ai-dsh-client-ui-settings-session-log/model-experience/kv-cache-effect: + en: c8b5628ff5370218 + zh: 5ebb7ff0959a3a69 +/deepseek-ai-dsh-client-ui-settings-session-log/known-limitations-and-deferred-work: + en: 637707c46284b048 + zh: 5d3feddb58cb9b50 +/deepseek-ai-dsh-client-ui-settings-session-log/known-limitations-and-deferred-work/dev-note: + en: 0a1253c6f7b5e8b9 + zh: 9f56528562c7c914 diff --git a/packages/client/ui-settings-session-log/README.md b/packages/client/ui-settings-session-log/README.md new file mode 100644 index 0000000000..e2d59b5e6d --- /dev/null +++ b/packages/client/ui-settings-session-log/README.md @@ -0,0 +1,70 @@ +--- +description: "Control Session-log upload with DeepSeek API requests from General settings." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-settings-session-log + +English | [中文](README.zh.md) + +## Summary + +Use **Upload Session Log when using the official model API** above the version number in **Settings → General** to control Session-log upload with DeepSeek API requests. The switch shows the accepted Host setting and saves each change immediately. It appears only while the Host exposes the upload setting; read-only clients cannot change it. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +The Web bundle mounts this companion. Its switch edits the Host plugin's `enabled` field through the shared configuration form. A save failure keeps the accepted value and shows a toast that survives closing Settings. See [Session-log upload](../../session/session-log-deepseek/README.md#configuration) for request timing, resumed uploads, and the separate OpenTelemetry setting. + +----- + + +## Understand the implementation + +
+Implementation details — click to expand + +The companion contributes a `settings.general.item` row while `session-log-deepseek` is served and a `shell.overlay` toast for mutation outcomes. The shared form owns persistence and reconnect synchronization; the companion owns only pending-write state and localized notices. + +
+ +----- + + +## Further Exploration + +- [Settings forms](../ui-settings/README.md) — accepted Host values and ordered writes. +- [Session-log upload](../../session/session-log-deepseek/README.md) — request contribution and acceptance tracking. +- [Web Client](../../../docs/subsystems/web-client.md) — plugin composition and settings placement. + + +## Model Experience + +None, as this browser preference contributes no model-visible content. + +#### KV Cache effect + +None; the setting controls request metadata outside model input. + +## Known Limitations and Deferred Work + + + +- The row is unavailable when the Host does not expose the `session-log-deepseek` namespace. +- No runtime invariant companion is published: accepted enablement comes directly from the shared configuration form, without an independent copy. + + +### Dev Note + +None. diff --git a/packages/client/ui-settings-session-log/README.zh.md b/packages/client/ui-settings-session-log/README.zh.md new file mode 100644 index 0000000000..ffa6d06b13 --- /dev/null +++ b/packages/client/ui-settings-session-log/README.zh.md @@ -0,0 +1,70 @@ +--- +description: "在通用设置中控制随 DeepSeek API 请求上传会话日志。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-client-ui-settings-session-log + +[English](README.md) | 中文 + +## 概述 + +使用**设置 → 通用**中版本号上方的**在使用官方模型 API 时上传 Session Log**开关控制随 DeepSeek API 请求上传会话日志。开关显示 Host 已接受的设置,并立即保存每次修改。仅当 Host 提供上传设置时显示该项;只读客户端无法修改。 + +## 目录 + +- [使用本包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步探索](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与暂缓事项](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用本包 + +Web bundle 挂载本配套插件。开关通过共享配置表单修改 Host 插件的 `enabled` 字段。保存失败时保留已接受的值,并显示关闭设置后仍可见的提示。请求生效时机、恢复上传和独立的 OpenTelemetry 设置见[会话日志上传](../../session/session-log-deepseek/README.zh.md#configuration)。 + +----- + + +## 理解实现 + +
+实现细节——点击展开 + +Host 提供 `session-log-deepseek` 时,配套插件贡献一个 `settings.general.item` 行,并通过 `shell.overlay` 提示保存结果。共享表单负责持久化和重连同步;配套插件仅管理待完成写入状态和本地化提示。 + +
+ +----- + + +## 进一步探索 + +- [设置表单](../ui-settings/README.zh.md)——Host 已接受的值与有序写入。 +- [会话日志上传](../../session/session-log-deepseek/README.zh.md)——请求字段与接收进度。 +- [Web 客户端](../../../docs/subsystems/web-client.zh.md)——插件组合与设置位置。 + + +## 模型体验 + +无;此浏览器设置不贡献模型可见内容。 + +#### KV Cache 影响 + +无;该设置控制模型输入之外的请求元数据。 + +## 已知限制与暂缓事项 + + + +- Host 不提供 `session-log-deepseek` 命名空间时,该行不可用。 +- 不发布运行时不变量配套插件:已接受的启用状态直接来自共享配置表单,没有独立副本。 + + +### 开发备注 + +无。 diff --git a/packages/client/ui-settings-session-log/package.json b/packages/client/ui-settings-session-log/package.json new file mode 100644 index 0000000000..b3c8338ed2 --- /dev/null +++ b/packages/client/ui-settings-session-log/package.json @@ -0,0 +1,67 @@ +{ + "name": "@deepseek-ai/dsh-client-ui-settings-session-log", + "description": "General settings control for Session-log upload with DeepSeek API requests", + "version": "0.2.0-rc.1", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/client/ui-settings-session-log" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./client": { + "types": "./lib/types/client/index.d.ts", + "default": "./lib/client.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "dsh": { + "client": { + "inject": [ + "@deepseek-ai/dsh-client-locale", + "@deepseek-ai/dsh-client-ui-settings", + "@deepseek-ai/dsh-client-ui-layout" + ], + "platform": "web" + } + }, + "scripts": { + "bundle": "tsdown", + "watch": "tsdown --watch" + }, + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:~" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-api-remotes": "workspace:*", + "@deepseek-ai/dsh-client-locale": "workspace:*", + "@deepseek-ai/dsh-client-store": "workspace:*", + "@deepseek-ai/dsh-client-test-runtime": "workspace:*", + "@deepseek-ai/dsh-client-ui-primitives": "workspace:*", + "@deepseek-ai/dsh-client-ui-renderer": "workspace:*", + "@deepseek-ai/dsh-client-ui-settings": "workspace:*", + "@deepseek-ai/dsh-client-ui-slots": "workspace:*", + "@types/react": "~18.3.1", + "react": "^18.2.0", + "@deepseek-ai/dsh-client-ui-layout": "workspace:*", + "react-dom": "^18.2.0", + "@types/react-dom": "~18.3.0" + }, + "files": [ + "lib/index.js", + "lib/client.js", + "lib/types/**/*.d.ts" + ] +} diff --git a/packages/client/ui-settings-session-log/src/client/UploadRow.module.css b/packages/client/ui-settings-session-log/src/client/UploadRow.module.css new file mode 100644 index 0000000000..b3751d7f0b --- /dev/null +++ b/packages/client/ui-settings-session-log/src/client/UploadRow.module.css @@ -0,0 +1,20 @@ +.row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 16px 0; + border-bottom: 0.5px solid var(--dsw-alias-border-l2); +} + +.title { + font-size: 14px; + line-height: 20px; +} + +.description { + margin-top: 4px; + color: var(--dsw-alias-label-secondary); + font-size: 12px; + line-height: 18px; +} diff --git a/packages/client/ui-settings-session-log/src/client/UploadRow.tsx b/packages/client/ui-settings-session-log/src/client/UploadRow.tsx new file mode 100644 index 0000000000..458752156c --- /dev/null +++ b/packages/client/ui-settings-session-log/src/client/UploadRow.tsx @@ -0,0 +1,50 @@ +/** General settings row and persistent shell notice for API log uploads. */ +import { Switch, Toast } from '@deepseek-ai/dsh-client-ui-primitives' +import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' +import type { ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client' +import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type { UploadMutation, UploadSettings } from './upload-preference.ts' +import css from './UploadRow.module.css' + +/** Accepted settings and mutation callbacks supplied by the plugin. */ +export interface UploadInjected { + hooks: { + upload: ObservableSnapshot> + mutation: ObservableSnapshot + } + setEnabled(enabled: boolean): Promise + dismiss(): void +} + +type Face = InjectFace & PropsLocale<'settings.sessionLog'> + +/** + * Render the accepted API upload state. + * @param props - settings hooks, writer and localized copy. + * @returns the bottom preference row. + */ +export function UploadRow({ useUpload, useMutation, setEnabled, t }: PropsRuntime<'settings.general.item'> & Face) { + const upload = useUpload(value => value) + const busy = useMutation(value => value.busy) + return
+
+
{t('title')}
+
{t('description')}
+
+ { void setEnabled(enabled) }} /> +
+} + +/** + * Keep the save outcome visible after settings closes. + * @param props - mutation hook, dismissal and localized copy. + * @returns the current toast, or nothing. + */ +export function UploadToast({ useMutation, dismiss, t }: PropsRuntime<'shell.overlay'> & Face) { + const state = useMutation(value => value) + if (state.notice === null) return null + return +} diff --git a/packages/client/ui-settings-session-log/src/client/index.ts b/packages/client/ui-settings-session-log/src/client/index.ts new file mode 100644 index 0000000000..05863f3fba --- /dev/null +++ b/packages/client/ui-settings-session-log/src/client/index.ts @@ -0,0 +1,41 @@ +/** General settings companion for the Host Session-log upload configuration. */ +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-client-locale/client' +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' +import type {} from '@deepseek-ai/dsh-client-ui-layout/client' +import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' +import { UploadPreference, type UploadSettings } from './upload-preference.ts' +import { UploadRow, UploadToast, type UploadInjected } from './UploadRow.tsx' +import { en, zh } from './locales.ts' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + /** API Session-log preference copy. */ + 'settings.sessionLog': keyof typeof en + } +} + +/** Services used by the browser companion. */ +export const inject = ['slots', 'locale', 'configForms'] + +/** + * Register the preference while its Host configuration is available. + * @param ctx - browser plugin context. + */ +export function apply(ctx: Context): void { + const locale = 'settings.sessionLog' + const namespace = 'session-log-deepseek' + ctx.effect(() => ctx.locale.register(locale, { en, zh })) + const form = ctx.configForms.get(namespace) + const preference = new UploadPreference(form) + const face = (): UploadInjected => ({ + hooks: { upload: form, mutation: preference.state }, + setEnabled: enabled => preference.setEnabled(enabled), + dismiss: () => { preference.dismiss() }, + }) + ctx.effect(() => ctx.configForms.whileServed([namespace], () => ctx.slots.inject('settings.general.item', () => + ctx.slots.register({ name: 'settings.general.item', id: namespace, order: 90, locale, inject: face }, UploadRow)))) + ctx.slots.inject('shell.overlay', () => ctx.slots.register({ + name: 'shell.overlay', id: 'session-log-upload-toast', locale, inject: face, + }, UploadToast)) +} diff --git a/packages/client/ui-settings-session-log/src/client/locales.ts b/packages/client/ui-settings-session-log/src/client/locales.ts new file mode 100644 index 0000000000..accdd2058a --- /dev/null +++ b/packages/client/ui-settings-session-log/src/client/locales.ts @@ -0,0 +1,15 @@ +/** Copy for the API Session-log upload preference. */ +export const en = { + title: 'Upload Session Log when using the official model API', + description: 'Help improve DeepSeek models and products.', + saved: 'Preference saved', + failed: 'Could not save preference', +} + +/** Chinese preference copy. */ +export const zh: Record = { + title: '在使用官方模型 API 时上传 Session Log', + description: '帮助改进 DeepSeek 模型与产品', + saved: '设置已保存', + failed: '无法保存设置', +} diff --git a/packages/client/ui-settings-session-log/src/client/upload-preference.ts b/packages/client/ui-settings-session-log/src/client/upload-preference.ts new file mode 100644 index 0000000000..044f5402ca --- /dev/null +++ b/packages/client/ui-settings-session-log/src/client/upload-preference.ts @@ -0,0 +1,50 @@ +/** Ordered preference writes and notices that survive the settings panel. */ +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client' + +/** Fields exposed by the Session-log plugin. */ +export interface UploadSettings { + enabled: boolean +} + +/** Mutation state shared by the preference row and shell notice. */ +export interface UploadMutation { + busy: boolean + notice: 'saved' | 'failed' | null + sequence: number +} + +/** Writes the Host setting without publishing an optimistic upload state. */ +export class UploadPreference { + /** Observable mutation outcome. */ + readonly state = createSnapshotStore({ busy: false, notice: null, sequence: 0 }) + + /** @param form - Host-owned configuration form. */ + constructor(private readonly form: ConfigForm) {} + + /** + * Persist enablement; refusal or transport failure leaves the accepted value visible. + * @param enabled - requested upload state. + * @returns completion after the write settles. + */ + async setEnabled(enabled: boolean): Promise { + if (this.state.getSnapshot().busy) return + this.state.update((state) => { state.busy = true; state.notice = null }) + let accepted = false + try { + accepted = await this.form.set('enabled', enabled) + } catch (_error) { + // The shell notice reports the failed write while the form retains accepted state. + } + this.state.update((state) => { + state.busy = false + state.notice = accepted ? 'saved' : 'failed' + state.sequence++ + }) + } + + /** Clear the displayed mutation notice. */ + dismiss(): void { + this.state.update((state) => { state.notice = null }) + } +} diff --git a/packages/client/ui-settings-session-log/src/css-modules.d.ts b/packages/client/ui-settings-session-log/src/css-modules.d.ts new file mode 100644 index 0000000000..8811db1264 --- /dev/null +++ b/packages/client/ui-settings-session-log/src/css-modules.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: Record + export default classes +} diff --git a/packages/client/ui-settings-session-log/src/index.ts b/packages/client/ui-settings-session-log/src/index.ts new file mode 100644 index 0000000000..6910de42d7 --- /dev/null +++ b/packages/client/ui-settings-session-log/src/index.ts @@ -0,0 +1,4 @@ +/** Browser companion for the Session-log upload preference. */ + +/** Mount the browser entry through the Loader. */ +export function apply(): void {} diff --git a/packages/client/ui-settings-session-log/tests/apply.client.spec.ts b/packages/client/ui-settings-session-log/tests/apply.client.spec.ts new file mode 100644 index 0000000000..aee231dffc --- /dev/null +++ b/packages/client/ui-settings-session-log/tests/apply.client.spec.ts @@ -0,0 +1,59 @@ +/** The preference follows namespace availability and plugin disposal. */ +import { Context } from '@deepseek-ai/cordis' +import { expect, it, vi } from 'vitest' +import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client' +import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime' +import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' +import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client' +import { apply, inject } from '../src/client/index.ts' +import { apply as hostApply } from '../src/index.ts' +import type { UploadInjected } from '../src/client/UploadRow.tsx' + +it('adds the last General row only while served and removes all contributions on disposal', async () => { + expect(hostApply).not.toThrow() + const ctx = new Context() + try { + await ctx.plugin(SlotRegistry).await() + ctx.provide('locale', new LocaleRuntime(ctx)) + const describe = vi.fn(async () => ({ ok: true as const, value: { + writable: true, hasDocument: true, + namespaces: [] as { ns: string; schema: object; value: object; applies: 'live'; secrets: never[]; revision: number }[], + } })) + const remote = new TestRemote(ctx, { settings: { describe } }) + await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await() + const slots = ctx.get('slots') as SlotRegistry + slots.register({ name: 'root', children: { + 'settings.general.item': { kind: 'list', scope: 'root' }, + 'shell.overlay': { kind: 'list', scope: 'root' }, + } } as never, () => null) + const fiber = ctx.plugin({ inject, apply }) + await fiber.await() + await ctx.configForms.describe().ensure() + expect(slots.entries('settings.general.item')).toHaveLength(0) + describe.mockResolvedValue({ ok: true, value: { writable: true, hasDocument: true, namespaces: [{ + ns: 'session-log-deepseek', schema: {}, value: { enabled: true }, applies: 'live', secrets: [], revision: 1, + }] } }) + remote.emit('settings/document-updated', ['session-log-deepseek', 1]) + await vi.waitFor(() => { expect(slots.entries('settings.general.item')).toHaveLength(1) }) + expect(slots.entries('settings.general.item')[0]!.options).toMatchObject({ id: 'session-log-deepseek', order: 90 }) + expect(slots.entries('shell.overlay')).toHaveLength(1) + const row = slots.entries('settings.general.item')[0]! + const face = row.inject!() as UploadInjected & Record + const form = ctx.configForms.get('session-log-deepseek') + const set = vi.spyOn(form, 'set').mockResolvedValue(false) + await face.setEnabled(false) + expect(set).toHaveBeenCalledWith('enabled', false) + expect(face.hooks.mutation.getSnapshot().notice).toBe('failed') + const toast = slots.entries('shell.overlay')[0]!.inject!() as UploadInjected & Record + expect(toast.hooks.mutation).toBe(face.hooks.mutation) + toast.dismiss() + expect(face.hooks.mutation.getSnapshot().notice).toBeNull() + describe.mockResolvedValue({ ok: true, value: { writable: true, hasDocument: true, namespaces: [] } }) + remote.emit('settings/document-updated', ['session-log-deepseek', 2]) + await vi.waitFor(() => { expect(slots.entries('settings.general.item')).toHaveLength(0) }) + await fiber.dispose() + expect(slots.entries('shell.overlay')).toHaveLength(0) + } finally { + await ctx.fiber.dispose() + } +}) diff --git a/packages/client/ui-settings-session-log/tests/expected/upload-row.zh.txt b/packages/client/ui-settings-session-log/tests/expected/upload-row.zh.txt new file mode 100644 index 0000000000..dffca17e1d --- /dev/null +++ b/packages/client/ui-settings-session-log/tests/expected/upload-row.zh.txt @@ -0,0 +1 @@ +在使用官方模型 API 时上传 Session Log帮助改进 DeepSeek 模型与产品 diff --git a/packages/client/ui-settings-session-log/tests/upload.client.spec.tsx b/packages/client/ui-settings-session-log/tests/upload.client.spec.tsx new file mode 100644 index 0000000000..955d767892 --- /dev/null +++ b/packages/client/ui-settings-session-log/tests/upload.client.spec.tsx @@ -0,0 +1,79 @@ +// @vitest-environment jsdom +/** The switch follows accepted Host state, including refused and delayed saves. */ +import { afterEach, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime' +import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' +import type { ConfigForm, ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client' +import type { ComponentProps } from 'react' +import { UploadRow, UploadToast } from '../src/client/UploadRow.tsx' +import { UploadPreference, type UploadSettings } from '../src/client/upload-preference.ts' +import { en, zh } from '../src/client/locales.ts' + +afterEach(cleanup) + +function fixture(writable = true) { + const formState = createSnapshotStore>({ + status: 'ready', value: { enabled: true }, base: {}, user: {}, revision: 0, writable, mode: 'host', + }) + const set = vi.fn(async (_field: string, enabled: unknown) => { + formState.update((state) => { state.value = { enabled: enabled === true } }) + return true + }) + const form: ConfigForm = { + getSnapshot: () => formState.getSnapshot(), subscribe: listener => formState.subscribe(listener), + set, unset: vi.fn(), mutate: vi.fn(), + } + const preference = new UploadPreference(form) + const props = { + useUpload: bindSnapshotSelector(formState), useMutation: bindSnapshotSelector(preference.state), + setEnabled: (enabled: boolean) => preference.setEnabled(enabled), dismiss: () => { preference.dismiss() }, + t: (key: keyof typeof en) => en[key], + } as ComponentProps + return { formState, set, preference, props } +} + +it('shows a saved preference and keeps its notice after the row unmounts', async () => { + const { props, set } = fixture() + const row = render() + render() + fireEvent.click(screen.getByRole('switch', { name: en.title })) + await waitFor(() => { expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('false') }) + expect(set).toHaveBeenCalledWith('enabled', false) + row.unmount() + expect(screen.getByText(en.saved)).toBeTruthy() +}) + +it.each(['refused', 'rejected'] as const)('retains accepted state after a %s write', async (failure) => { + const { props, set } = fixture() + if (failure === 'refused') set.mockResolvedValue(false) + else set.mockRejectedValue(new Error('Disconnected')) + render(<>) + fireEvent.click(screen.getByRole('switch')) + await screen.findByText(en.failed) + expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('true') +}) + +it('disables input until a pending write settles', async () => { + const { props, set, preference } = fixture() + const pending = Promise.withResolvers() + set.mockReturnValue(pending.promise) + render() + fireEvent.click(screen.getByRole('switch')) + expect(screen.getByRole('switch').hasAttribute('disabled')).toBe(true) + await preference.setEnabled(true) + expect(set).toHaveBeenCalledTimes(1) + pending.resolve(false) + await waitFor(() => { expect(screen.getByRole('switch').hasAttribute('disabled')).toBe(false) }) +}) + +it('shows the Chinese label and cannot edit read-only settings', async () => { + const { props, set } = fixture(false) + const translated: Record = zh + const { container } = render( translated[key] ?? key} />) + await expect(container.textContent + '\n').toMatchFileSnapshot('./expected/upload-row.zh.txt') + expect(screen.getByRole('switch', { name: zh.title }).hasAttribute('disabled')).toBe(true) + expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('true') + fireEvent.click(screen.getByRole('switch')) + expect(set).not.toHaveBeenCalled() +}) diff --git a/packages/client/ui-settings-session-log/tsconfig.json b/packages/client/ui-settings-session-log/tsconfig.json new file mode 100644 index 0000000000..2649e3e6eb --- /dev/null +++ b/packages/client/ui-settings-session-log/tsconfig.json @@ -0,0 +1,42 @@ +{ + "extends": "../../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../locale/tsconfig.client.json" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../store" + }, + { + "path": "../../test-support/client-runtime" + }, + { + "path": "../ui-primitives" + }, + { + "path": "../ui-renderer" + }, + { + "path": "../ui-layout" + }, + { + "path": "../ui-settings/tsconfig.client.json" + }, + { + "path": "../../api/remotes/tsconfig.client.json" + }, + { + "path": "../ui-slots" + } + ] +} diff --git a/packages/client/ui-settings-session-log/tsdown.config.ts b/packages/client/ui-settings-session-log/tsdown.config.ts new file mode 100644 index 0000000000..701ce5e140 --- /dev/null +++ b/packages/client/ui-settings-session-log/tsdown.config.ts @@ -0,0 +1,3 @@ +import { clientBundle } from '../tsdown.client.ts' + +export default clientBundle('@deepseek-ai/dsh-client-ui-settings-session-log', ['lib/types/index.js']) diff --git a/packages/client/ui-settings-shell/package.json b/packages/client/ui-settings-shell/package.json index c5c9e2993b..fac183346c 100644 --- a/packages/client/ui-settings-shell/package.json +++ b/packages/client/ui-settings-shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-shell", "description": "Settings page of the shell executor on the dsh web client's Plugins page: the command timeout and the per-stream output cap of the shell namespace", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-subagent/package.json b/packages/client/ui-settings-subagent/package.json index ce4053a11c..c8b9b1b7ae 100644 --- a/packages/client/ui-settings-subagent/package.json +++ b/packages/client/ui-settings-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-subagent", "description": "Settings page of Subagent delegation on the dsh web client's Plugins page: recursion depth, parallel capacity, and the models agents may choose for subagents", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings-web-search/package.json b/packages/client/ui-settings-web-search/package.json index 54f6d15630..7a04857145 100644 --- a/packages/client/ui-settings-web-search/package.json +++ b/packages/client/ui-settings-web-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings-web-search", "description": "Settings page of the DeepSeek web-search provider on the dsh web client's Plugins page: its API key, endpoint, and per-request search budget", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-settings/package.json b/packages/client/ui-settings/package.json index 5397a1ff93..322d42c0aa 100644 --- a/packages/client/ui-settings/package.json +++ b/packages/client/ui-settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-settings", "description": "Settings domain base plugin: shared configuration forms and the canonical settings slot-type contract", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-shortcuts/package.json b/packages/client/ui-shortcuts/package.json index 45ed72b60b..1e5af26650 100644 --- a/packages/client/ui-shortcuts/package.json +++ b/packages/client/ui-shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-shortcuts", "description": "Keyboard shortcut reference, recording, and local preference editing", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-browser/package.json b/packages/client/ui-sidebar-browser/package.json index a0341886ae..31531b12c8 100644 --- a/packages/client/ui-sidebar-browser/package.json +++ b/packages/client/ui-sidebar-browser/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-browser", "description": "Sandboxed Web browser tabs for the right Sidebar", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-documentpreview/package.json b/packages/client/ui-sidebar-documentpreview/package.json index f9379f659e..4a47f20e7c 100644 --- a/packages/client/ui-sidebar-documentpreview/package.json +++ b/packages/client/ui-sidebar-documentpreview/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-documentpreview", "description": "Extensible Sidebar previews for Office documents, spreadsheets, Markdown, code, images, PDF, HTML, and plain text", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-files/package.json b/packages/client/ui-sidebar-files/package.json index f5d99f16a4..48441fcfbb 100644 --- a/packages/client/ui-sidebar-files/package.json +++ b/packages/client/ui-sidebar-files/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-files", "description": "Workspace file tree tab type for the right Sidebar: lazy directory listing over the workspaceFiles Remote namespace, opening files into the Sidebar", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index 796919440f..c7743e1595 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-right", "description": "Right Sidebar: the docking surface's session-bound state, its panel and header expand control, and the navigation service over it", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar-terminal/package.json b/packages/client/ui-sidebar-terminal/package.json index 7f769cdace..78a3a981d9 100644 --- a/packages/client/ui-sidebar-terminal/package.json +++ b/packages/client/ui-sidebar-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar-terminal", "description": "Interactive shell tabs for the right Sidebar", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json index b470973be5..fff3ea5b30 100644 --- a/packages/client/ui-sidebar/package.json +++ b/packages/client/ui-sidebar/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-sidebar", "description": "Sidebar plugin: session multi-level tree, search, grouping, state dots", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-skill/README.i18n.yaml b/packages/client/ui-skill/README.i18n.yaml index f1abf210fd..86d3394580 100644 --- a/packages/client/ui-skill/README.i18n.yaml +++ b/packages/client/ui-skill/README.i18n.yaml @@ -21,8 +21,8 @@ en: 2ba1a6158c85b46e zh: 9ab58ceef8743849 /deepseek-ai-dsh-client-ui-skill/use-this-package/the-skill-tool-row: - en: 52c7aee8faf26795 - zh: f68e2f17cc7e369a + en: 452686249f73702d + zh: 47b0a191dfacba30 /deepseek-ai-dsh-client-ui-skill/understand-the-implementation: en: df2a0ee9d2638f41 zh: 3b4af7e505e990ca diff --git a/packages/client/ui-skill/README.md b/packages/client/ui-skill/README.md index 0234ce8fb6..2789896035 100644 --- a/packages/client/ui-skill/README.md +++ b/packages/client/ui-skill/README.md @@ -35,7 +35,7 @@ Ordinary-session candidates come from the `skills/list` Remote; the host serves The `preparing` stage shows only the skill icon and title, with no requested skill name or expansion. The `start` and `result` stages use the recorded arguments and output described below; preparation does not consult the installed catalog. -A collapsed row renders the skill glyph, `Skill` title, and requested skill name on the same secondary font-size axis, line height, row height, and glyph scaling as ordinary Tool rows. Every lifecycle state retains the ordinary skill glyph; running calls carry the transcript shimmer, while failures replace the name with the first error line and interruptions retain their explicit status text. A settled row expands into a bounded `Instructions` card containing the exact durable tool output, with the standard trajectory `Inspect` affordance when available; this expanded card keeps its own typography. The row derives its name, lifecycle, and body only from the frozen call/result slice supplied by ui-tool, never from the current catalog, so replay stays stable when installed skills or their descriptions change. +A collapsed row renders the skill glyph, `Skill` title, and requested skill name on the same secondary font-size axis, line height, row height, and glyph scaling as ordinary Tool rows. Every lifecycle state retains the ordinary skill glyph; running calls carry one left-to-right highlight across the title, separator, and skill name, while failures replace the name with the first error line and interruptions retain their explicit status text. A settled row expands into a bounded `Instructions` card containing the exact durable tool output, with the standard trajectory `Inspect` affordance when available; this expanded card keeps its own typography. The row derives its name, lifecycle, and body only from the frozen call/result slice supplied by ui-tool, never from the current catalog, so replay stays stable when installed skills or their descriptions change. Hovering over `/name` highlights the entire reference. Clicking a known skill opens its provider-supplied `SKILL.md` path in the right Sidebar while keeping the token editable. An uncached click shares the per-Session catalog fetch and opens when it completes, retaining the clicked Session address. Preset changes, connection resets, and plugin disposal cancel pending previews; a later click fetches the current catalog again. Skills without a file path remain invocable but have no file preview. diff --git a/packages/client/ui-skill/README.zh.md b/packages/client/ui-skill/README.zh.md index 5b039ec1c5..4568c0cacc 100644 --- a/packages/client/ui-skill/README.zh.md +++ b/packages/client/ui-skill/README.zh.md @@ -35,7 +35,7 @@ kind: "package-reference" `preparing` 阶段只显示技能图标与标题,不显示请求加载的技能名称,也不能展开。`start` 和 `result` 阶段使用下述记录参数与输出;准备阶段不查询已安装目录。 -收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。所有生命周期状态都保留普通 skill 图标;运行中的调用带有 transcript(文本记录)的扫光效果,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。 +收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。所有生命周期状态都保留普通 skill 图标;运行中的调用以同一道高光从左到右扫过标题、分隔符与 skill 名称,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。 鼠标移入 `/name` 时,背景覆盖整个引用。点击已知 skill 会在右侧栏打开提供方给出的 `SKILL.md` 路径,同时保留文本的可编辑性。缓存未就绪时,点击复用该 Session 的目录请求,在完成后按点击时的 Session 地址打开预览。切换预设、重置连接和插件释放会取消待处理的预览;后续点击重新获取当前目录。没有文件路径的 skill 仍可调用,但没有文件预览。 diff --git a/packages/client/ui-skill/package.json b/packages/client/ui-skill/package.json index d52d341fb9..d9ed058b6c 100644 --- a/packages/client/ui-skill/package.json +++ b/packages/client/ui-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-skill", "description": "Web skill references and the dedicated skill tool row", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-skill/src/client/SkillRow.module.css b/packages/client/ui-skill/src/client/SkillRow.module.css index 31abbe10cf..1e3c502688 100644 --- a/packages/client/ui-skill/src/client/SkillRow.module.css +++ b/packages/client/ui-skill/src/client/SkillRow.module.css @@ -18,27 +18,6 @@ cursor: pointer; } -.card[data-state='running'] .row::after, -.card[data-state='preparing'] .row::after { - content: ''; - position: absolute; - inset: 0 auto 0 0; - width: 300px; - background: linear-gradient( - 90deg, - transparent 0%, - color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, - transparent 100% - ); - animation: dsh-skill-row-sweep 2.6s ease-out infinite; - pointer-events: none; -} - -@keyframes dsh-skill-row-sweep { - 0% { left: -300px; } - 90%, 100% { left: 100%; } -} - .leading { position: relative; flex: none; @@ -210,11 +189,6 @@ } @media (prefers-reduced-motion: reduce) { - .card[data-state='running'] .row::after { - animation: none; - display: none; - } - .iconIdle, .chevronHover, .inspectButton { diff --git a/packages/client/ui-skill/src/client/SkillRow.tsx b/packages/client/ui-skill/src/client/SkillRow.tsx index 0800bc7812..cee91367cf 100644 --- a/packages/client/ui-skill/src/client/SkillRow.tsx +++ b/packages/client/ui-skill/src/client/SkillRow.tsx @@ -1,6 +1,6 @@ import { useState, type KeyboardEvent, type ReactNode } from 'react' import { - IconChevronDownOutlineRegular, IconInspectOutlineRegular, IconSkillOutlineRegular, + IconChevronDownOutlineRegular, IconInspectOutlineRegular, IconSkillOutlineRegular, TextShimmer, } from '@deepseek-ai/dsh-client-ui-primitives' import type { StartedToolCallViewProps, ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' @@ -104,7 +104,7 @@ export function SkillRow(props: SkillRowProps) {
{props.t('row.preparing')} - {props.t('row.title')} + {props.t('row.title')}
return @@ -116,6 +116,7 @@ function StartedSkillRow({ block, inspect, t }: Exclude { setExpanded(value => !value) @@ -142,14 +143,16 @@ function StartedSkillRow({ block, inspect, t }: Exclude {leading} {status !== null ? {status} : null} - {t('row.title')} - - - {summary} - + + {t('row.title')} + + + {summary} + + {open ? (
diff --git a/packages/client/ui-skill/tests/skill-row.client.spec.tsx b/packages/client/ui-skill/tests/skill-row.client.spec.tsx index a34b8ff4e9..34b143d0ef 100644 --- a/packages/client/ui-skill/tests/skill-row.client.spec.tsx +++ b/packages/client/ui-skill/tests/skill-row.client.spec.tsx @@ -110,7 +110,7 @@ describe('SkillRow', () => { }))} />) const row = screen.getByRole('button', { name: 'skill 加载失败加载技能SkillError: missing resource' }) expect(view.container.querySelector('[data-tool="skill"]')?.getAttribute('data-state')).toBe('error') - expect(view.container.querySelector('[data-tool="skill"] > div > span:first-child svg')).not.toBeNull() + expect(view.container.querySelector('[data-tool="skill"] [role="button"] svg')).not.toBeNull() expect(view.container.querySelector('[data-tool="skill"] [data-state]')).toBeNull() expect(row.textContent).not.toContain('Check SKILL.md.') fireEvent.click(row) @@ -124,8 +124,8 @@ describe('SkillRow', () => { error: { name: 'InterruptedError', code: 'interrupted' }, }))} />) const stoppedSummary = stoppedView.getByText('skill 加载已中止') - expect(stoppedSummary.className).toContain('stoppedSummary') - expect(stoppedView.container.querySelector('[data-tool="skill"] > div > span:first-child svg')).not.toBeNull() + expect(stoppedSummary.parentElement?.className).toContain('stoppedSummary') + expect(stoppedView.container.querySelector('[data-tool="skill"] [role="button"] svg')).not.toBeNull() expect(stoppedView.container.querySelector('[data-tool="skill"] [data-state]')).toBeNull() cleanup() diff --git a/packages/client/ui-slots/package.json b/packages/client/ui-slots/package.json index 0ad0e20bb7..774bfca205 100644 --- a/packages/client/ui-slots/package.json +++ b/packages/client/ui-slots/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-slots", "description": "Slot registry pure core: typed ordinary Slots and reusable Component Factories, derived props, Store seats, and renderer installation", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-subagent/package.json b/packages/client/ui-subagent/package.json index 9d09b1e1ae..75d2663933 100644 --- a/packages/client/ui-subagent/package.json +++ b/packages/client/ui-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-subagent", "description": "Subagent conversation catalog, continuation routing UI, and '@' reference source", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-theme/README.i18n.yaml b/packages/client/ui-theme/README.i18n.yaml index 6d7d8b255c..82ac586927 100644 --- a/packages/client/ui-theme/README.i18n.yaml +++ b/packages/client/ui-theme/README.i18n.yaml @@ -30,8 +30,8 @@ en: 8fbfdb050e880f9b zh: dd201f049036c5c4 /deepseek-ai-dsh-client-ui-theme/understand-the-implementation/stylesheets: - en: b3fe877c28a600d8 - zh: c8a28e4d9189c0ea + en: 76374ee500418cfa + zh: 741256fbc54c8207 /deepseek-ai-dsh-client-ui-theme/understand-the-implementation/scrollbar-rebinding: en: bf76e7c34a331595 zh: 5accd0260ef7c61e diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index a07cc5e08d..0dc68a7b41 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -67,6 +67,8 @@ Menu icons use `--dsw-alias-menu-icon`: neutral-bluish 800 in light mode and `la System toasts use `--dsw-alias-toast-bg` and `--dsw-alias-toast-label` for a shared background and text color across callers. Document previews pair `--dsw-alias-bg-document-preview` with `--dsw-alias-label-document-preview` so the backdrop and status text follow the same theme. Tooltip keycaps use `--dsw-alias-tooltip-key-bg`, a lighter fill derived from the tooltip background in each palette. Switch thumbs read `--dsw-alias-switch-thumb`: white in light mode and neutral-bluish 400 in dark mode, so an off switch stays lighter than its track without the glare of pure white. +`--dsw-alias-label-shimmer` supplies an overlay for the shared text shimmer: black at 30% alpha in the light palette and white at 45% alpha in the dark palette. `--dsw-alias-label-deep-diving` and `--dsw-alias-label-deep-diving-shimmer` supply the blue activity label and sweep; the dark palette uses a lighter, less saturated label with a brighter blue sweep. + `brand-font.css` exports the local Montserrat Light, Regular and Medium faces (normal style, weights 300, 400 and 500), with `montserrat-light.woff2`, `montserrat-regular.woff2`, `montserrat-medium.woff2` and its SIL Open Font License in `lib/styles/`. Desktop bundles the same stylesheet, font and license for offline welcome brand text; ordinary UI keeps its system font stack. `corner-shape.css` smooths every rounded corner: inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape` and applies it to all elements and their `::before`/`::after` through the universal selector, so engines without `corner-shape` keep circular corners. Full-round shapes — `border-radius: 50%` circles and pill radii — pair `corner-shape: round` with their radius in the owning component sheet because a superellipse deforms them; the corner-shape stylesheet spec enforces that pairing across every package stylesheet. diff --git a/packages/client/ui-theme/README.zh.md b/packages/client/ui-theme/README.zh.md index c1a3abcee4..5767eebced 100644 --- a/packages/client/ui-theme/README.zh.md +++ b/packages/client/ui-theme/README.zh.md @@ -67,6 +67,8 @@ kind: "package-reference" 系统提示使用 `--dsw-alias-toast-bg` 和 `--dsw-alias-toast-label`,在各调用方之间统一背景与文字颜色。文档预览配对使用 `--dsw-alias-bg-document-preview` 与 `--dsw-alias-label-document-preview`,使底色与状态文字遵循相同主题。Tooltip 键帽使用 `--dsw-alias-tooltip-key-bg`,由各主题的 tooltip 背景派生稍浅的填充。开关滑块读取 `--dsw-alias-switch-thumb`:浅色模式为纯白,深色模式为 neutral-bluish 400,使关闭态滑块比轨道更亮,同时避免纯白在暗色下过亮。 +`--dsw-alias-label-shimmer` 为共享文字扫光提供叠加色:浅色配色使用 30% alpha 的黑色,深色配色使用 45% alpha 的白色。`--dsw-alias-label-deep-diving` 和 `--dsw-alias-label-deep-diving-shimmer` 提供蓝色活动文本和扫光颜色;深色主题使用更亮、饱和度更低的文字,并以更亮的蓝色扫光。 + `brand-font.css` 导出本地 Montserrat Light、Regular 和 Medium 字体(正体、字重 300、400 和 500),`lib/styles/` 同时提供 `montserrat-light.woff2`、`montserrat-regular.woff2`、`montserrat-medium.woff2` 及其 SIL Open Font License。Desktop 将同一份样式表、字体和许可证打包,用于欢迎页品牌文字的离线显示;普通界面保留系统字体栈。 `corner-shape.css` 平滑所有圆角:在 `@supports (corner-shape: superellipse(1.5))` 内定义 `--dsw-corner-shape`,并通过通配选择器应用到所有元素及其 `::before`/`::after`,因此不支持 `corner-shape` 的引擎保持普通圆弧。正圆形状——`border-radius: 50%` 的圆与胶囊半径——因超级椭圆会使其变形,须在所属组件样式表中把 `corner-shape: round` 与半径声明配对;corner-shape 样式表 spec 跨全部包样式表强制这一配对。 diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json index cc0e7919f6..612439386b 100644 --- a/packages/client/ui-theme/package.json +++ b/packages/client/ui-theme/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-theme", "description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-theme/src/styles/design-platform.css b/packages/client/ui-theme/src/styles/design-platform.css index d9a7207983..af094e9779 100644 --- a/packages/client/ui-theme/src/styles/design-platform.css +++ b/packages/client/ui-theme/src/styles/design-platform.css @@ -213,6 +213,7 @@ body { --dsw-alias-interactive-bg-hover: rgba(38, 49, 72, 0.06); --dsw-alias-label-caption: var(--dsw-static-neutral-bluish-400); --dsw-alias-label-deep-diving: color-mix(in srgb, var(--dsw-static-deepseek-500) 70%, var(--dsw-static-blue-950)); + --dsw-alias-label-deep-diving-shimmer: color-mix(in srgb, var(--dsw-static-deepseek-500) 30%, var(--dsw-static-blue-950)); --dsw-alias-label-dimmed: var(--dsw-static-neutral-bluish-200); --dsw-alias-label-primary-bluish: var(--dsw-static-blue-900); --dsw-alias-label-primary-dimmed: var(--dsw-static-neutral-bluish-950); @@ -220,6 +221,7 @@ body { --dsw-alias-label-primary-inverted: var(--dsw-static-neutral-bluish-00); --dsw-alias-label-primary: var(--dsw-static-neutral-bluish-1000); --dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-700); + --dsw-alias-label-shimmer: color-mix(in srgb, var(--dsw-static-neutral-1000) 30%, transparent); --dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-600); --dsw-alias-menu-icon: var(--dsw-static-neutral-bluish-800); --dsw-alias-link: var(--dsw-static-deepseek-500); @@ -326,6 +328,7 @@ body[data-ds-dark-theme] { --dsw-alias-interactive-bg-hover: rgba(255, 255, 255, 0.08); --dsw-alias-label-caption: var(--dsw-static-neutral-bluish-600); --dsw-alias-label-deep-diving: color-mix(in srgb, var(--dsw-static-deepseek-450) 55%, var(--dsw-static-neutral-bluish-400)); + --dsw-alias-label-deep-diving-shimmer: color-mix(in srgb, var(--dsw-static-blue-300) 65%, var(--dsw-static-deepseek-400)); --dsw-alias-label-dimmed: var(--dsw-static-neutral-bluish-750); --dsw-alias-label-primary-bluish: var(--dsw-static-neutral-bluish-50); --dsw-alias-label-primary-dimmed: var(--dsw-static-neutral-bluish-100); @@ -333,6 +336,7 @@ body[data-ds-dark-theme] { --dsw-alias-label-primary-inverted: var(--dsw-static-neutral-bluish-800); --dsw-alias-label-primary: var(--dsw-static-neutral-bluish-50); --dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-300); + --dsw-alias-label-shimmer: color-mix(in srgb, var(--dsw-static-neutral-00) 45%, transparent); --dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-400); --dsw-alias-menu-icon: var(--dsw-alias-label-primary-dimmed); --dsw-alias-link: var(--dsw-static-deepseek-400); diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index 91a6e15e99..fc442d774a 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/README.i18n.yaml @@ -21,8 +21,8 @@ en: 5332d87e76037f07 zh: db5d06e7e46b8429 /deepseek-ai-dsh-client-ui-tool/use-this-package/built-in-views: - en: 4b69e36952574494 - zh: 1f9842b40931adb6 + en: 53bd3369a3b868b0 + zh: 030f281a5bd66c6f /deepseek-ai-dsh-client-ui-tool/understand-the-implementation: en: 00bc7b0c84edc603 zh: 1cfcf5b435248a91 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index b78008e6b0..86e5287626 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -49,7 +49,7 @@ The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the `phase` dis Every registered view receives the explicit `preparing`, `start`, and `result` props declared in [the Tool slot types](src/client/contract/slots.ts). Shared rows use the same `ToolRow` in all stages. Their row model selects the title and combines any generic tool-name prefix with the available argument summary independently of lifecycle state. Tool-owned titles omit the English name. The shared argument parser returns no call during preparation and does not parse partial JSON. Write/edit use separate preparing and dispatched components, so only the preparing component invokes `useToolCallArgumentsPartial`; start and result share the dispatched component. Custom renderers such as Bash, Skill, and Cordis handle preparation separately; their argument-dependent components accept `StartedToolCallViewProps`. -This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and PTC dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in PTC dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while background acknowledgements remain collapsed. A native or PTC dispatch failure carrying `AUTO_REVIEW_DENIED` shows the Auto review verdict in its collapsed row and one normalized not-executed reason when expanded; a missing or whitespace-only reason uses localized fallback text. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. +This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and PTC dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running atomic rows share one left-to-right highlight across their titles, separators, summaries, and suffixes, while settled rows stay static. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in PTC dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while background acknowledgements remain collapsed. A native or PTC dispatch failure carrying `AUTO_REVIEW_DENIED` shows the Auto review verdict in its collapsed row and one normalized not-executed reason when expanded; a missing or whitespace-only reason uses localized fallback text. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. ----- diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 39d4a5652e..f33ec97dc2 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -49,7 +49,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、`phase` 判别字 每个注册视图都接收[工具 slot 类型](src/client/contract/slots.ts)声明的显式 `preparing`、`start` 和 `result` props。通用行在三个阶段使用同一个 `ToolRow`。行模型统一选择标题,并组合通用工具名前缀与已有参数摘要,不按生命周期阶段改变前缀;专用标题不附带英文名。准备阶段的共享参数解析入口直接返回无调用,不解析部分 JSON。write/edit 将准备态和派发后阶段拆成两个组件,只有准备态组件调用 `useToolCallArgumentsPartial`,start 与 result 共用派发后组件。Bash、Skill、Cordis 等自定义 renderer 分别处理准备态,其依赖参数的组件接收 `StartedToolCallViewProps`。 -本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、运行中的 `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 PTC dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 PTC dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。带有 `AUTO_REVIEW_DENIED` 的原生或 PTC dispatch 失败会在折叠行显示 Auto review 裁决,展开时显示一行归一化后的“未执行”原因;原因缺失或只有空白时使用本地化 fallback 文案。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 +本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、运行中的 `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 PTC dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中的原子工具行以同一道高光从左到右扫过标题、分隔符、摘要与后缀;已完成行保持静态。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 PTC dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。带有 `AUTO_REVIEW_DENIED` 的原生或 PTC dispatch 失败会在折叠行显示 Auto review 裁决,展开时显示一行归一化后的“未执行”原因;原因缺失或只有空白时使用本地化 fallback 文案。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 ----- diff --git a/packages/client/ui-tool/package.json b/packages/client/ui-tool/package.json index 4cdbd1c499..2e0a891062 100644 --- a/packages/client/ui-tool/package.json +++ b/packages/client/ui-tool/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-tool", "description": "Client Tool call-tree renderer and keyed per-tool presentation slot", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx index c9279a279c..f4a1e0b486 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx @@ -213,7 +213,7 @@ export const ToolRow = memo(function ToolRow({ /* An empty summary drops the separator with it (a row that is only its title shows no trailing dot). */ <> - + {openFile !== undefined ? ( ) : linkHref !== undefined ? ( - {summaryText} + {summaryText} ) : ( - {summaryText} + {summaryText} )} {suffix !== null && ( - {suffix} + {suffix} )} ), [diffStat, summaryLinkKeyDown, linkHref, openFile, running, state, suffix, summaryText]) diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx index 47ae2d1b6d..d8fd8032d7 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx @@ -109,15 +109,17 @@ const StartedBashRow = memo(function StartedBashRow({ toolName, block, sessionId > {leading} {status !== null && {status}} - {t(model.titleKey)} - - - {settlementLine ?? normalSummary} - + + {t(model.titleKey)} + + + {settlementLine ?? normalSummary} + +
{open && (
diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index 96fa4fd5b3..847e557cf6 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-trajectory", "description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-user-questions/package.json b/packages/client/ui-user-questions/package.json index f94b0d70ed..a04ddc8bb8 100644 --- a/packages/client/ui-user-questions/package.json +++ b/packages/client/ui-user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-user-questions", "description": "Web ask_user_question composer takeover and plan-review presentation UI", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workflow-run/package.json b/packages/client/ui-workflow-run/package.json index 29342ad3bf..766107f3a1 100644 --- a/packages/client/ui-workflow-run/package.json +++ b/packages/client/ui-workflow-run/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workflow-run", "description": "Durable workflow-run Conversation Node and nested member disclosure for dsh web", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css b/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css index 290c479d0d..15eab9b06f 100644 --- a/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css +++ b/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css @@ -264,3 +264,11 @@ padding-left: 12px; } } + +.headerContent { + flex: 1; +} + +.headerContentLayout { + gap: 6px; +} diff --git a/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx b/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx index ee98a85fbf..ff13de9cc3 100644 --- a/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx +++ b/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx @@ -219,6 +219,8 @@ function RunHeader({ children, count, name, onToggle, open, status, t }: { previewChevron={false} keepContentWhenOpen rowClassName={css.runHeader} + contentClassName={css.headerContent} + contentLayoutClassName={css.headerContentLayout} leadingClassName={css.runLeading} titleClassName={css.runTitle} collapsedContent={( @@ -312,6 +314,8 @@ function PhaseSection({ previewChevron={false} keepContentWhenOpen rowClassName={css.phaseHeader} + contentClassName={css.headerContent} + contentLayoutClassName={css.headerContentLayout} leadingClassName={css.phaseLeading} titleClassName={css.phaseTitle} collapsedContent={( diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json index 03698fd96c..1a05121bf0 100644 --- a/packages/client/ui-workspace/package.json +++ b/packages/client/ui-workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-workspace", "description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/client/web/package.json b/packages/client/web/package.json index 0d0518625d..f70f5915d6 100644 --- a/packages/client/web/package.json +++ b/packages/client/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-web", "description": "Web boot kernel: static module table, Cordis loader, framework-free boot page, and UI-renderer handoff", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/command-compact/package.json b/packages/compaction/command-compact/package.json index 7f58b3541e..1f8d4f4637 100644 --- a/packages/compaction/command-compact/package.json +++ b/packages/compaction/command-compact/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-compact", "description": "Human-facing slash command for explicit session compaction", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-basic/package.json b/packages/compaction/compaction-basic/package.json index 6c9a7130d5..d3a8ce720a 100644 --- a/packages/compaction/compaction-basic/package.json +++ b/packages/compaction/compaction-basic/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-basic", "description": "Token-meter-driven compaction policy and LLM summarization backend for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-image-offload/package.json b/packages/compaction/compaction-image-offload/package.json index cc79880f14..b3c4900367 100644 --- a/packages/compaction/compaction-image-offload/package.json +++ b/packages/compaction/compaction-image-offload/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-image-offload", "description": "Durable image offload for image-capable routes: replace over-budget request images with placeholders and retry", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction-tool-result-pruner/package.json b/packages/compaction/compaction-tool-result-pruner/package.json index 55cf2ff9bf..ef848d35ad 100644 --- a/packages/compaction/compaction-tool-result-pruner/package.json +++ b/packages/compaction/compaction-tool-result-pruner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction-tool-result-pruner", "description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/compaction/compaction/package.json b/packages/compaction/compaction/package.json index 11661c9678..0ea2253877 100644 --- a/packages/compaction/compaction/package.json +++ b/packages/compaction/compaction/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-compaction", "description": "Abstract compaction service seam (ctx.compaction) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/computer-use/computer-use/package.json b/packages/computer-use/computer-use/package.json index 32b142e879..a67fb347b1 100644 --- a/packages/computer-use/computer-use/package.json +++ b/packages/computer-use/computer-use/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-computer-use", "description": "Exclusive named computer-use provider registration", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/agent-instructions/package.json b/packages/context/agent-instructions/package.json index f1e644cecf..3c38c183c4 100644 --- a/packages/context/agent-instructions/package.json +++ b/packages/context/agent-instructions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-instructions", "description": "Workspace context loader for AGENTS.md/CLAUDE.md instruction files", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference-local/package.json b/packages/context/file-reference-local/package.json index b469e81c75..5f353a3f91 100644 --- a/packages/context/file-reference-local/package.json +++ b/packages/context/file-reference-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference-local", "description": "Local-filesystem ctx.fileReferences provider with bounded fuzzy indexes", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json index d7b9bfe11b..b6b2da8a49 100644 --- a/packages/context/file-reference/package.json +++ b/packages/context/file-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-file-reference", "description": "File-reference discovery contract and shared @file grammar", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/session-reference/package.json b/packages/context/session-reference/package.json index 6153e5fae4..99d99e38ac 100644 --- a/packages/context/session-reference/package.json +++ b/packages/context/session-reference/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-reference", "description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index 528fe7e0b7..0fd18e2ddb 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-time-context", "description": "Durable per-step context with the current time and elapsed time", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/context/tmux-context/package.json b/packages/context/tmux-context/package.json index 6a552ff1e1..0540c27f3f 100644 --- a/packages/context/tmux-context/package.json +++ b/packages/context/tmux-context/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tmux-context", "description": "Opt-in durable per-step context with this agent's tmux pane and window location", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-default-model/package.json b/packages/core/agent-default-model/package.json index 31679aba19..334238a71e 100644 --- a/packages/core/agent-default-model/package.json +++ b/packages/core/agent-default-model/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-default-model", "description": "Default model selection shared by Agent entry points", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 2712ccaa31..a82a9da37b 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop", "description": "The concrete agent loop plugin for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent-tool-presentation/package.json b/packages/core/agent-tool-presentation/package.json index f8628f22cc..a8157439cc 100644 --- a/packages/core/agent-tool-presentation/package.json +++ b/packages/core/agent-tool-presentation/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-tool-presentation", "description": "Agent-plane presentation selector: composes one agent's tools as PTC mode, native, or both", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index 424d4c2d40..c094cd19fe 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent", "description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/scope/package.json b/packages/core/scope/package.json index 2641c48dd3..f60c594582 100644 --- a/packages/core/scope/package.json +++ b/packages/core/scope/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-scope", "description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/session/package.json b/packages/core/session/package.json index b3c9e7e014..73977b2dd6 100644 --- a/packages/core/session/package.json +++ b/packages/core/session/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session", "description": "Event-sourced session store for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json index 36fb67c733..ad32a71ea0 100644 --- a/packages/core/system-prompt/package.json +++ b/packages/core/system-prompt/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-system-prompt", "description": "System prompt assembly registry for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json index 13fc83cfea..a49afc303a 100644 --- a/packages/core/tools/package.json +++ b/packages/core/tools/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tools", "description": "Tool registry and execution pipeline for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/authorization/package.json b/packages/credentials/authorization/package.json index adde6e5287..d154c10f62 100644 --- a/packages/credentials/authorization/package.json +++ b/packages/credentials/authorization/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-authorization", "description": "Authorization seam (ctx.authorization): plugin-owned flows that obtain a credential through a conversation with the human", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials-local/package.json b/packages/credentials/credentials-local/package.json index 0ee64d2696..1968829be9 100644 --- a/packages/credentials/credentials-local/package.json +++ b/packages/credentials/credentials-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials-local", "description": "File-backed credentials provider ($DSH_HOME/.env under the live process environment) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/credentials/package.json b/packages/credentials/credentials/package.json index a20aa7d42c..42f4380d6b 100644 --- a/packages/credentials/credentials/package.json +++ b/packages/credentials/credentials/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-credentials", "description": "Abstract credential seam (ctx.credentials): settings carry references to secrets, providers own the values", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/deepseek-account-platform/package.json b/packages/credentials/deepseek-account-platform/package.json index c34e2a06d7..8a5b2426df 100644 --- a/packages/credentials/deepseek-account-platform/package.json +++ b/packages/credentials/deepseek-account-platform/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-account-platform", "description": "Authorize DeepSeek accounts through browser PKCE", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/credentials/deepseek-account/package.json b/packages/credentials/deepseek-account/package.json index e0be8d2a44..193520c76d 100644 --- a/packages/credentials/deepseek-account/package.json +++ b/packages/credentials/deepseek-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-account", "description": "Read account state and resolve official API credentials", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/tool-present/package.json b/packages/deliverables/tool-present/package.json index 8aa79b33ba..fbb26c2ae6 100644 --- a/packages/deliverables/tool-present/package.json +++ b/packages/deliverables/tool-present/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-present", "description": "Explicit workspace file delivery declarations for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/deliverables/workspace-changes/package.json b/packages/deliverables/workspace-changes/package.json index d71bbb829f..2c68d8bceb 100644 --- a/packages/deliverables/workspace-changes/package.json +++ b/packages/deliverables/workspace-changes/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace-changes", "description": "Per-turn workspace file changes recorded from git working-tree snapshots and whole-file captures, with per-file comparisons, for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/document/office-to-pdf/package.json b/packages/document/office-to-pdf/package.json index 8b0d09acf5..675fbb7dd9 100644 --- a/packages/document/office-to-pdf/package.json +++ b/packages/document/office-to-pdf/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-office-to-pdf", "description": "Shared Office-to-PDF conversion with bounded queues and caching", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team-profile/package.json b/packages/experimental/agent-team-profile/package.json index 852dd3d9b3..2f1b45c19f 100644 --- a/packages/experimental/agent-team-profile/package.json +++ b/packages/experimental/agent-team-profile/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team-profile", "description": "Agent Teams collaboration, tools, and Web UI in one experimental bundle", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/agent-team/package.json b/packages/experimental/agent-team/package.json index 19306fc98f..d73fa056cd 100644 --- a/packages/experimental/agent-team/package.json +++ b/packages/experimental/agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-agent-team", "description": "Implicit-root Agent Teams roster, durable peer mailbox, and shared task DAG", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/api-speech-to-text/package.json b/packages/experimental/api-speech-to-text/package.json index 5435f37725..fc671c4af8 100644 --- a/packages/experimental/api-speech-to-text/package.json +++ b/packages/experimental/api-speech-to-text/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-api-speech-to-text", "description": "Authenticated experimental speech transcription for browser clients", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/auto-review/package.json b/packages/experimental/auto-review/package.json index e877961c20..62818ec32e 100644 --- a/packages/experimental/auto-review/package.json +++ b/packages/experimental/auto-review/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-auto-review", "description": "Per-tool LLM authorization review for the DeepSeek Harness Auto permission preset", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "repository": { "type": "git", "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", diff --git a/packages/experimental/browser-use-chrome-devtools-mcp/package.json b/packages/experimental/browser-use-chrome-devtools-mcp/package.json index d21a7b2588..81e0f7bccf 100644 --- a/packages/experimental/browser-use-chrome-devtools-mcp/package.json +++ b/packages/experimental/browser-use-chrome-devtools-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp", "description": "Experimental per-Session Chromium browser tools through chrome-devtools-mcp", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-playwright-mcp/package.json b/packages/experimental/browser-use-playwright-mcp/package.json index f5b9c4cbd2..af69b5014a 100644 --- a/packages/experimental/browser-use-playwright-mcp/package.json +++ b/packages/experimental/browser-use-playwright-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-playwright-mcp", "description": "Experimental per-Session Chromium browser tools through @playwright/mcp", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-runtime/package.json b/packages/experimental/browser-use-runtime/package.json index 7a12ae409c..8ad15c11a1 100644 --- a/packages/experimental/browser-use-runtime/package.json +++ b/packages/experimental/browser-use-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-runtime", "description": "Session-owned browser resource lifecycles and MCP integration for experimental providers", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/browser-use-stagehand-native/package.json b/packages/experimental/browser-use-stagehand-native/package.json index eddd65447f..babd09bce5 100644 --- a/packages/experimental/browser-use-stagehand-native/package.json +++ b/packages/experimental/browser-use-stagehand-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-browser-use-stagehand-native", "description": "Experimental Stagehand browser tools with separately configured native models", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-agent-team/package.json b/packages/experimental/client-ui-agent-team/package.json index dad156e20b..752c5c93b7 100644 --- a/packages/experimental/client-ui-agent-team/package.json +++ b/packages/experimental/client-ui-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-agent-team", "description": "Web Agent Teams roster, task board, and teammate navigation", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/client-ui-voice-input/package.json b/packages/experimental/client-ui-voice-input/package.json index adb8f79185..7daded7062 100644 --- a/packages/experimental/client-ui-voice-input/package.json +++ b/packages/experimental/client-ui-voice-input/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-client-ui-voice-input", "description": "Record speech and insert editable text into the conversation draft", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/computer-use-cua-driver-mcp/package.json b/packages/experimental/computer-use-cua-driver-mcp/package.json index 51c48ec893..8e2c5a48b0 100644 --- a/packages/experimental/computer-use-cua-driver-mcp/package.json +++ b/packages/experimental/computer-use-cua-driver-mcp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp", "description": "Experimental computer use through an installed Cua Driver MCP executable", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/computer-use-cua-driver-native/package.json b/packages/experimental/computer-use-cua-driver-native/package.json index cab84c9a22..bab4a0944b 100644 --- a/packages/experimental/computer-use-cua-driver-native/package.json +++ b/packages/experimental/computer-use-cua-driver-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-computer-use-cua-driver-native", "description": "Experimental computer-use provider embedding the Cua Driver native npm SDK", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/inspector/package.json b/packages/experimental/inspector/package.json index 5a3de17a4a..a545cf4c71 100644 --- a/packages/experimental/inspector/package.json +++ b/packages/experimental/inspector/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-inspector", "description": "Experimental cross-realm CDP hub for Host debugging and Client Runtime inspection", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/ptc-runtime-python/package.json b/packages/experimental/ptc-runtime-python/package.json index 9b0ea3ac10..6f5cea3dc8 100644 --- a/packages/experimental/ptc-runtime-python/package.json +++ b/packages/experimental/ptc-runtime-python/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-ptc-runtime-python", "description": "CPython subprocess implementation of the DeepSeek Harness PTC execution seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "repository": { "type": "git", "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", diff --git a/packages/experimental/schedule-bundle/package.json b/packages/experimental/schedule-bundle/package.json index cd36b381ea..54a3fe78cc 100644 --- a/packages/experimental/schedule-bundle/package.json +++ b/packages/experimental/schedule-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-schedule-bundle", "description": "Optional bundle that adds Schedule, its task page, and time context to the Web composition", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/speech-to-text-sensevoice/package.json b/packages/experimental/speech-to-text-sensevoice/package.json index f50feeafc2..e2a9e2b3a2 100644 --- a/packages/experimental/speech-to-text-sensevoice/package.json +++ b/packages/experimental/speech-to-text-sensevoice/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-speech-to-text-sensevoice", "description": "Local SenseVoice ONNX transcription with a managed sherpa-onnx process", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/speech-to-text/package.json b/packages/experimental/speech-to-text/package.json index 5fb5c30932..afdc78f42c 100644 --- a/packages/experimental/speech-to-text/package.json +++ b/packages/experimental/speech-to-text/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-speech-to-text", "description": "Experimental speech recognition with independently selectable providers", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/tool-agent-team/package.json b/packages/experimental/tool-agent-team/package.json index f83746bf76..5837c8d4a7 100644 --- a/packages/experimental/tool-agent-team/package.json +++ b/packages/experimental/tool-agent-team/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-tool-agent-team", "description": "Scoped model-facing Agent Teams tools over ctx.agentTeams", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/voice-input-bundle/package.json b/packages/experimental/voice-input-bundle/package.json index c859a80cbb..489237a886 100644 --- a/packages/experimental/voice-input-bundle/package.json +++ b/packages/experimental/voice-input-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-voice-input-bundle", "description": "Experimental voice input with local SenseVoice; downloads its runtime on first use", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webworker-packer/package.json b/packages/experimental/webworker-packer/package.json index e31a6c7fc3..2ec62e870d 100644 --- a/packages/experimental/webworker-packer/package.json +++ b/packages/experimental/webworker-packer/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-packer", "description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webworker-runtime/README.i18n.yaml b/packages/experimental/webworker-runtime/README.i18n.yaml index f8492ab94f..d720e524cd 100644 --- a/packages/experimental/webworker-runtime/README.i18n.yaml +++ b/packages/experimental/webworker-runtime/README.i18n.yaml @@ -15,8 +15,8 @@ en: 4e53164159b5ce41 zh: 89582dc0294d0776 /deepseek-ai-dsh-experimental-webworker-runtime/use-this-package: - en: b26cf7ab7c321c58 - zh: 0adf7210dfdf68bb + en: 7c80a0d046f93fb1 + zh: a9fdcc7a9f9a8828 /deepseek-ai-dsh-experimental-webworker-runtime/model-experience: en: 61fce9b4c7dc96e6 zh: dc2d931063a38327 @@ -24,8 +24,8 @@ en: ca75c51c89c2c9b0 zh: 64715dc7e6d02f71 /deepseek-ai-dsh-experimental-webworker-runtime/known-limitations-and-deferred-work: - en: 206354046ab86288 - zh: b3cbcf9c663dd978 + en: 6fe586817a42fdad + zh: c19ab678d61e82d3 /deepseek-ai-dsh-experimental-webworker-runtime/known-limitations-and-deferred-work/dev-note: en: f14ab6f2449c3c53 zh: e3f7d3a3d772c0b7 diff --git a/packages/experimental/webworker-runtime/README.md b/packages/experimental/webworker-runtime/README.md index 91e6a6f0aa..f968b40ec0 100644 --- a/packages/experimental/webworker-runtime/README.md +++ b/packages/experimental/webworker-runtime/README.md @@ -27,10 +27,10 @@ The public npm package exposes the runtime library at its root, the self-contain Three build artifacts and one source-owned process layer: -- **`lib/index.js` (assembly library)** — `createWorkerHost`/`startWorkerHost` mount the base image and any ordered data overlays (`storage/`), install the module loader (`module-system/`) and the `process` shim, boot the tree through the image's own `dsh-app-boot`, and hand the tunnel its serving seams. Overlays may replace files only under `home/` and `workspace/`; they cannot replace the base manifest, configuration, or modules. The image layout contract (`image-layout.ts`: virtual root, config/manifest paths, empty directories, the `lowered` wrapper-contract gate) is shared with the packer. The Worker supplies `profileContext` for an editable profile at `home/profiles/preview/`: the packed rows seed its insertion patch, and ConfigEditor persists overrides in that VFS patch. ConfigEditor reconciles each write and Loader commits live values; package management and Node module HMR stay disabled. Deployment overlays keep JSONL session logs plaintext and preset roots at the image's `config/agent-presets`. Configuration edits last for the current Worker lifetime. +- **`lib/index.js` (assembly library)** — `createWorkerHost`/`startWorkerHost` mount the base image and any ordered data overlays (`storage/`), install the module loader (`module-system/`) and the `process` shim, boot the tree through the image's own `dsh-app-boot`, and hand the tunnel its serving seams. Overlays may replace files only under `home/` and `workspace/`; they cannot replace the base manifest, configuration, or modules. The image layout contract (`image-layout.ts`: virtual root, config/manifest paths, empty directories, the `lowered` wrapper-contract gate) is shared with the packer. The Worker supplies `profileContext` for an editable profile at `home/profiles/preview/`: the packed rows seed its insertion patch, and ConfigEditor persists overrides in that VFS patch. ConfigEditor reconciles each write and Loader commits live values. The Worker commits `appReady` once the tunnel serves, so HMR loads and queues profile configuration changes; the `process` shim reports no entry script, so HMR performs no Node module-cache classification. Plugin management loads and edits the profile, and its package operations fail with the `execa` refusal. Deployment overlays keep JSONL session logs plaintext and preset roots at the image's `config/agent-presets`. Configuration edits last for the current Worker lifetime. - **`lib/worker.js` (worker bundle)** — the assembly plus this package's Node-compatibility layer as one self-contained ES module. The module proxy table (`module-proxies.ts`) is the only platform fork: `node:*` builtins over VFS/tunnel/browser primitives, structural stubs that fail loud on the console for what a browser cannot do, and native/binary package replacements. `node:module` supplies `createRequire().resolve` and `.resolve.paths()` over the image package root, so unchanged packages can discover manifests without evaluating their modules. The `node:util` JSON-data comparator compares own enumerable properties, preserving Node admission decisions for recorded fields such as `__proto__`. The global `process` shim carries Node detection fields including `title`, preventing Worker execution from entering DOM-only branches. The pack-time parser reports statically named module requests, including module-scope direct calls of the form `createRequire(import.meta.url)('pkg')` through a named `node:module` or `module` import, to the packer's reachability walk. Stored, CommonJS-obtained, and rebased `createRequire` calls require image entry seeds. VFS mutations drive `node:fs` callback, polling, and promise watchers; open descriptors retain file identity and access mode across rename, replacement, and unlink, `FileHandle.stat({ bigint: true })` reports the same device and inode identity as a path stat while the name still refers to that file, and `FileHandle.chmod()` updates the opened file identity; `readable-stream` supplies `node:stream/promises` completion and pipeline helpers and the stream state machine used by file streams and unchanged image packages such as Chokidar and readdirp. AsyncLocalStorage carries sync-stack causality across `await` through the snapshot/restore faces the pack-time lowering injects. The worker holds no compiler: an image the packer did not lower is refused at mount. The callback `fs.realpath.native` alias resolves within the mounted VFS and supports `util.promisify`. -- **`src/shell/` (the worker's own process layer)** — a browser worker cannot fork, so `node:child_process` is not a stub but an implementation: `spawn` starts the command in its own Web Worker — this same bundle, told by its first frame to be a shell process — and reports it through the `ChildProcess` surface the subprocess service consumes. The command runs off the host's thread, `SIGKILL` terminates it whatever it is doing, and it reaches the VFS only by message (the host serves those frames). Worker platform executables preserve native-package protocols such as Landlock without replacing their JavaScript packages or coupling their implementations to `node:child_process`; ordinary commands use the package's evaluator and coreutils command table. The grammar is `@yarnpkg/parsers`' `parseShell`, while `execSync`/`fork` still refuse because they need a real process. -- **`lib/client.js` (page half)** — startup has two independent stages. `chooseWorkerHostSource({ image?, fixtureManifest? })` optionally owns the boot barrier and fixture manifest: without `preview-fixture` it waits at the source chooser, while a valid query selects directly; either path returns ordered overlays. `connectWorkerHost(worker, { image?, overlays? })` remains the public base-runtime connector; callers that skip the chooser get an empty overlay list. `apps/web` invokes both and supplies its statically bundled Worker. The opening `init` frame carries the base and ordered overlay URLs, the boot payload delivers the structured index-injection table, and `applyIndexInjections` executes it before the shell entry runs. Script preload rows are advisory and skipped because `/plugins` resources resolve only through the tunnel; `loadBundle` fetches each combo on first demand, embeds its tunnel-only source map as a Base64 data URL, and executes the script as a Blob. The tunnel also exposes fetch-shaped transport, the independent file-upload carrier, and the API client. Request frames preserve Blob bodies through structured clone and transfer `ReadableStream` ownership. The Host Worker streams both forms into the route, so neither browser thread creates a complete byte array for a generic-file upload. A Remote stream's uplink crosses as `stream-uplink-item` / `stream-uplink-end` frames into an unbounded `TunnelUplink` that drops items arriving after `end`: the page and its worker are one trusted same-origin boundary, so the Host inbox cap does not apply. +- **`src/shell/` (the worker's own process layer)** — a browser worker cannot fork, so `node:child_process` is not a stub but an implementation: `spawn` starts the command in its own Web Worker — this same bundle, told by its first frame to be a shell process — and reports it through the `ChildProcess` surface the subprocess service consumes. The command runs off the host's thread, `SIGKILL` terminates it whatever it is doing, and it reaches the VFS only by message (the host serves those frames). Worker platform executables preserve native-package protocols such as Landlock without replacing their JavaScript packages or coupling their implementations to `node:child_process`; `xdg-open` sends one VFS text file to the page's read-only viewer, so desktop-opener actions such as **Open configuration file** show the file in the page; ordinary commands use the package's evaluator and coreutils command table. The grammar is `@yarnpkg/parsers`' `parseShell`, while `execSync`/`fork` still refuse because they need a real process. +- **`lib/client.js` (page half)** — startup has two independent stages. `chooseWorkerHostSource({ image?, fixtureManifest? })` optionally owns the boot barrier and fixture manifest: without `preview-fixture` it waits at the source chooser, while a valid query selects directly; either path returns ordered overlays. `connectWorkerHost(worker, { image?, overlays? })` remains the public base-runtime connector; callers that skip the chooser get an empty overlay list. `apps/web` invokes both and supplies its statically bundled Worker. The opening `init` frame carries the base and ordered overlay URLs, the boot payload delivers the structured index-injection table, and `applyIndexInjections` executes it before the shell entry runs. Script preload rows are advisory and skipped because `/plugins` resources resolve only through the tunnel; `loadBundle` fetches each combo on first demand, embeds its tunnel-only source map as a Base64 data URL, and executes the script as a Blob. The worker's unsolicited `view-text` frame opens the page's modal text viewer. The tunnel also exposes fetch-shaped transport, the independent file-upload carrier, and the API client. Request frames preserve Blob bodies through structured clone and transfer `ReadableStream` ownership. The Host Worker streams both forms into the route, so neither browser thread creates a complete byte array for a generic-file upload. A Remote stream's uplink crosses as `stream-uplink-item` / `stream-uplink-end` frames into an unbounded `TunnelUplink` that drops items arriving after `end`: the page and its worker are one trusted same-origin boundary, so the Host inbox cap does not apply. Acceptance lives in `apps/web/tests/preview-boot.e2e.ts`, which serves the real built pages and drives the pre-boot chooser plus Worker activation in headless Chromium. The empty selection exercises first-run startup. The `vfs-example` overlay supplies ordinary workspace files and plaintext persistence artifacts for cold Workspace/Session discovery, tool presentation, subagent navigation, and history paging without a model request. The [packer](../webworker-packer/README.md) prepares current-generation successors from committed historical logs in Node; those source logs remain byte-identical in the overlay. The fixture generator owns the acceptance test's current projection cache. The chooser reserves WebFS as a separate user-authorized source; that provider does not read the built-in fixture. @@ -54,7 +54,7 @@ None; this package neither assembles nor sends a provider request. - **The worker composition writes plaintext session logs** (`compression: 'none'` boot patch): it carries no Zstandard codec, so exported logs are `.jsonl`, never `.jsonl.zstd`. - **`node:dns/promises`, `node:vm`, `node:net`, `node:sqlite`, `node:worker_threads` are structural stubs**: every call reports its refusal on the console and throws. Rows needing native DNS, a real process, or realm isolation cannot run here. - **Desktop product telemetry is unavailable**: `got.post` reports an explicit worker-host refusal. Got and its Node HTTP dependencies are excluded from the browser image; Desktop reporting remains disabled in the preview composition. -- **Host package commands are unavailable**: `execa` reports an explicit worker-host refusal; the preview cannot run pnpm or install native dependencies. +- **Host package commands are unavailable**: `execa` reports an explicit worker-host refusal; the preview cannot run pnpm, install plugins, or install native dependencies. - **PTC Node programs are unavailable**: the process shim exposes `/dsh/bin/node` as its executable identity so the provider can activate, but the Worker has neither a Node executable nor `stripTypeScriptTypes`. Program execution fails before launching a child. - **Office conversion requires a Node Host**: the LibreOffice kit replacement rejects converter creation with `unavailable`. The browser image excludes the kit and its engine dependencies; the Office preview reports that conversion is unavailable. - **Filesystem watchers observe only the mounted VFS**: image seeding is silent and the VFS has no symlinks or external writers. `persistent`, `ref()`, and `unref()` preserve the Node API but cannot control a dedicated Worker's lifetime because browsers expose no ref-counted event loop. diff --git a/packages/experimental/webworker-runtime/README.zh.md b/packages/experimental/webworker-runtime/README.zh.md index f83f1c4e55..43efcf7227 100644 --- a/packages/experimental/webworker-runtime/README.zh.md +++ b/packages/experimental/webworker-runtime/README.zh.md @@ -27,10 +27,10 @@ kind: "package-library" 一条 tsdown 管线产出三个构建产物;另有一层由源码维护的进程实现: -- **`lib/index.js`(装配库)**——`createWorkerHost`/`startWorkerHost` 挂载基础镜像和按序排列的数据 overlays(`storage/`)、安装模块加载器(`module-system/`)与 `process` shim、经镜像自带的 `dsh-app-boot` 启动插件树,并把服务 seam 交给隧道。Overlay 只能替换 `home/` 与 `workspace/` 下的文件,不能替换基础 manifest、配置或模块。镜像布局契约(`image-layout.ts`:虚拟根、config/manifest 路径、空目录、`lowered` 包装契约门)与 packer 共享。Worker 为 `home/profiles/preview/` 下可编辑的 profile 提供 `profileContext`:打包行初始化其插入 patch,ConfigEditor 将覆盖值持久化到该 VFS patch。ConfigEditor 协调每次写入,Loader 提交实时值;包管理和 Node 模块 HMR 保持禁用。部署 overlay 保持 JSONL 会话日志为明文,preset 根指向镜像内 `config/agent-presets`。配置修改在当前 Worker 生命周期内有效。预设声明包含在组合后的配置中。 +- **`lib/index.js`(装配库)**——`createWorkerHost`/`startWorkerHost` 挂载基础镜像和按序排列的数据 overlays(`storage/`)、安装模块加载器(`module-system/`)与 `process` shim、经镜像自带的 `dsh-app-boot` 启动插件树,并把服务 seam 交给隧道。Overlay 只能替换 `home/` 与 `workspace/` 下的文件,不能替换基础 manifest、配置或模块。镜像布局契约(`image-layout.ts`:虚拟根、config/manifest 路径、空目录、`lowered` 包装契约门)与 packer 共享。Worker 为 `home/profiles/preview/` 下可编辑的 profile 提供 `profileContext`:打包行初始化其插入 patch,ConfigEditor 将覆盖值持久化到该 VFS patch。ConfigEditor 协调每次写入,Loader 提交实时值。Worker 在隧道开始服务后提交 `appReady`,因此 HMR 可以加载并排队处理 profile 配置变更;`process` shim 不报告入口脚本,因此 HMR 不做 Node 模块缓存分类。插件管理可以加载并编辑 profile,其包操作会因 `execa` 拒绝而失败。部署 overlay 保持 JSONL 会话日志为明文,preset 根指向镜像内 `config/agent-presets`。配置修改在当前 Worker 生命周期内有效。预设声明包含在组合后的配置中。 - **`lib/worker.js`(worker 束)**——装配库加本包的 Node 兼容层,合成一个自含 ES module。模块代理表(`module-proxies.ts`)是唯一平台叉口:`node:*` 内建走 VFS、隧道和浏览器原语,浏览器做不到的走结构化 stub(调用即在 console 报错并抛出),native/binary 包则替换执行后端。`node:module` 在镜像 package 根之上提供 `createRequire().resolve` 与 `.resolve.paths()`,使未修改的包无需执行目标模块即可发现 manifest。`node:util` 的 JSON 数据比较器检查可枚举的自有属性,使 `__proto__` 等记录字段的准入判定与 Node 一致。全局 `process` shim 带有包括 `title` 在内的 Node 环境识别字段,避免 Worker 执行误入仅适用于 DOM 的分支。pack 期解析器会把名称静态可知的模块请求报告给 packer 的可达性遍历,其中包括通过 `node:module` 或 `module` 具名导入在模块作用域直接发起的 `createRequire(import.meta.url)('pkg')` 调用。保存、经 CommonJS 获取或另设基准的 `createRequire` 调用需要镜像入口种子。VFS mutation 驱动 `node:fs` 的 callback、polling 和 promise watcher;打开的 descriptor 在 rename、replacement 和 unlink 后仍保留文件身份与访问模式,只要文件名仍指向该文件,`FileHandle.stat({ bigint: true })` 报告的 device 与 inode 身份就与路径 stat 相同,`FileHandle.chmod()` 则更新打开文件身份的权限;`readable-stream` 提供 `node:stream/promises` 的完成等待与管道方法,以及文件流以及 Chokidar、readdirp 等未修改镜像包所用的流状态机。AsyncLocalStorage 经 pack 时降低注入的 snapshot/restore 面在 `await` 间携带同步栈因果。worker 不带编译器:packer 未降低的镜像在挂载时被拒。回调式 `fs.realpath.native` 别名在已挂载的 VFS 内解析路径,并支持 `util.promisify`。 -- **`src/shell/`(worker 自己的进程层)**——浏览器 worker 无法 fork,所以 `node:child_process` 不是 stub 而是实现:`spawn` 把命令放进它自己的 Web Worker——就是这同一个束,由首帧告诉它「你是 shell 进程」——并以 subprocess 服务消费的 `ChildProcess` 面报告结果。命令不占宿主线程,`SIGKILL` 不管它在干什么都能终止它,而它只能靠消息触达 VFS(由宿主应答这些帧)。Worker 平台 executable 在不替换 JavaScript 包、也不把具体实现耦合进 `node:child_process` 的情况下保持 Landlock 等 native 包协议;普通命令使用本包的求值器与 coreutils 命令表。语法来自 `@yarnpkg/parsers` 的 `parseShell`,而 `execSync`/`fork` 依然拒绝,因为它们需要真进程。 -- **`lib/client.js`(页面半)**——启动分为相互独立的两段。`chooseWorkerHostSource({ image?, fixtureManifest? })` 可选地拥有 boot barrier 与 fixture manifest:没有 `preview-fixture` 时停在来源选择面板,合法 query 则直接选择;两条路径都返回按序排列的 overlays。`connectWorkerHost(worker, { image?, overlays? })` 仍是公开的基础运行态连接器;调用方跳过选择器时 overlay 列表为空。`apps/web` 调用这两段并提供静态打包的 Worker。开局 `init` 帧携带基础镜像与按序排列的 overlay URL,boot 载荷送达结构化 index 注入表,`applyIndexInjections` 在壳入口运行前逐行执行。脚本 preload 行只是提示,因此会被跳过:`/plugins` 资源只能经 tunnel 解析,`loadBundle` 会在首次需要时获取 combo、把仅 tunnel 可达的 sourcemap 内嵌为 Base64 data URL,再以 Blob 执行脚本。Tunnel 还暴露 fetch 形式的传输、独立文件上传载体和 API 客户端。请求帧通过结构化克隆保留 Blob 请求体,并转移 `ReadableStream` 的所有权。Host Worker 将两种请求体都逐块送入路由,因此通用文件上传不会在任何浏览器线程创建完整字节数组。Remote 流的上行以 `stream-uplink-item` / `stream-uplink-end` 帧进入没有上限的 `TunnelUplink`,`end` 之后到达的项被丢弃:页面与其 worker 是同一个可信的同源边界,Host 的 inbox 上限在此不适用。 +- **`src/shell/`(worker 自己的进程层)**——浏览器 worker 无法 fork,所以 `node:child_process` 不是 stub 而是实现:`spawn` 把命令放进它自己的 Web Worker——就是这同一个束,由首帧告诉它「你是 shell 进程」——并以 subprocess 服务消费的 `ChildProcess` 面报告结果。命令不占宿主线程,`SIGKILL` 不管它在干什么都能终止它,而它只能靠消息触达 VFS(由宿主应答这些帧)。Worker 平台 executable 在不替换 JavaScript 包、也不把具体实现耦合进 `node:child_process` 的情况下保持 Landlock 等 native 包协议;`xdg-open` 把一个 VFS 文本文件交给页面的只读查看器,因此 **打开配置文件** 等桌面打开操作会在页面内显示该文件;普通命令使用本包的求值器与 coreutils 命令表。语法来自 `@yarnpkg/parsers` 的 `parseShell`,而 `execSync`/`fork` 依然拒绝,因为它们需要真进程。 +- **`lib/client.js`(页面半)**——启动分为相互独立的两段。`chooseWorkerHostSource({ image?, fixtureManifest? })` 可选地拥有 boot barrier 与 fixture manifest:没有 `preview-fixture` 时停在来源选择面板,合法 query 则直接选择;两条路径都返回按序排列的 overlays。`connectWorkerHost(worker, { image?, overlays? })` 仍是公开的基础运行态连接器;调用方跳过选择器时 overlay 列表为空。`apps/web` 调用这两段并提供静态打包的 Worker。开局 `init` 帧携带基础镜像与按序排列的 overlay URL,boot 载荷送达结构化 index 注入表,`applyIndexInjections` 在壳入口运行前逐行执行。脚本 preload 行只是提示,因此会被跳过:`/plugins` 资源只能经 tunnel 解析,`loadBundle` 会在首次需要时获取 combo、把仅 tunnel 可达的 sourcemap 内嵌为 Base64 data URL,再以 Blob 执行脚本。worker 主动发出的 `view-text` 帧会打开页面的模态文本查看器。Tunnel 还暴露 fetch 形式的传输、独立文件上传载体和 API 客户端。请求帧通过结构化克隆保留 Blob 请求体,并转移 `ReadableStream` 的所有权。Host Worker 将两种请求体都逐块送入路由,因此通用文件上传不会在任何浏览器线程创建完整字节数组。Remote 流的上行以 `stream-uplink-item` / `stream-uplink-end` 帧进入没有上限的 `TunnelUplink`,`end` 之后到达的项被丢弃:页面与其 worker 是同一个可信的同源边界,Host 的 inbox 上限在此不适用。 验收在 `apps/web/tests/preview-boot.e2e.ts`:静态服务真实构建页面,在 headless Chromium 里驱动 pre-boot 选择面板与 Worker 激活。空白选择验证首次启动;`vfs-example` overlay 提供普通 workspace 文件与明文 persistence 产物,无需模型请求即可验证 Workspace/Session 冷发现、工具呈现、subagent 导航和历史分页。[打包器](../webworker-packer/README.zh.md)在 Node 中根据已提交的历史日志准备当前代后继文件;这些源日志在 overlay 中逐字节保持不变。fixture 生成器负责验收测试的当前投影缓存。选择面板为 WebFS 保留独立的用户授权来源;该 provider 不读取内置 fixture。 @@ -54,7 +54,7 @@ kind: "package-library" - **worker 组合写明文会话日志**(`compression: 'none'` boot patch):不带 Zstandard 编解码器,导出日志是 `.jsonl`,不会是 `.jsonl.zstd`。 - **`node:dns/promises`、`node:vm`、`node:net`、`node:sqlite`、`node:worker_threads` 是结构化 stub**:每次调用在 console 报告拒绝并抛出。需要原生 DNS、真进程或真 realm 隔离的行在此无法运行。 - **桌面产品埋点不可用**:`got.post` 明确报告 worker host 不支持该调用。浏览器镜像不包含 Got 及其 Node HTTP 依赖;预览组合不启用桌面上报。 -- **宿主包管理命令不可用**:`execa` 明确报告 worker host 不支持该调用;预览无法运行 pnpm 或安装原生依赖。 +- **宿主包管理命令不可用**:`execa` 明确报告 worker host 不支持该调用;预览无法运行 pnpm、安装插件或安装原生依赖。 - **PTC Node 程序不可用**:process shim 用 `/dsh/bin/node` 表示可执行文件身份,使 provider 能够激活,但 Worker 既没有 Node 可执行文件,也没有 `stripTypeScriptTypes`。程序执行会在启动子进程前失败。 - **Office 转换需要 Node Host**:LibreOffice kit 的替代模块以 `unavailable` 拒绝创建转换器。浏览器镜像排除 kit 及其引擎依赖;Office 预览提示转换不可用。 - **文件 watcher 只能观察已挂载的 VFS**:镜像 seed 不产生事件,VFS 也没有符号链接或外部写入方。`persistent`、`ref()` 和 `unref()` 保留 Node API,但浏览器没有引用计数事件循环,因此这些接口不能控制 dedicated Worker 的生存期。 diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json index 558d033116..edf57ba68a 100644 --- a/packages/experimental/webworker-runtime/package.json +++ b/packages/experimental/webworker-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-experimental-webworker-runtime", "description": "Browser-only harness runtime: in-memory VFS, module transform and loader, postMessage tunnel, and the dedicated Web Worker assembly, with the Node-compatibility layer that lets the host tree run unchanged", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/experimental/webworker-runtime/src/client/client.ts b/packages/experimental/webworker-runtime/src/client/client.ts index e6a2a18537..3d0cfe1ac2 100644 --- a/packages/experimental/webworker-runtime/src/client/client.ts +++ b/packages/experimental/webworker-runtime/src/client/client.ts @@ -3,6 +3,7 @@ * turns fetch-shaped calls into `req` frames and rebuilds Responses from the * worker's `res` / `res-head`+`res-chunk`+`res-end` frames, so every consumer * (boot payload, bundle transport, ApiClient, Typert RPC) speaks plain HTTP. + * The worker's unsolicited `view-text` frame opens the page's text viewer. */ import type { IndexInjection } from '@deepseek-ai/dsh-host-webserver' @@ -18,6 +19,7 @@ import type { TunnelStreamItemFrame, TunnelStreamOpenFrame, } from '../transport/frames.ts' +import { showTextViewer } from './text-viewer.ts' /** Boot payload of the tunnel bootstrap route. */ export interface BootPayload { @@ -544,6 +546,10 @@ export class WorkerTunnel { controller.error(reason) return } + case 'view-text': { + showTextViewer(frame.path, frame.text) + return + } case 'stream-item': case 'stream-end': case 'stream-error': { diff --git a/packages/experimental/webworker-runtime/src/client/text-viewer.ts b/packages/experimental/webworker-runtime/src/client/text-viewer.ts new file mode 100644 index 0000000000..a0485d0d4e --- /dev/null +++ b/packages/experimental/webworker-runtime/src/client/text-viewer.ts @@ -0,0 +1,88 @@ +/** Read-only text viewer the page shows when the worker's `xdg-open` names a VFS file. */ + +const VIEWER_STYLE = ` + [data-preview-text-viewer] { + width: min(960px, calc(100vw - 32px)); + height: min(720px, calc(100dvh - 32px)); + padding: 0; + box-sizing: border-box; + border: 1px solid var(--dsw-alias-border-l2, rgb(0 0 0 / 10%)); + border-radius: var(--dsw-radius-lg, 16px); + color: var(--dsw-alias-label-primary, #0f1115); + background: var(--dsw-alias-bg-base, #fff); + font-family: var(--dsw-font-family, inherit); + } + [data-preview-text-viewer][open] { display: flex; flex-direction: column; } + [data-preview-text-viewer]::backdrop { background: rgb(0 0 0 / 32%); } + [data-preview-text-viewer] header { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid var(--dsw-alias-border-l1, rgb(0 0 0 / 6%)); + } + [data-preview-text-viewer] h2 { + flex: 1; + min-width: 0; + margin: 0; + overflow: hidden; + font-size: 14px; + line-height: 22px; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; + } + [data-preview-text-viewer] button { + height: 28px; + padding: 0 12px; + border: 1px solid var(--dsw-alias-border-l2, rgb(0 0 0 / 10%)); + border-radius: var(--dsw-radius-sm, 8px); + color: inherit; + background: transparent; + font: inherit; + cursor: pointer; + } + [data-preview-text-viewer] pre { + flex: 1; + margin: 0; + padding: 16px; + overflow: auto; + background: var(--dsw-alias-markdown-code-block, transparent); + font: 13px/20px ui-monospace, SFMono-Regular, Menlo, monospace; + white-space: pre; + } +` + +/** + * Show one file in the page's modal viewer, replacing any file it already shows. + * @param path - Absolute VFS path used as the title. + * @param text - File contents, rendered as plain text. + */ +export function showTextViewer(path: string, text: string): void { + let dialog = document.querySelector('dialog[data-preview-text-viewer]') + if (dialog === null) { + const style = document.createElement('style') + style.textContent = VIEWER_STYLE + document.head.append(style) + dialog = document.createElement('dialog') + dialog.dataset.previewTextViewer = '' + const header = document.createElement('header') + const title = document.createElement('h2') + const close = document.createElement('button') + close.type = 'button' + close.textContent = 'Close' + const shown = dialog + close.addEventListener('click', () => { shown.close() }) + // The app's modal layers own Escape and Tab through document listeners; + // keys pressed in this top-layer dialog belong to its native handling. + dialog.addEventListener('keydown', (event) => { event.stopPropagation() }) + header.append(title, close) + dialog.append(header, document.createElement('pre')) + document.body.append(dialog) + } + const title = dialog.querySelector('h2') as HTMLHeadingElement + title.textContent = path + title.title = path + ;(dialog.querySelector('pre') as HTMLPreElement).textContent = text + if (!dialog.open) dialog.showModal() +} diff --git a/packages/experimental/webworker-runtime/src/node/globals/process.ts b/packages/experimental/webworker-runtime/src/node/globals/process.ts index cd06fcc084..e3e0d1e7e3 100644 --- a/packages/experimental/webworker-runtime/src/node/globals/process.ts +++ b/packages/experimental/webworker-runtime/src/node/globals/process.ts @@ -9,13 +9,16 @@ import { requireActiveModuleLoader } from '../../module-system/module-loader.ts' import { processAlive, signalProcess } from '../process-table.ts' +/** Virtual executable identity; the worker has no Node binary behind it. */ +const EXEC_PATH = '/dsh/bin/node' + /** Construction inputs for {@link installProcessGlobal}. */ export interface ProcessShimOptions { /** Virtual root reported by `cwd()`. */ readonly cwd: string /** Environment the tree reads; `DSH_HOME` belongs here. */ readonly env: Readonly> - /** Argument vector reported to the tree. */ + /** Argument vector reported to the tree; defaults to the executable alone, since no entry script exists. */ readonly argv?: readonly string[] } @@ -89,9 +92,9 @@ export function installProcessGlobal(options: ProcessShimOptions): ProcessShim { } const shim: ProcessShim = { env: { ...options.env }, - argv: [...(options.argv ?? ['node', 'dsh-webworker'])], + argv: [...(options.argv ?? [EXEC_PATH])], execArgv: [], - execPath: '/dsh/bin/node', + execPath: EXEC_PATH, title: 'dsh-webworker', platform: 'linux', arch: 'x64', diff --git a/packages/experimental/webworker-runtime/src/shell/process/virtual-executables.ts b/packages/experimental/webworker-runtime/src/shell/process/virtual-executables.ts index 6f04cf1865..9649e1dd7d 100644 --- a/packages/experimental/webworker-runtime/src/shell/process/virtual-executables.ts +++ b/packages/experimental/webworker-runtime/src/shell/process/virtual-executables.ts @@ -2,6 +2,7 @@ import { basename } from '../../module-system/posix-path.ts' import type { ShellFileSystem } from '../types.ts' import { LANDLOCK_EXECUTABLE } from './landlock.ts' +import { XDG_OPEN_EXECUTABLE } from './xdg-open.ts' /** Completed virtual executable invocation. */ export interface VirtualExecutableExit { @@ -49,6 +50,7 @@ export interface VirtualExecutable { const EXECUTABLES: ReadonlyMap = new Map([ [LANDLOCK_EXECUTABLE.name, LANDLOCK_EXECUTABLE], + [XDG_OPEN_EXECUTABLE.name, XDG_OPEN_EXECUTABLE], ]) /** diff --git a/packages/experimental/webworker-runtime/src/shell/process/xdg-open.ts b/packages/experimental/webworker-runtime/src/shell/process/xdg-open.ts new file mode 100644 index 0000000000..c3cf68e34b --- /dev/null +++ b/packages/experimental/webworker-runtime/src/shell/process/xdg-open.ts @@ -0,0 +1,46 @@ +/** + * The Worker's `xdg-open`: hands one VFS text file to the page's read-only + * viewer, the only desktop a preview has. Exit statuses follow xdg-open(1): + * 1 usage, 2 missing file, 3 no viewer for the target, 4 action failed. + */ +import { resolve } from '../../module-system/posix-path.ts' +import type { VirtualExecutable, VirtualExecutableExit } from './virtual-executables.ts' + +/** Page-side display of one text file. */ +export type TextViewer = (path: string, text: string) => void + +let viewer: TextViewer | undefined + +/** + * Install the page viewer `xdg-open` reports to; the worker host calls this once its tunnel exists. + * @param next - Viewer that forwards to the page. + */ +export function setTextViewer(next: TextViewer): void { + viewer = next +} + +function exit(exitCode: number, stderr = ''): VirtualExecutableExit { + return { kind: 'exit', exitCode, stdout: '', stderr } +} + +/** Virtual executable replacing the desktop opener with the page's text viewer. */ +export const XDG_OPEN_EXECUTABLE: VirtualExecutable = { + name: 'xdg-open', + async prepare(args, context) { + if (args.length !== 1) return exit(1, 'xdg-open: usage: xdg-open { file | URL }\n') + const path = resolve(context.cwd, args[0] as string) + const stats = await context.filesystem.stat(path) + if (stats === undefined) return exit(2, `xdg-open: file '${path}' does not exist\n`) + if (stats.directory) return exit(3, `xdg-open: no viewer for directory '${path}'\n`) + if (viewer === undefined) return exit(3, 'xdg-open: no page viewer is attached\n') + try { + viewer(path, await context.filesystem.readText(path)) + } catch (error) { + return exit(4, `xdg-open: ${error instanceof Error ? error.message : String(error)}\n`) + } + return exit(0) + }, + runSync() { + return { kind: 'asynchronous' } + }, +} diff --git a/packages/experimental/webworker-runtime/src/transport/frames.ts b/packages/experimental/webworker-runtime/src/transport/frames.ts index 744338be1d..adddcbf0f1 100644 --- a/packages/experimental/webworker-runtime/src/transport/frames.ts +++ b/packages/experimental/webworker-runtime/src/transport/frames.ts @@ -133,8 +133,17 @@ export interface TunnelStreamErrorFrame { } } +/** Worker request to show one VFS text file in the page's read-only viewer; no reply follows. */ +export interface TunnelViewTextFrame { + readonly t: 'view-text' + /** Absolute VFS path, shown as the viewer title. */ + readonly path: string + readonly text: string +} + /** Frames the worker emits. */ export type TunnelOutboundFrame = + | TunnelViewTextFrame | TunnelResponseFrame | TunnelResponseHeadFrame | TunnelResponseChunkFrame diff --git a/packages/experimental/webworker-runtime/src/transport/tunnel.ts b/packages/experimental/webworker-runtime/src/transport/tunnel.ts index b94f927a9d..f9230df552 100644 --- a/packages/experimental/webworker-runtime/src/transport/tunnel.ts +++ b/packages/experimental/webworker-runtime/src/transport/tunnel.ts @@ -302,6 +302,15 @@ export class TunnelServer { for (const frame of this.queue.splice(0)) this.refuse(frame, message) } + /** + * Ask the page to show one text file in its read-only viewer. + * @param path - Absolute VFS path the viewer names. + * @param text - File contents. + */ + viewText(path: string, text: string): void { + this.send({ t: 'view-text', path, text }) + } + private send(frame: TunnelOutboundFrame, transfer?: Transferable[]): void { this.port.postMessage(frame, transfer) } diff --git a/packages/experimental/webworker-runtime/src/worker-host.ts b/packages/experimental/webworker-runtime/src/worker-host.ts index 6dfa7097b0..d30eaefed5 100644 --- a/packages/experimental/webworker-runtime/src/worker-host.ts +++ b/packages/experimental/webworker-runtime/src/worker-host.ts @@ -33,6 +33,7 @@ import { TunnelServer, type TunnelPort } from './transport/tunnel.ts' import { inflateImage, inflateImageStream } from './storage/image-gzip.ts' import { loadVfsImage, loadVfsOverlay, MemoryVfs } from './storage/memory.ts' import { setActiveVfs } from './storage/active.ts' +import { setTextViewer } from './shell/process/xdg-open.ts' import { DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_HOME_DIRECTORY, IMAGE_MANIFEST_PATH, LOWERING_VERSION, @@ -176,6 +177,7 @@ export function createWorkerHost(options: WorkerHostOptions): WorkerHost { ...options.privilegedMethods === undefined ? {} : { privilegedMethods: options.privilegedMethods }, ...options.unaryApiLane === undefined ? {} : { unaryApiLane: options.unaryApiLane }, }) + setTextViewer((path, text) => { tunnel.viewText(path, text) }) let vfs: MemoryVfs | undefined let modules: WorkerModuleLoader | undefined @@ -228,8 +230,14 @@ export function createWorkerHost(options: WorkerHostOptions): WorkerHost { ): Promise } const cmdline = require('@deepseek-ai/dsh-cmdline') as { - provideCmdline(ctx: unknown, host: { args: readonly string[]; exit: (code: number) => void }): void + provideCmdline(ctx: unknown, host: { + args: readonly string[] + exit: (code: number) => void + ready: { onReady(listener: () => void): () => void } + }): void } + // Resolved once the tunnel serves; a failed start never resolves it. + const started = Promise.withResolvers() const { patches, profile } = bootPatches(loader, mounted, configPath, root) const profileConfig = join(profile.dir, 'cordis.yml') @@ -242,6 +250,13 @@ export function createWorkerHost(options: WorkerHostOptions): WorkerHost { cmdline.provideCmdline(hostCtx, { args: [...(options.cmdlineArgs ?? ['--host', '127.0.0.1', '--port', String(port), '--no-open'])], exit: (code: number) => { console.warn(`webworker host: tree requested exit(${String(code)})`) }, + ready: { + onReady: (listener) => { + let pending = true + void started.promise.then(() => { if (pending) listener() }) + return () => { pending = false } + }, + }, }) }) context = ctx @@ -264,6 +279,7 @@ export function createWorkerHost(options: WorkerHostOptions): WorkerHost { typertGateway.wireStream.open(endpoint, payload, uplink, undefined, signal), streamFailure: typertGateway.wireStream.failure, }) + started.resolve() } catch (reason) { tunnel.fail(reason) throw reason @@ -395,11 +411,6 @@ function bootPatches( : {}) as Record const patches: Array = [] - // The image has no package manager or Node module-reload cache. ConfigEditor - // reconciles writes directly; Loader commits accepted live values. - for (const id of ['hmr', 'plugin-manager']) { - if (find(rows, id) !== undefined) patches.push({ id, disabled: true }) - } // The worker carries no compression codec, and the VFS is in-memory anyway: // the JSONL backend's plaintext path is the composition's one legal encoding. const jsonl = find(rows, 'session-persistence-jsonl') diff --git a/packages/experimental/webworker-runtime/tests/client/text-viewer.spec.ts b/packages/experimental/webworker-runtime/tests/client/text-viewer.spec.ts new file mode 100644 index 0000000000..394693977d --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/client/text-viewer.spec.ts @@ -0,0 +1,38 @@ +// @vitest-environment jsdom + +import { beforeAll, expect, it } from 'vitest' +import { showTextViewer } from '../../src/client/text-viewer.ts' + +beforeAll(() => { + // jsdom lacks the modal dialog methods. + HTMLDialogElement.prototype.showModal = function showModal(this: HTMLDialogElement) { this.open = true } + HTMLDialogElement.prototype.close = function close(this: HTMLDialogElement) { this.open = false } +}) + +function viewer(): HTMLDialogElement { + return document.querySelector('dialog[data-preview-text-viewer]') as HTMLDialogElement +} + +it('shows the file as plain text, replaces it on reuse, and closes from its button', () => { + showTextViewer('/dsh/home/a.yml', 'first') + expect(viewer().open).toBe(true) + expect(viewer().querySelector('h2')?.textContent).toBe('/dsh/home/a.yml') + expect(viewer().querySelector('pre')?.innerHTML).toBe('<b>first</b>') + + showTextViewer('/dsh/home/b.yml', 'second') + expect(document.querySelectorAll('dialog[data-preview-text-viewer]')).toHaveLength(1) + expect(viewer().querySelector('pre')?.textContent).toBe('second') + + viewer().querySelector('button')?.click() + expect(viewer().open).toBe(false) +}) + +it('keeps keys pressed in the viewer from reaching document-level modal layers', () => { + showTextViewer('/dsh/home/a.yml', 'text') + let reached = false + const listener = (): void => { reached = true } + document.addEventListener('keydown', listener) + viewer().querySelector('button')?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) + document.removeEventListener('keydown', listener) + expect(reached).toBe(false) +}) diff --git a/packages/experimental/webworker-runtime/tests/node/process-shim.spec.ts b/packages/experimental/webworker-runtime/tests/node/process-shim.spec.ts index 34ef15811d..992df10f52 100644 --- a/packages/experimental/webworker-runtime/tests/node/process-shim.spec.ts +++ b/packages/experimental/webworker-runtime/tests/node/process-shim.spec.ts @@ -29,6 +29,8 @@ describe('process shim', () => { it('exposes an executable identity without enabling Node programs', () => { const shim = installProcessGlobal({ cwd: '/dsh', env: {} }) expect(shim.execPath).toBe('/dsh/bin/node') + // No entry script: consumers that read argv[1] as the main module find none. + expect(shim.argv).toEqual([shim.execPath]) expect(spawnSync(shim.execPath, ['--eval', 'throw new Error("must not execute")']).error?.code).toBe('ENOENT') }) diff --git a/packages/experimental/webworker-runtime/tests/shell/xdg-open.spec.ts b/packages/experimental/webworker-runtime/tests/shell/xdg-open.spec.ts new file mode 100644 index 0000000000..fd903bcc5e --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/shell/xdg-open.spec.ts @@ -0,0 +1,46 @@ +import { beforeEach, expect, it, vi } from 'vitest' +import { MemoryVfs } from '../../src/storage/memory.ts' +import { setActiveVfs } from '../../src/storage/active.ts' +import { hostFileSystem } from '../../src/shell/fs-access.ts' +import { spawn } from '../../src/node/builtin_modules/implemented/child_process.ts' +import { setTextViewer, XDG_OPEN_EXECUTABLE } from '../../src/shell/process/xdg-open.ts' + +const PROFILE = '/dsh/home/profiles/preview' + +beforeEach(() => { + const vfs = new MemoryVfs() + vfs.mkdirSync(PROFILE, { recursive: true }) + vfs.writeFileSync(`${PROFILE}/cordis.patch.yml`, '- insert: []\n') + setActiveVfs(vfs) +}) + +async function run(args: readonly string[]): Promise<{ code: number | null; stderr: string }> { + const child = spawn('xdg-open', args, { cwd: PROFILE }) + let stderr = '' + child.stderr?.on('data', (chunk: unknown) => { stderr += String(chunk) }) + const code = await new Promise((settle) => { child.on('close', (value: unknown) => { settle(value as number | null) }) }) + return { code, stderr } +} + +it('refuses with status 3 before a page viewer is attached', async () => { + vi.resetModules() + const fresh = await import('../../src/shell/process/xdg-open.ts') + const result = await fresh.XDG_OPEN_EXECUTABLE.prepare([`${PROFILE}/cordis.patch.yml`], { cwd: PROFILE, filesystem: hostFileSystem() }) + expect(result).toEqual({ kind: 'exit', exitCode: 3, stdout: '', stderr: 'xdg-open: no page viewer is attached\n' }) +}) + +it('hands a spawned relative path to the page viewer', async () => { + const viewer = vi.fn() + setTextViewer(viewer) + expect(await run(['cordis.patch.yml'])).toEqual({ code: 0, stderr: '' }) + expect(viewer).toHaveBeenCalledWith(`${PROFILE}/cordis.patch.yml`, '- insert: []\n') +}) + +it('reports usage, missing files, directories, and viewer failures with xdg-open statuses', async () => { + setTextViewer(() => { throw new Error('page closed') }) + expect(await run([])).toEqual({ code: 1, stderr: 'xdg-open: usage: xdg-open { file | URL }\n' }) + expect(await run(['missing.yml'])).toEqual({ code: 2, stderr: `xdg-open: file '${PROFILE}/missing.yml' does not exist\n` }) + expect(await run(['.'])).toEqual({ code: 3, stderr: `xdg-open: no viewer for directory '${PROFILE}'\n` }) + expect(await run(['cordis.patch.yml'])).toEqual({ code: 4, stderr: 'xdg-open: page closed\n' }) + expect(XDG_OPEN_EXECUTABLE.runSync(['cordis.patch.yml'])).toEqual({ kind: 'asynchronous' }) +}) diff --git a/packages/extensions/cordis-client-runner/package.json b/packages/extensions/cordis-client-runner/package.json index 02ed3fcd7b..36ea27e95a 100644 --- a/packages/extensions/cordis-client-runner/package.json +++ b/packages/extensions/cordis-client-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-client-runner", "description": "Browser half of dynamic dual-half plugin packages: event subscription, closure evaluation, guard facade, and loader entries", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index ef586cd268..5c8aa61316 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -2358,6 +2358,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ 'client-ui-permission-presets PermissionRow id \'permission\'', 'client-ui-settings-general DeveloperToolsRow id \'developer-tools\'', 'client-ui-settings-general CurrentVersionRow id \'current-version\'', + 'client-ui-settings-session-log UploadRow', 'client-ui-shortcuts ShortcutsRow id \'shortcuts\'', 'client-ui-theme AppearanceRow id \'appearance\'', 'client-ui-theme FontSizeRow id \'font-size\'', @@ -2805,6 +2806,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ 'client-ui-schedule ScheduleDeleteToast id \'schedule.delete-toast\'', 'client-ui-settings-account DesktopOnboardingEntry id \'desktop-onboarding\'', 'client-ui-settings-account AccountPlatformHost id \'account.platform-page\'', + 'client-ui-settings-session-log UploadToast id \'session-log-upload-toast\'', 'client-ui-shortcuts ShortcutReference id \'shortcuts\'', 'client-ui-workspace SessionRenameDialog id \'workspace.session-rename\'', 'client-ui-workspace SessionArchiveConfirmDialog id \'workspace.session-archive\'', diff --git a/packages/extensions/cordis-host-runner/package.json b/packages/extensions/cordis-host-runner/package.json index ba3431ffd5..385e2bf7c4 100644 --- a/packages/extensions/cordis-host-runner/package.json +++ b/packages/extensions/cordis-host-runner/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-cordis-host-runner", "description": "Dynamic package definition registry, host-half sandbox lifecycle, and invoke handler table for model-mounted dual-half packages", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/tool-cordis/package.json b/packages/extensions/tool-cordis/package.json index dc3a0ac5e5..744a771082 100644 --- a/packages/extensions/tool-cordis/package.json +++ b/packages/extensions/tool-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-cordis", "description": "Read-only runtime API inspection for Harness plugin development", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/extensions/ui-cordis/package.json b/packages/extensions/ui-cordis/package.json index 25b2f9237a..7b3db45e75 100644 --- a/packages/extensions/ui-cordis/package.json +++ b/packages/extensions/ui-cordis/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-ui-cordis", "description": "Cordis dynamic-plugin definition card: the keyed cordis_define tool row with its run/stop switch", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json index 1ca50f3bca..3ee6a77059 100644 --- a/packages/feedback/command-feedback/package.json +++ b/packages/feedback/command-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-feedback", "description": "Log-only session feedback: the record event, the sessionFeedback Host Remote, and the human-facing slash command", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/feedback/message-feedback/package.json b/packages/feedback/message-feedback/package.json index b35b911009..617a840b66 100644 --- a/packages/feedback/message-feedback/package.json +++ b/packages/feedback/message-feedback/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-message-feedback", "description": "Canonical Session-log ratings and notes for finalized assistant messages", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index 76ba730c66..93d3d2c52c 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-local", "description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-observation-policy/package.json b/packages/fs/fs-observation-policy/package.json index 01a9ea53a6..dc9f84f6ca 100644 --- a/packages/fs/fs-observation-policy/package.json +++ b/packages/fs/fs-observation-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-observation-policy", "description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs-sandbox/package.json b/packages/fs/fs-sandbox/package.json index 8ab811b3bc..bc261daf63 100644 --- a/packages/fs/fs-sandbox/package.json +++ b/packages/fs/fs-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-sandbox", "description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index 47ee79c760..239425f1e9 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs", "description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs-search/package.json b/packages/fs/tool-fs-search/package.json index ee00a243e2..04a5c78373 100644 --- a/packages/fs/tool-fs-search/package.json +++ b/packages/fs/tool-fs-search/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs-search", "description": "Model-facing filesystem discovery tools (glob, grep) backed by the packaged ripgrep binary (@vscode/ripgrep)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index 3b535e2f75..0df6c6996b 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-fs", "description": "Model-facing filesystem tools (read, write, edit) over the DeepSeek Harness filesystem seam (ctx.fs)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/fs/tool-str-replace-editor/package.json b/packages/fs/tool-str-replace-editor/package.json index f950d01fb4..8dca9bf976 100644 --- a/packages/fs/tool-str-replace-editor/package.json +++ b/packages/fs/tool-str-replace-editor/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-str-replace-editor", "description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/goal/command-goal/package.json b/packages/goal/command-goal/package.json index 44a1ffaf4f..da24ea4524 100644 --- a/packages/goal/command-goal/package.json +++ b/packages/goal/command-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-command-goal", "description": "Human-facing slash command for persisted same-session goals", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal-round-driver/package.json b/packages/goal/goal-round-driver/package.json index ee47dabd9a..89429a27ad 100644 --- a/packages/goal/goal-round-driver/package.json +++ b/packages/goal/goal-round-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal-round-driver", "description": "Race-fenced same-session goal-round driver", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/goal/goal/package.json b/packages/goal/goal/package.json index f59480801e..685d1209ad 100644 --- a/packages/goal/goal/package.json +++ b/packages/goal/goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-goal", "description": "Event-sourced same-session goal state and lifecycle service for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json index 8c1e0653a4..0a19ee1afc 100644 --- a/packages/goal/tool-goal/package.json +++ b/packages/goal/tool-goal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-goal", "description": "Model-facing same-session goal tools with execution-time authority checks", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/guard/repeat-tool-reminder/package.json b/packages/guard/repeat-tool-reminder/package.json index 2718d04fe3..08d9788606 100644 --- a/packages/guard/repeat-tool-reminder/package.json +++ b/packages/guard/repeat-tool-reminder/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-repeat-tool-reminder", "description": "Repeat-tool-call guard plugin: advisory reminders when an agent loops on identical tool calls", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/guard/timeout-policy/package.json b/packages/guard/timeout-policy/package.json index a6a4320ccb..77a78147d6 100644 --- a/packages/guard/timeout-policy/package.json +++ b/packages/guard/timeout-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-call-timeout-policy", "description": "Tool-call timeout policy: a tools/execute wrapper that arms a per-tool deadline on exec.signal and returns TOOL_TIMEOUT when it wins", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hook-protocol/package.json b/packages/hooks/hook-protocol/package.json index 7600b26a5c..9fb6216b64 100644 --- a/packages/hooks/hook-protocol/package.json +++ b/packages/hooks/hook-protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hook-protocol", "description": "Shared Claude Code / Codex hook wire protocol: matcher engine, stdin/exit-code/stdout codec, multi-hook merge, and hook/* session events", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hooks-claude-code/package.json b/packages/hooks/hooks-claude-code/package.json index fe100865d9..7ab584e6cb 100644 --- a/packages/hooks/hooks-claude-code/package.json +++ b/packages/hooks/hooks-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-claude-code", "description": "Bridge plugin: run a Claude Code hooks.json / settings hook config on the DeepSeek Harness interception seams", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/hooks/hooks-codex/package.json b/packages/hooks/hooks-codex/package.json index 8417e9c7aa..738a6c0e74 100644 --- a/packages/hooks/hooks-codex/package.json +++ b/packages/hooks/hooks-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-hooks-codex", "description": "Bridge plugin: run a Codex hooks.json hook config on the DeepSeek Harness interception seams", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-auto/package.json b/packages/host/directory-picker-auto/package.json index eed31877e4..074632b9fd 100644 --- a/packages/host/directory-picker-auto/package.json +++ b/packages/host/directory-picker-auto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-auto", "description": "Adaptive chooser of the directory-picker seam: resolves the host situation at boot and mounts the native or browse backend for the DeepSeek Harness web GUI host", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-browse/package.json b/packages/host/directory-picker-browse/package.json index 870d604ced..2f84cb6f5b 100644 --- a/packages/host/directory-picker-browse/package.json +++ b/packages/host/directory-picker-browse/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-browse", "description": "In-app browsing backend of the directory-picker seam (listing/creation primitives over the host filesystem)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker-native/package.json b/packages/host/directory-picker-native/package.json index 003e853ecf..aa0c367d15 100644 --- a/packages/host/directory-picker-native/package.json +++ b/packages/host/directory-picker-native/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker-native", "description": "Native-OS-chooser backend of the directory-picker seam for the DeepSeek Harness web GUI host", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/directory-picker/package.json b/packages/host/directory-picker/package.json index feb32e9466..6d09bb61f5 100644 --- a/packages/host/directory-picker/package.json +++ b/packages/host/directory-picker/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-directory-picker", "description": "Abstract workspace-directory picking seam (ctx.directoryPicker) for the DeepSeek Harness web GUI host", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/frontend-static/package.json b/packages/host/frontend-static/package.json index 6adff7528d..d8e9a48ad5 100644 --- a/packages/host/frontend-static/package.json +++ b/packages/host/frontend-static/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-frontend-static", "description": "SPA dist server for the Web shell: owns the webserver fallback seat, serving explicit index entries and static assets with traversal rejection and 404 misses", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/open-in-app/package.json b/packages/host/open-in-app/package.json index fd750e3467..7408392f17 100644 --- a/packages/host/open-in-app/package.json +++ b/packages/host/open-in-app/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-open-in-app", "description": "Host half of open-in-app: resolved application catalog, icons, and the launch endpoint as three webServer routes", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/plugin-inventory/package.json b/packages/host/plugin-inventory/package.json index 1f84a5c5a5..7afe923326 100644 --- a/packages/host/plugin-inventory/package.json +++ b/packages/host/plugin-inventory/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-plugin-inventory", "description": "Read-only Remote projection of current Cordis Loader plugin state", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/product-telemetry-otel/package.json b/packages/host/product-telemetry-otel/package.json index 6a5ba372cb..74717155a7 100644 --- a/packages/host/product-telemetry-otel/package.json +++ b/packages/host/product-telemetry-otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-product-telemetry-otel", "description": "Explicit product usage events exported through OpenTelemetry HTTP logs", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/host/webserver/package.json b/packages/host/webserver/package.json index 5755bc0b61..9502a73316 100644 --- a/packages/host/webserver/package.json +++ b/packages/host/webserver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-host-webserver", "description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/identity/anonymous-user-id/package.json b/packages/identity/anonymous-user-id/package.json index ce04b3b565..d6a490f7a0 100644 --- a/packages/identity/anonymous-user-id/package.json +++ b/packages/identity/anonymous-user-id/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-anonymous-user-id", "description": "Shared anonymous user identity for DeepSeek Harness telemetry and feedback correlation", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/commands/package.json b/packages/interaction/commands/package.json index 919397b35e..d959db6d02 100644 --- a/packages/interaction/commands/package.json +++ b/packages/interaction/commands/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-commands", "description": "Plugin-owned human command registry for DeepSeek Harness UIs", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/permission-presets/package.json b/packages/interaction/permission-presets/package.json index 176598b825..bf090f0e98 100644 --- a/packages/interaction/permission-presets/package.json +++ b/packages/interaction/permission-presets/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-permission-presets", "description": "User-facing permission presets (ctx.permissionPresets) for the DeepSeek Harness: one product-level Permissions select bundling the sandbox-mode and approval-policy knobs, written through to their own session events", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/tool-ask-user/package.json b/packages/interaction/tool-ask-user/package.json index fa01b16a1a..d7d5b14539 100644 --- a/packages/interaction/tool-ask-user/package.json +++ b/packages/interaction/tool-ask-user/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ask-user", "description": "Model-facing ask_user_question tool over the ctx.userQuestions seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-approval/package.json b/packages/interaction/user-approval/package.json index 935dceb2e4..b5a2393630 100644 --- a/packages/interaction/user-approval/package.json +++ b/packages/interaction/user-approval/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-approval", "description": "User-approval seam (ctx.approval) for the DeepSeek Harness: one-shot permission decisions dispatched to composed answerers over the approval/request waterfall, fail-closed by default", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/interaction/user-questions/package.json b/packages/interaction/user-questions/package.json index 7f240c4b79..18c3fe846e 100644 --- a/packages/interaction/user-questions/package.json +++ b/packages/interaction/user-questions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-user-questions", "description": "Abstract user-questions seam (ctx.userQuestions) for asking the human during agent runs", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs-local/package.json b/packages/jobs/jobs-local/package.json index ea9870a8b5..bb92baa161 100644 --- a/packages/jobs/jobs-local/package.json +++ b/packages/jobs/jobs-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs-local", "description": "Process-local implementation of the DeepSeek Harness background job registry seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/jobs/package.json b/packages/jobs/jobs/package.json index c5d6067236..5ecd85da67 100644 --- a/packages/jobs/jobs/package.json +++ b/packages/jobs/jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-jobs", "description": "Background job registry (ctx.jobs) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/jobs/tool-jobs/package.json b/packages/jobs/tool-jobs/package.json index 102e1f8f41..75e332f183 100644 --- a/packages/jobs/tool-jobs/package.json +++ b/packages/jobs/tool-jobs/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-jobs", "description": "Model-facing background job control tools (job_output, job_list, job_kill) over the ctx.jobs registry", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/deepseek-llm-api-extensions/package.json b/packages/llm/deepseek-llm-api-extensions/package.json index 0bcbbd73ec..0911981026 100644 --- a/packages/llm/deepseek-llm-api-extensions/package.json +++ b/packages/llm/deepseek-llm-api-extensions/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deepseek-llm-api-extensions", "description": "Additive request-field registry for the official DeepSeek LLM API adapter", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek-account/package.json b/packages/llm/llm-deepseek-account/package.json index 8ee22709c2..bfce46dd6f 100644 --- a/packages/llm/llm-deepseek-account/package.json +++ b/packages/llm/llm-deepseek-account/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek-account", "description": "DeepSeek account provider authentication and discovery", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek-api-key/package.json b/packages/llm/llm-deepseek-api-key/package.json index 010d315891..a76d9596ca 100644 --- a/packages/llm/llm-deepseek-api-key/package.json +++ b/packages/llm/llm-deepseek-api-key/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek-api-key", "description": "DeepSeek api-key provider authentication and discovery", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index e40284c246..694965e5e0 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-deepseek", "description": "DeepSeek Messages adapter", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index 6c41c662ed..e186f0c3b9 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-pi-ai", "description": "pi-ai-backed DeepSeek adapter for the DeepSeek Harness LLM seam (design-verification twin of dsh-llm-deepseek)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm-retry/package.json b/packages/llm/llm-retry/package.json index 1e052883fd..77c08d152a 100644 --- a/packages/llm/llm-retry/package.json +++ b/packages/llm/llm-retry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-retry", "description": "Provider-routed LLM request retry policy for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index 3f37e5c959..af39128dbf 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm", "description": "Provider-neutral LLM service interface for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/plugin-package-inventory-deepseek/package.json b/packages/llm/plugin-package-inventory-deepseek/package.json index deedeab829..c760f6cfb1 100644 --- a/packages/llm/plugin-package-inventory-deepseek/package.json +++ b/packages/llm/plugin-package-inventory-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-package-inventory-deepseek", "description": "Active Loader-backed plugin package inventory for official DeepSeek LLM API requests", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json index 2330e61a0d..dd65c9b1fe 100644 --- a/packages/llm/token-meter/package.json +++ b/packages/llm/token-meter/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-token-meter", "description": "Replay-aware token measurement service (ctx.tokenMeter) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp-stdio/package.json b/packages/lsp/lsp-stdio/package.json index 39e7cd9c92..e45d7f0ae2 100644 --- a/packages/lsp/lsp-stdio/package.json +++ b/packages/lsp/lsp-stdio/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp-stdio", "description": "Generic stdio language-server provider for the DeepSeek Harness LSP capability seam (ctx.lsp) — spawns configured servers, translates JSON-RPC, and serves transient-open goToDefinition/findReferences/goToImplementation/hover queries in the host filesystem namespace", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/lsp/package.json b/packages/lsp/lsp/package.json index 091e8ffbc7..24511d2c79 100644 --- a/packages/lsp/lsp/package.json +++ b/packages/lsp/lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lsp", "description": "Abstract LSP capability seam (ctx.lsp) for the DeepSeek Harness — language-server provider registry keyed by branded id and extension mapping, order-independent per-query selection, normalized definition/references/implementation/hover requests and results, and the LspError taxonomy", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/lsp/tool-lsp/package.json b/packages/lsp/tool-lsp/package.json index 8711c079a0..26482a9b54 100644 --- a/packages/lsp/tool-lsp/package.json +++ b/packages/lsp/tool-lsp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-lsp", "description": "Model-facing lsp tool over the DeepSeek Harness LSP capability seam (ctx.lsp) — one read-only tool with goToDefinition/findReferences/goToImplementation/hover operations, one-based UTF-16 cursor coordinates, bounded location rendering, and hover normalization", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/mcp/mcp-client/package.json b/packages/mcp/mcp-client/package.json index 18a9fe43b0..1461d58066 100644 --- a/packages/mcp/mcp-client/package.json +++ b/packages/mcp/mcp-client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-mcp-client", "description": "MCP client bridge: connects to MCP servers and registers their tools on ctx.tools", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/mcp/mcp-resources/package.json b/packages/mcp/mcp-resources/package.json index 59a2c1af39..4448d74bc2 100644 --- a/packages/mcp/mcp-resources/package.json +++ b/packages/mcp/mcp-resources/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-mcp-resources", "description": "Scoped MCP resource discovery and reading through shared model tools", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json index 0cc8836c97..0e5060920a 100644 --- a/packages/plan/plan-mode/package.json +++ b/packages/plan/plan-mode/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plan-mode", "description": "Logged per-agent plan mode with deployment guidance, a direct slash command, and a user-reviewed exit", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/preset/agent-preset-registry/package.json b/packages/preset/agent-preset-registry/package.json index 5fe50a6a84..5b823ff82d 100644 --- a/packages/preset/agent-preset-registry/package.json +++ b/packages/preset/agent-preset-registry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-preset-registry", "description": "Declarative Agent preset registry and profile-backed editing", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/preset/agent-preset/package.json b/packages/preset/agent-preset/package.json index 3ce3697829..9c044e6984 100644 --- a/packages/preset/agent-preset/package.json +++ b/packages/preset/agent-preset/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-preset", "description": "Declare an Agent capability composition in Cordis YAML", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md b/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md index 7ccf6a43ce..73b8560f80 100644 --- a/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md +++ b/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md @@ -96,6 +96,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo | `@deepseek-ai/dsh-client-ui-settings-models` | yes | Models settings and shared product-onboarding dialogs over existing settings and credential joins | | `@deepseek-ai/dsh-client-ui-settings-plugin-inventory` | no | Read-only Cordis Loader inventory tab in Web Plugins settings | | `@deepseek-ai/dsh-client-ui-settings-plugins` | no | Built-in plugins settings section for the dsh web client: the Settings navigation entry and the tab chrome feature-owned tabs register into | +| `@deepseek-ai/dsh-client-ui-settings-session-log` | no | General settings control for Session-log upload with DeepSeek API requests | | `@deepseek-ai/dsh-client-ui-settings-shell` | no | Settings page of the shell executor on the dsh web client's Plugins page: the command timeout and the per-stream output cap of the shell namespace | | `@deepseek-ai/dsh-client-ui-settings-subagent` | no | Settings page of Subagent delegation on the dsh web client's Plugins page: recursion depth, parallel capacity, and the models agents may choose for subagents | | `@deepseek-ai/dsh-client-ui-settings-web-search` | no | Settings page of the DeepSeek web-search provider on the dsh web client's Plugins page: its API key, endpoint, and per-request search budget | diff --git a/packages/preset/persona/package.json b/packages/preset/persona/package.json index 90b41c117a..b5aeb2ebde 100644 --- a/packages/preset/persona/package.json +++ b/packages/preset/persona/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-persona", "description": "Composition-authored deployment persona section for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/ptc-runtime/ptc-runtime-node/package.json b/packages/ptc-runtime/ptc-runtime-node/package.json index 3d39209d41..1ad2a9e94c 100644 --- a/packages/ptc-runtime/ptc-runtime-node/package.json +++ b/packages/ptc-runtime/ptc-runtime-node/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ptc-runtime-node", "description": "Sandboxed Node process implementation of the DeepSeek Harness PTC execution capability", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/ptc-runtime/ptc-runtime/package.json b/packages/ptc-runtime/ptc-runtime/package.json index bc48cdba39..166f6c55ad 100644 --- a/packages/ptc-runtime/ptc-runtime/package.json +++ b/packages/ptc-runtime/ptc-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ptc-runtime", "description": "Abstract PTC execution seam (ctx.ptcRuntime) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/runtime-diagnostics/invariants/package.json b/packages/runtime-diagnostics/invariants/package.json index fedcd2f7bb..9ad5419b39 100644 --- a/packages/runtime-diagnostics/invariants/package.json +++ b/packages/runtime-diagnostics/invariants/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-invariants", "description": "Registry service for package-owned DeepSeek Harness runtime invariants", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-local/README.i18n.yaml b/packages/sandbox/sandbox-local/README.i18n.yaml index 7e7b0ba744..ea1d108692 100644 --- a/packages/sandbox/sandbox-local/README.i18n.yaml +++ b/packages/sandbox/sandbox-local/README.i18n.yaml @@ -36,8 +36,8 @@ en: f0c44248c2ca6acc zh: 554e9f706671d222 /deepseek-ai-dsh-sandbox-local/understand-the-implementation/platform-profiles: - en: e87ee1858b20e643 - zh: fabdc6db8264dc11 + en: eae0f9047f297f42 + zh: ad76290c0e4d7722 /deepseek-ai-dsh-sandbox-local/understand-the-implementation/denial-and-runner-failure-dialects: en: 62adfc9229c656cc zh: 11b1534b6d61e5de diff --git a/packages/sandbox/sandbox-local/README.md b/packages/sandbox/sandbox-local/README.md index 278f2e5810..a2e0965af6 100644 --- a/packages/sandbox/sandbox-local/README.md +++ b/packages/sandbox/sandbox-local/README.md @@ -80,6 +80,8 @@ The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow The Windows rung keeps one deterministic write SID and standing ACE per workspace, while every live session/workspace pair gets a random private temp directory with a distinct SID and revocable ACE — sessions sharing a workspace share its intended write authority without inheriting one another's temp authority. A fresh provider always chooses a new temp path and SID, so crash residue cannot block or authorize a resumed session. The rung reports `partial` enforcement because NTFS hard links alias one file object across paths, reads stay unconfined, and a tree another AppContainer tool has ACL'd with a package SID is unreadable to the Low-integrity child. +With the built-in Windows runner and a skill registry, this provider registers the [ACL diagnosis skill](../sandbox-windows-acl/README.md#failures-and-recovery). An operator-supplied `runnerCommand` does not register it; provider disposal removes the skill and its extracted resources. + When the built ACL runner is absent, source launch pins the `tsx/esm/api` loader and TypeScript path mapping to this installation. The command's working directory and ambient `TSX_TSCONFIG_PATH` cannot select the runner's source dependencies. ### Denial and runner-failure dialects diff --git a/packages/sandbox/sandbox-local/README.zh.md b/packages/sandbox/sandbox-local/README.zh.md index a7854c30d0..9f868b291e 100644 --- a/packages/sandbox/sandbox-local/README.zh.md +++ b/packages/sandbox/sandbox-local/README.zh.md @@ -80,6 +80,8 @@ Seatbelt profile 默认允许,带 `(deny file-write*)` 与来自共享 `writab Windows 档为每个工作区保留一个确定性写入 SID 和常驻 ACE,同时为每个活跃的会话/工作区对分配一个随机私有临时目录,以及不同的 SID 和可撤销 ACE——共享工作区的会话共享其预期写权限,却不会继承彼此的临时目录权限。新的提供方总会选择新的临时路径和 SID,因此崩溃残留既无法阻止恢复的会话,也无法向其授权。该档报告 `partial` 强制执行,因为 NTFS 硬链接会把同一文件对象别名为多个路径、读取仍不受限,且被其他 AppContainer 工具以包 SID 标记过的目录树对 Low 完整性子进程不可读。 +当内置 Windows runner 与技能注册表同时可用时,此提供方会注册 [ACL 诊断技能](../sandbox-windows-acl/README.zh.md#failures-and-recovery)。自定义 `runnerCommand` 不注册该技能;提供方释放时移除技能及其提取的资源。 + 构建后的 ACL runner 缺失时,源码启动将 `tsx/esm/api` 加载器和 TypeScript 路径映射固定到本安装目录。命令的工作目录和环境中的 `TSX_TSCONFIG_PATH` 无法选择 runner 的源码依赖。 ### 拒绝与 runner 失败方言 diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json index ff6e57550a..4a29327339 100644 --- a/packages/sandbox/sandbox-local/package.json +++ b/packages/sandbox/sandbox-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-local", "description": "Local process-sandbox backends for the DeepSeek Harness sandbox seam: bwrap, the npm-distributed landlock-run launcher, macOS Seatbelt, or the Windows ACL restricted-token runner — functionally probed, fail-closed", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-local/src/index.ts b/packages/sandbox/sandbox-local/src/index.ts index 5f180668cf..45028746ff 100644 --- a/packages/sandbox/sandbox-local/src/index.ts +++ b/packages/sandbox/sandbox-local/src/index.ts @@ -37,7 +37,7 @@ import z from '@deepseek-ai/schemastery' import { SandboxProvider, SandboxUnavailableError, canonicalPath } from '@deepseek-ai/dsh-sandbox' import type { ConfinedArgv, ConfinedSandboxMode, RunnerFailureRule, SandboxEnforcement, SandboxPolicy } from '@deepseek-ai/dsh-sandbox' import type { SessionId } from '@deepseek-ai/dsh-session' -import { AclWriteGrant, assertTempRootOutsideWorkspace, tempWriteSid, workspaceWriteSid } from '@deepseek-ai/dsh-sandbox-windows-acl' +import { AclWriteGrant, assertTempRootOutsideWorkspace, registerAclDiagnosisSkill, tempWriteSid, workspaceWriteSid } from '@deepseek-ai/dsh-sandbox-windows-acl' import { assertNever } from '@deepseek-ai/dsh-util-values' import { bwrapProfileArgs, landlockProfileArgs, seatbeltProfileArgs } from './profiles.ts' @@ -295,6 +295,12 @@ export class LocalSandboxProvider extends SandboxProvider { this.configuredRunnerFailureSignatures = runnerFailureSignatures this.probeTimeoutMs = config.probeTimeoutMs as number assertPositiveFinite('probeTimeoutMs', this.probeTimeoutMs) + // An operator-supplied runner does not use the ACL backend. The registry + // remains optional and may be mounted after this provider. + /* v8 ignore next 3 -- Windows-only registration; the Linux coverage lane cannot take this branch */ + if (process.platform === 'win32' && this.runnerCommand === undefined) { + ctx.inject(['skills'], (skillsCtx) => { registerAclDiagnosisSkill(skillsCtx) }) + } // The temp grants are revoked with the provider: a clean server // shutdown leaves no temp ACEs behind (workspace ACEs stand by design — // the reuse cache; an unclean shutdown leaves them for the next diff --git a/packages/sandbox/sandbox-local/tests/acl-grants.spec.ts b/packages/sandbox/sandbox-local/tests/acl-grants.spec.ts index 859fa1d8a5..646377e85c 100644 --- a/packages/sandbox/sandbox-local/tests/acl-grants.spec.ts +++ b/packages/sandbox/sandbox-local/tests/acl-grants.spec.ts @@ -50,6 +50,7 @@ vi.mock('@deepseek-ai/dsh-sandbox-windows-acl', () => { } } return { + registerAclDiagnosisSkill: vi.fn(), AclWriteGrant: MockAclWriteGrant, assertTempRootOutsideWorkspace: (workspaceRoot: string, tempRoot: string) => { const workspace = realpathSync.native(workspaceRoot) diff --git a/packages/sandbox/sandbox-policy/package.json b/packages/sandbox/sandbox-policy/package.json index daad301311..94e4760ef1 100644 --- a/packages/sandbox/sandbox-policy/package.json +++ b/packages/sandbox/sandbox-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-policy", "description": "Per-call sandbox policy resolver and current model context: deployment fallbacks plus each session's mode and workspace root, shared by every enforcing capability family", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sandbox/sandbox-windows-acl/README.i18n.yaml b/packages/sandbox/sandbox-windows-acl/README.i18n.yaml index 899cb22541..95d12002ac 100644 --- a/packages/sandbox/sandbox-windows-acl/README.i18n.yaml +++ b/packages/sandbox/sandbox-windows-acl/README.i18n.yaml @@ -25,10 +25,10 @@ zh: 30bc370a5281bf0d /deepseek-ai-dsh-sandbox-windows-acl/use-this-package/what-confinement-gives-you: en: 289e65bddf20dd79 - zh: 44b9fcedf944546a + zh: 7ff72ed64c4b4d1c /deepseek-ai-dsh-sandbox-windows-acl/use-this-package/failures-and-recovery: - en: f93372e784cda63e - zh: f8371114f3481550 + en: 47c32eff504fcd87 + zh: 83c13c06ccb2e07a /deepseek-ai-dsh-sandbox-windows-acl/understand-the-implementation: en: c2cde86c85c2c619 zh: a29a114e591f9d3f @@ -51,14 +51,14 @@ en: 7850da9522bf984b zh: a9f2273a85bafcf9 /deepseek-ai-dsh-sandbox-windows-acl/model-experience: - en: c94416e26fec28cd - zh: a16f435e8afbdf30 + en: bbb19594a2c2e4d4 + zh: 075a65d9b9182aad /deepseek-ai-dsh-sandbox-windows-acl/model-experience/kv-cache-effect: - en: 796af37d6f6add68 - zh: 1e0dc5a58e5456be + en: 817eb1b7de13872e + zh: 06fe175e41e7459b /deepseek-ai-dsh-sandbox-windows-acl/known-limitations-and-deferred-work: - en: 2b6b967c62766d47 - zh: 0faf9ec390504967 + en: 046831a864786b2a + zh: 4a261fbab3396ea4 /deepseek-ai-dsh-sandbox-windows-acl/known-limitations-and-deferred-work/dev-note: en: cc273320f6a1beaf zh: 5c57a312ddc50651 diff --git a/packages/sandbox/sandbox-windows-acl/README.md b/packages/sandbox/sandbox-windows-acl/README.md index 42b2290028..402071c72a 100644 --- a/packages/sandbox/sandbox-windows-acl/README.md +++ b/packages/sandbox/sandbox-windows-acl/README.md @@ -75,6 +75,14 @@ Temp isolation is per live session/workspace pair: sessions sharing a workspace `init()` throws on any Win32 failure — the child is never spawned unrestricted. A runner that fails before executing the command prints `windows-acl-run: ` to stderr and exits 127, which the seam's runner-failure rules classify as a broken sandbox rather than a denial. Cleanup is best-effort by design: `dispose()` attempts every temp revocation and aggregates failures into an `AggregateError`. +A denial this backend cannot explain is diagnosed by the `diagnose-windows-sandbox-acl` skill bundled in `assets/`. `registerAclDiagnosisSkill` contributes it when `dsh-sandbox-local` uses its built-in Windows runner and a skill registry is available. Registration extracts resources for external PowerShell, including in ASAR and SEA deployments; fiber disposal unregisters the provider and removes its copy. Missing assets fail registration. + +Diagnosis leaves ACLs and existing content unchanged. `-Fix` removes explicit package allow ACEs ancestor first and verifies their inherited copies disappeared; the requested path needs effective `WRITE_DAC` and `WRITE_OWNER`. `-GrantFullControl` adds the caller's missing rights. Repairs require `WRITE_DAC`, preserve denies, owner, inheritance and SACL, and back up each changed DACL. Targets must be strictly inside `-AllowRoot`; reparse paths and managed application trees are refused. Failed repairs restore attempted changes in reverse order and exit nonzero. Follow the [bundled skill](assets/diagnose-windows-sandbox-acl/SKILL.md) for repair selection, original-operation verification and recovery. + +Every run emits `REPORT` JSON records of observations, actions, reasons and verification; unknown observations remain unknown. `-Compact -Out ` persists the full report and prints a summary with all inspected paths, findings, actions, recovery commands and `nextAction`. Action completion records API execution; verification records the observed result. The [recovery decisions](../../../.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md#acl-diagnosis-and-recovery) explain resource ownership and rollback limits. + +Inspection visits the requested object and each ancestor through the filesystem root. Each ACL observation's `path` identifies the inspected object; the classification's `details.packageObjects` lists objects carrying package allow ACEs without establishing the cause of the original failure. The [parent-package example](tests/expected/parent-package-report.jsonl) retains the fixture paths and shows selected fields, with `fixturePackageAces` projecting only the synthetic test SID from `aces`; host-owned ACEs and ancestors are omitted. The [failed-grant example](tests/expected/denied-grant-report.txt) retains operation paths and reasons. + ----- @@ -156,14 +164,16 @@ Start with the subsystem reference for the shared vocabulary, then the provider ## Model Experience -Indirectly, through [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md), [`dsh-pwsh-sandbox`](../../shell/pwsh-sandbox/README.md), and their tools, which render this backend's partial-enforcement and denial facts (the confined stderr the tool layer classifies through `denialSignatures`) while the [`dsh-sandbox`](../sandbox/README.md) seam owns the `SANDBOX_UNAVAILABLE` text and `sandbox-local` owns runner selection. +Indirectly, through [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md), [`dsh-pwsh-sandbox`](../../shell/pwsh-sandbox/README.md), and their tools, which render this backend's partial-enforcement and denial facts (the confined stderr the tool layer classifies through `denialSignatures`) while the [`dsh-sandbox`](../sandbox/README.md) seam owns the `SANDBOX_UNAVAILABLE` text and `sandbox-local` owns runner selection. On Windows this package also contributes one catalog entry, the bundled `diagnose-windows-sandbox-acl` skill, which is where the model learns to diagnose a denial the tool layer can only report. #### KV Cache effect -None directly; the denial surface belongs to the tool layer. +One extra catalog entry on Windows: the skill's description enters context with the catalog, and the body enters only when invoked. Resource extraction happens at registration and does not add script contents to model context. The denial surface itself stays in the tool layer. ## Known Limitations and Deferred Work +- **Diagnosis preserves integrity labels** — it cannot fix Low-labeled executables affecting programs launched outside DSH. It also cannot repair a caller's missing `WRITE_DAC`; extracted and recovery scripts are user-writable and are not supported elevated entrypoints. Resource directories can remain after an unclean shutdown. + @@ -178,6 +188,7 @@ These limits define when the backend is a poor fit or needs special operational - **Read-side confinement and network policy are out of scope** — `WRITE_RESTRICTED` intersects write accesses only; pair this backend with a read-side policy for stronger confinement. - **Reads stop at objects another AppContainer-based tool has ACL'd with a package SID** — a file whose DACL carries an ACE for a package SID (`S-1-15-2-…`) is inaccessible to a Low-integrity token on this host, even when the same DACL grants the user full control and Everyone read (observed: adding that single ACE to a fresh file reproduces the denial, granting Everyone read does not lift it, and the same bytes copied elsewhere stay readable). The kernel rule behind it is unconfirmed and not this package's to change; tools that sandbox themselves with AppContainers stamp such ACEs, so a tree they touched becomes unreadable to this backend's child. Removing the foreign ACE (or re-installing the affected tree) restores access. - **Wide-directory and FAT-volume warnings are deferred; the FAT-class residue is unverified** — the UI-side warnings are not implemented, a FAT volume as a grant root fails loudly, and a FAT-class target outside the granted roots stores no security descriptor; its effective integrity label is assigned by the system rather than recorded on the object, so the label layer's behavior there is untested. FAT stays legacy residue. +- **The bundled diagnosis skill needs an unconfined caller** — both the classification and the repairs read or write security descriptors a confined child cannot reach, so the session must escalate that one call; with no approval channel the escalation fails closed and the path stays undiagnosed. - **PowerShell language mode differs by confined mode** — under `read-only`, PowerShell cannot create its AppLocker probe files in temp and conservatively starts in ConstrainedLanguage (`Add-Type`, non-core .NET static calls, COM, and reflection fail); the shipped `workspace-write` path lets the probe complete, so pwsh stays in FullLanguage unless host-wide WDAC/AppLocker policy says otherwise, while a direct `AclSandbox` with `tempDir: null` has no such guarantee. This split is PowerShell startup behavior, not part of the ACL write boundary. diff --git a/packages/sandbox/sandbox-windows-acl/README.zh.md b/packages/sandbox/sandbox-windows-acl/README.zh.md index 6a99eec14a..623e9cea06 100644 --- a/packages/sandbox/sandbox-windows-acl/README.zh.md +++ b/packages/sandbox/sandbox-windows-acl/README.zh.md @@ -71,10 +71,20 @@ rmSync(tempDir, { recursive: true, force: true }) 临时隔离按每个活跃的会话/工作区对进行:共享工作区的会话共享其写权限,但无法写入彼此的临时目录。新的提供方总会选择新的临时路径和 SID,因此崩溃残留既无法阻止恢复的会话,也无法向其授权。 + + ### 失败与恢复 `init()` 在任何 Win32 失败时抛出——子进程绝不会不受限制地 spawn。执行命令前失败的 runner 会向 stderr 打印 `windows-acl-run: ` 并以 127 退出,seam 的 runner 失败规则将其归类为损坏的沙箱,而非拒绝。清理按设计尽力而为:`dispose()` 会尝试全部临时撤销并把失败聚合为 `AggregateError`。 +此后端无法解释的拒绝交给 `assets/` 中随包发布的 `diagnose-windows-sandbox-acl` 技能诊断。当 `dsh-sandbox-local` 使用内置 Windows runner 且技能注册表可用时,`registerAclDiagnosisSkill` 会注册它。注册时提取供外部 PowerShell 使用的资源,ASAR 和 SEA 部署也适用;fiber 释放时注销提供者并删除副本。缺少资源会导致注册失败。 + +诊断不修改 ACL 或已有内容。`-Fix` 从祖先开始移除显式包允许 ACE,并验证其继承副本已消失;请求路径需要有效的 `WRITE_DAC` 和 `WRITE_OWNER`。`-GrantFullControl` 补充调用者缺少的权限。修复要求 `WRITE_DAC`,保留拒绝条目、所有者、继承和 SACL,并备份每个修改的 DACL。目标必须严格位于 `-AllowRoot` 内;重解析路径和受管理的应用目录会被拒绝。修复失败时按相反顺序恢复尝试修改的内容,并以非零状态退出。修复选择、原始操作验证及恢复流程见[内置技能](assets/diagnose-windows-sandbox-acl/SKILL.md)。 + +每次执行都会输出包含观察、操作、原因和验证结果的 `REPORT` JSON 记录;未知观察保持未知。`-Compact -Out ` 保存完整报告,打印包含全部分析路径、发现、操作、恢复命令和 `nextAction` 的摘要。操作完成记录 API 执行情况,验证记录实际观察到的结果。[恢复决策](../../../.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md#acl-diagnosis-and-recovery) 说明资源归属和回滚限制。 + +检查会从请求对象逐级访问祖先,直到文件系统根目录。每条 ACL 观察的 `path` 标识实际检查的对象;分类结果的 `details.packageObjects` 列出包含包允许 ACE 的对象,但不能据此确认原始失败的原因。[父目录包 SID 示例](tests/expected/parent-package-report.jsonl) 保留测试目录路径并展示选定字段,其中 `fixturePackageAces` 只从 `aces` 提取合成测试 SID,省略机器自带的 ACE 和祖先目录。[授权失败示例](tests/expected/denied-grant-report.txt) 保留操作路径和原因。 + ----- @@ -156,14 +166,16 @@ seam 先把确定性工作区 SID 的 ACE 常驻物化(每个工作区每服 ## 模型体验 -间接地通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.zh.md)、[`dsh-pwsh-sandbox`](../../shell/pwsh-sandbox/README.zh.md) 及其工具呈现;它们渲染此后端的部分强制执行与拒绝事实(工具层通过 `denialSignatures` 分类的受限 stderr),而 [`dsh-sandbox`](../sandbox/README.zh.md) seam 拥有 `SANDBOX_UNAVAILABLE` 文本、`sandbox-local` 拥有 runner 选择。 +间接地通过 [`dsh-bash-sandbox`](../../shell/bash-sandbox/README.zh.md)、[`dsh-pwsh-sandbox`](../../shell/pwsh-sandbox/README.zh.md) 及其工具呈现;它们渲染此后端的部分强制执行与拒绝事实(工具层通过 `denialSignatures` 分类的受限 stderr),而 [`dsh-sandbox`](../sandbox/README.zh.md) seam 拥有 `SANDBOX_UNAVAILABLE` 文本、`sandbox-local` 拥有 runner 选择。在 Windows 上本包还贡献一个目录条目——随包发布的 `diagnose-windows-sandbox-acl` 技能,模型正是从它学会诊断工具层只能上报的拒绝。 #### KV Cache 影响 -无直接影响;拒绝面属于工具层。 +Windows 上多一个目录条目:技能描述随目录进入上下文,正文只在调用时进入。资源在注册时提取,不会把脚本内容加入模型上下文。拒绝面本身仍属于工具层。 ## 已知限制与延期工作 +- **诊断保留完整性标签**——它无法修复 Low 标签的可执行文件影响用户在 DSH 外启动程序的问题,也无法修复调用者缺少 `WRITE_DAC` 的情况;提取的脚本和恢复脚本均可由用户写入,不支持作为提权入口。非正常退出后可能残留资源目录。 + @@ -178,6 +190,7 @@ seam 先把确定性工作区 SID 的 ACE 常驻物化(每个工作区每服 - **读侧隔离与网络策略不在范围内**——`WRITE_RESTRICTED` 只交叉检查写访问;将此后端与读侧策略配对以获得更强隔离。 - **读取会被其他基于 AppContainer 的工具以包 SID 授权过的对象挡住。** 在本机上,当文件的 DACL 携带针对包 SID(`S-1-15-2-…`)的 ACE 时,Low 完整性的令牌无法访问它——即使同一份 DACL 同时向用户授予完全控制、向 Everyone 授予读取(已观测:只给新文件加这一条 ACE 即可复现拒绝,补授 Everyone 读取无法解除,而同样内容复制到别处仍可读)。其背后的内核规则尚未确证,也不由本包掌控;以 AppContainer 自我隔离的工具正是会写入这类 ACE,因此被它们标记过的目录树对本后端的子进程将不可读。移除外来 ACE(或重新安装受影响的目录树)即可恢复访问。 - **宽目录与 FAT 卷警告已推迟;FAT 类残留未经验证。** UI 侧警告尚未实现,FAT 卷作为授权根会大声失败,而授权根之外的 FAT 类目标不存储安全描述符;其有效完整性标签由系统分配而非记录在对象上,因此标签层在该处的行为未经测试。FAT 仍被视为遗留残留。 +- **随包发布的诊断技能需要不受限的调用者。** 无论分类还是修复,都要读写受限子进程够不到的安全描述符,因此会话必须为那一次调用升权;没有审批通道时升权 fail-closed,该路径保持未诊断。 - **PowerShell 语言模式因受限模式而异。** 在 `read-only` 下,PowerShell 无法在临时目录中创建 AppLocker 探针文件,因此会保守地以 ConstrainedLanguage 启动(`Add-Type`、非核心 .NET 静态调用、COM 与反射失败);交付的 `workspace-write` 路径可让探针完成,因此除非主机范围的 WDAC/AppLocker 策略另有规定,否则 pwsh 保持 FullLanguage,而直接使用 `AclSandbox` 并配置 `tempDir: null` 时则没有这一保证。这一区别属于 PowerShell 启动行为,不是 ACL 写入边界的一部分。 diff --git a/packages/sandbox/sandbox-windows-acl/assets/diagnose-windows-sandbox-acl/SKILL.md b/packages/sandbox/sandbox-windows-acl/assets/diagnose-windows-sandbox-acl/SKILL.md new file mode 100644 index 0000000000..68f3a1f8df --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/assets/diagnose-windows-sandbox-acl/SKILL.md @@ -0,0 +1,46 @@ +--- +name: diagnose-windows-sandbox-acl +description: 'Use on Windows for unexpected DSH sandbox access denials: workspace writes or listing fail, or an ordinarily readable path cannot be read. The bundled script inspects the path and every ancestor, reports observations and action reasons, and supports scoped, backed-up ACL repairs. Expected confinement denials need no ACL repair.' +--- + +# Diagnose Windows sandbox ACL failures + +**Stop after any failed/refused repair or failed verification.** A completed DACL write is not a successful repair. Never continue from a failed grant to `-Fix`, repeat the grant, or remove a deny ACE. The script automatically restores attempted changes from the failed invocation; verify its rollback result. Restore earlier successful invocations in reverse order using their exact recovery commands, then stop. A later backup alone cannot undo earlier repairs. + +## Decide whether diagnosis applies + +Diagnose unexpected writes/listing denied inside a `workspace-write` workspace, or reads the signed-in user should plainly have. Root-only failure and uniform failure are both eligible. + +Stop and explain expected denials: writes outside the workspace, any write in `read-only`, piped grandchild `spawn EPERM`, or ConstrainedLanguage errors for .NET/COM/reflection. Request an approval for the one call you still need; do not repair ACLs for these cases. + +## Read the report before choosing a repair + +Resolve the script from this skill's resource directory. Run it in the current sandbox first: classification completes under `workspace-write` with a writable `-Out` directory, while `read-only` cannot write one and refuses. When a denial or an unwritable report directory prevents the call, request one escalation of that same call through the normal approval path instead of assuming the diagnosis cannot run; if approval is refused, unavailable to the session, or forbidden by policy, report the path as undiagnosed and stop. Unconfined does not elevate the Windows token. Use one path per invocation; do not repeat `-Path` or pass a comma-separated string to `pwsh -File`. + +```powershell +& '\scripts\diagnose-windows-sandbox-acl.ps1' -Path '' -Out '' -Compact +exit $LASTEXITCODE +``` + +Substitute full, quoted paths. Choose a persistent, user-owned `-Out` directory, preferably beside the failing workspace within the authorized tree. Never put reports or backups in the skill resource directory: it is deleted when the skill unloads. `-Compact` saves a unique full JSONL report and prints one `REPORT` summary. Diagnosis changes no ACL or existing content; it only writes this requested report. The summary includes all `inspectedPaths`, findings, decisions, ACL actions, verification, rollback state and `nextAction`. Use the summary to decide; read only specific records from the saved report when evidence is missing, without dumping the whole report or reading the implementation to rediscover these instructions. + +Each full record has `kind`, `operation`, `path`, `status`, `reason`, `details`. Unknown observations remain unknown. A deny ACE's presence alone does not establish causation. DSH provisions `S-1-4-*` workspace grants and an Everyone `DeleteSubdirectoriesAndFiles` deny; these are expected confinement entries, not package conflicts or automatically unresolved faults. Coexisting package ACEs may still block the confined child even when provisioning fails first; do not dismiss either finding. `completed` actions only confirm API execution. Check verification and the final summary. Missing summary means unconfirmed completion: inspect the report and recovery artifacts before doing anything else. + +| Verdict | Next step, only with complete observations | +|---|---| +| `CULPRIT` | Explain the affected paths and individual package allow SIDs; select `-Fix`. | +| `PRECONDITION` | Explain missing effective `WRITE_DAC`/`WRITE_OWNER`; select `-GrantFullControl` if the caller has `WRITE_DAC`. | +| `BOTH` | Grant on the affected object first. Only after verified success may a separate invocation use `-Fix`. Failure ends this sequence. | +| `UNREADABLE`, `INCOMPLETE`, `NOT_THIS_CLASS` | Report observations and stop; do not infer a safe repair. | + +## Run the selected repair + +Explain the finding and cost first. `-Fix` removes individual `S-1-15-2-*` allow ACEs at their explicit sources, ancestor first, then verifies inherited entries disappeared; well-known groups ending in 1 or 2 are preserved. This removes those packages' access. `-GrantFullControl` adds a current-user allow ACE; it cannot cancel an explicit deny. Both preserve owner, inheritance and SACL, including Low integrity labels. Low executable labels and their effects outside DSH are outside this repair's scope. Full control supplies `WRITE_DAC` and `WRITE_OWNER`; taking ownership alone does not. + +Append `-AllowRoot ''` and **exactly one** of `-Fix` or `-GrantFullControl` to the diagnostic command, before `exit`. Keep `-Compact -Out`. Every explicit source must be strictly inside that root; reparse paths and managed application trees (`LocalAppData\Packages`, `ProgramFiles\WindowsApps`) are refused. Never widen the authorized root to reach an ancestor. Missing effective `WRITE_DAC` in the unconfined caller requires stopping and reporting the path and missing rights for permission-policy review. Extracted scripts and recovery copies are user-writable: never supply commands to run them elevated, initiate UAC/`runas`, or run the whole agent elevated. + +Follow `nextAction`: `stop` ends repairs; `restore_pending_then_stop` requires the reported recovery commands in their listed order, then stop. Verified rollback does not turn a failed repair into success. Keep full report/backup paths and exact recovery commands for every invocation; never abbreviate executable paths. On recovery failure, stop and report the remaining commands without more repairs. + +For `verify_original_confined_operation`, repeat the original failed operation in the original confined context. If it still fails, restore this workflow's successful repairs in reverse order, report the error and stop. Do not start another repair cycle. + +Use the user's language for progress and final reporting. Concisely report observations and analyzed paths (including relevant ancestors), actions and reasons, verification, and the next step; link full reports. Label any authorized unconfined fallback as such, not as successful repair. Explain that another tool may recreate a removed package ACE. Never write probe files, edit ACLs by hand, change owners, erase denies, or replace child permissions recursively. Do not provide manual ACL-edit commands for the user either: only bundled repairs and generated recovery commands are supported; unresolved denies require permission-policy review or moving the workspace. diff --git a/packages/sandbox/sandbox-windows-acl/assets/diagnose-windows-sandbox-acl/scripts/diagnose-windows-sandbox-acl.ps1 b/packages/sandbox/sandbox-windows-acl/assets/diagnose-windows-sandbox-acl/scripts/diagnose-windows-sandbox-acl.ps1 new file mode 100644 index 0000000000..3106e444fa --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/assets/diagnose-windows-sandbox-acl/scripts/diagnose-windows-sandbox-acl.ps1 @@ -0,0 +1,721 @@ +<# +.SYNOPSIS + Read-only diagnosis, and whitelisted repair, of Windows ACL objects that the + DSH sandbox cannot open. + +.DESCRIPTION + A denial inside the DSH sandbox is worth diagnosing only when it contradicts + what the active mode promises: writes inside the workspace, or reads that the + signed-in user should plainly have. This script reports why such an object is + unreadable or unopenable, and repairs package-ACE conflicts or missing rights. + + Diagnosis (the default) never changes ACLs: it reads each requested path + and every ancestor with .NET and native icacls, and prints machine-parseable + lines for the caller to interpret. REPORT JSON records distinguish observed + facts, decisions and their reasons, attempted operations, and verification. + A caught exception still emits the operation history and recovery commands. + Failed repairs restore attempted DACL changes in reverse order and stop. + + Repair (-Fix) removes ONLY allow ACEs whose SID is an AppContainer package SID + (S-1-15-2-*, excluding the well-known groups ending in 1 or 2). Removing them + also removes that package's access. Repair requires + -AllowRoot, refuses any target outside it or reached through a reparse point, + and records a DACL backup with an independent recovery script. The grant checks + effective rights; removal verifies that other icacls lines remain unchanged. + + The script never creates, deletes, or writes the contents of any file. Its + only content writes are recovery artifacts and explicitly requested reports. + +.PARAMETER Path + One or more failing paths. Each is diagnosed together with its ancestors. + +.PARAMETER AllowRoot + Required with any mutation. Every modified object must be strictly inside this directory. + +.PARAMETER Out + Required with -Fix, -GrantFullControl or -Compact. Receives recovery artifacts + and, with -Compact, a unique full JSONL report. + +.PARAMETER Compact + Save every REPORT record under -Out and print a decision summary with the + inspected paths, findings, actions, verification and pending recovery commands. + +.PARAMETER Fix + Remove individual package allow ACEs, preserving well-known package groups. + Without a mutation switch, only diagnose. + +.PARAMETER GrantFullControl + Grant the current user full control on the requested path. Full control is what + the documented precondition needs: it carries WRITE_DAC for the DACL and + WRITE_OWNER for the mandatory label DSH writes in the same call, while taking + ownership supplies only WRITE_DAC. The owner is never changed. Mutually + exclusive with -Fix. + +.PARAMETER Restore + Restore the DACL recovery record for exactly one -Path. Requires -AllowRoot + and WRITE_DAC, preserves owner and SACL, and is exclusive with either repair. + +.EXAMPLE + pwsh -File diagnose-windows-sandbox-acl.ps1 -Path 'C:\Users\me\.ssh' + +.EXAMPLE + pwsh -File diagnose-windows-sandbox-acl.ps1 -Path '\.dsh-acl-fixture-x\case1' -AllowRoot '\.dsh-acl-fixture-x' -Out '\.dsh-acl-fixture-x\report' -Fix +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string[]]$Path, + [string]$AllowRoot, + [string]$Out, + [switch]$Fix, + [switch]$GrantFullControl, + [string]$Restore, + [switch]$Compact +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# These two well-known groups name all packages, not one conflicting package. +$PACKAGE_SID = '^S-1-15-2-(?![12]$)' +$LOW_LABEL_SID = 'S-1-16-4096' + +function Write-Line { param([string]$Text) if (-not $Compact) { Write-Output $Text } } + +# Reports bypass the success pipeline so diagnostics cannot become a function's +# return value. Each JSON record occupies one stdout line, including paths/errors. +function Write-Report { + param([string]$Kind, [string]$Operation, [string]$Target, [string]$Status, [string]$Reason, $Details = @{}) + $record = [ordered]@{ kind = $Kind; operation = $Operation; path = $Target; status = $Status; reason = $Reason; details = $Details } + $script:reports.Add($record) + if ($Kind -eq 'observation' -and $Status -in @('unknown', 'unreadable', 'partial')) { $script:observationFailures++ } + $json = $record | ConvertTo-Json -Depth 12 -Compress + if ($script:reportWriter) { + try { $script:reportWriter.WriteLine($json); $script:reportWriter.Flush() } + catch { + $script:reportWriter.Dispose() + $script:reportWriter = $null + $script:reportFailed = $true + [Console]::Out.WriteLine('REPORT ' + $json) + throw + } + } + if (-not $Compact -or $script:reportFailed) { [Console]::Out.WriteLine('REPORT ' + $json) } +} + +function Invoke-ReportedOperation { + param([string]$Operation, [string]$Target, [string]$Reason, [string]$Effect, [scriptblock]$Action) + $entry = [ordered]@{ id = $script:operations.Count + 1; operation = $Operation; path = $Target; effect = $Effect; status = 'started' } + $script:operations.Add($entry) + if ($Effect -eq 'acl' -and $Operation -ne 'restore_dacl' -and $script:recoveries.Count -gt 0) { + $script:recoveries[-1].attempted = $true + } + Write-Report action $Operation $Target started $Reason @{ id = $entry.id; effect = $Effect } + try { + $result = & $Action + $entry.status = 'completed' + Write-Report action $Operation $Target completed $Reason @{ id = $entry.id; effect = $Effect } + return $result + } catch { + $entry.status = 'failed' + Write-Report action $Operation $Target failed $Reason @{ + id = $entry.id; effect = $Effect; error = (Get-HResultChain $_.Exception) + state = $(if ($Effect -eq 'none') { 'No mutation requested by this operation.' } else { 'The operation may have partially changed state; completion is unconfirmed.' }) + } + throw + } +} + +function Write-Decision { + param([string]$Operation, [string]$Target, [string]$Status, [string]$Reason) + Write-Report decision $Operation $Target $Status $Reason +} + +# Unwrap nested exceptions: PowerShell wraps Win32 failures, and only the inner +# exception carries the real HRESULT the caller needs to classify the failure. +function Get-HResultChain { + param([System.Exception]$Exception) + $chain = @() + $e = $Exception + while ($null -ne $e) { + $chain += ('{0}=0x{1:X8}/win32={2}: {3}' -f $e.GetType().Name, $e.HResult, ($e.HResult -band 0xFFFF), $e.Message) + $e = $e.InnerException + } + return ($chain -join ' <- ') +} + +function Get-CurrentIdentity { + return [System.Security.Principal.WindowsIdentity]::GetCurrent() +} + +# Integrity level affects the symptom: the same foreign +# ACE fails a grant when the caller is below Medium and merely blocks the child +# when the caller is not. +# The integrity level lives in the token's group list, which .NET filters out of +# WindowsIdentity.Groups, so read it through GetTokenInformation. Add-Type compiles +# in-process on PowerShell 7. Never spawn whoami.exe for this: inside a below-Medium +# token that process fails to initialize (STATUS_DLL_INIT_FAILED, 0xc0000142) and +# raises an application-error dialog on the user's desktop. +function Initialize-NativeApi { + if (-not ('Dsh.TokenInfo' -as [type])) { + Add-Type -Namespace Dsh -Name TokenInfo -MemberDefinition @' +[StructLayout(LayoutKind.Sequential)] +public struct SID_AND_ATTRIBUTES { public IntPtr Sid; public uint Attributes; } +[StructLayout(LayoutKind.Sequential)] +public struct TOKEN_MANDATORY_LABEL { public SID_AND_ATTRIBUTES Label; } +[DllImport("advapi32.dll", SetLastError = true)] +public static extern bool GetTokenInformation(IntPtr token, int infoClass, IntPtr info, uint length, out uint returned); +[DllImport("advapi32.dll", SetLastError = true)] +public static extern IntPtr GetSidSubAuthority(IntPtr sid, uint index); +[DllImport("advapi32.dll", SetLastError = true)] +public static extern IntPtr GetSidSubAuthorityCount(IntPtr sid); +[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] +private static extern Microsoft.Win32.SafeHandles.SafeFileHandle CreateFileW( + string path, uint access, uint share, IntPtr security, uint disposition, uint flags, IntPtr template); +// OPEN_EXISTING never creates or changes contents. Windows evaluates the caller's +// effective access, including group attributes, denies, ownership and inheritance. +public static bool HasAccess(string path, uint access) { + using (var handle = CreateFileW(path, access, 7, IntPtr.Zero, 3, 0x02200000, IntPtr.Zero)) { + if (!handle.IsInvalid) { return true; } + int error = Marshal.GetLastWin32Error(); + if (error == 5) { return false; } + throw new System.ComponentModel.Win32Exception(error, "CreateFileW access check: " + path); + } +} +[DllImport("advapi32.dll", CharSet = CharSet.Unicode)] +private static extern uint SetNamedSecurityInfoW(string path, int type, uint information, + IntPtr owner, IntPtr group, byte[] dacl, IntPtr sacl); +public static void SetDacl(string path, string sddl) { + var descriptor = new System.Security.AccessControl.RawSecurityDescriptor(sddl); + byte[] dacl = null; + if (descriptor.DiscretionaryAcl != null) { + dacl = new byte[descriptor.DiscretionaryAcl.BinaryLength]; + descriptor.DiscretionaryAcl.GetBinaryForm(dacl, 0); + } + bool isProtected = (descriptor.ControlFlags & System.Security.AccessControl.ControlFlags.DiscretionaryAclProtected) != 0; + // Only the DACL and its inheritance protection are written: never owner, group or SACL. + uint result = SetNamedSecurityInfoW(path, 1, 4u | (isProtected ? 0x80000000u : 0x20000000u), + IntPtr.Zero, IntPtr.Zero, dacl, IntPtr.Zero); + if (result != 0) { throw new System.ComponentModel.Win32Exception((int)result, "Write DACL: " + path); } +} +public static int Level(IntPtr token) { + uint length; + GetTokenInformation(token, 25, IntPtr.Zero, 0, out length); + if (length == 0) { throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Read integrity information length"); } + IntPtr buffer = Marshal.AllocHGlobal((int)length); + try { + if (!GetTokenInformation(token, 25, buffer, length, out length)) { throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Read integrity information"); } + IntPtr sid = Marshal.PtrToStructure(buffer).Label.Sid; + byte count = Marshal.ReadByte(GetSidSubAuthorityCount(sid)); + return Marshal.ReadInt32(GetSidSubAuthority(sid, (uint)(count - 1))); + } finally { Marshal.FreeHGlobal(buffer); } +} +'@ + } +} + +function Get-IntegritySid { + try { + $level = [Dsh.TokenInfo]::Level([System.Security.Principal.WindowsIdentity]::GetCurrent().Token) + switch ($level) { + 0 { return 'S-1-16-0 (Untrusted)' } + 4096 { return 'S-1-16-4096 (Low)' } + 8192 { return 'S-1-16-8192 (Medium)' } + 12288 { return 'S-1-16-12288 (High)' } + 16384 { return 'S-1-16-16384 (System)' } + default { return "S-1-16-$level (unrecognized level)" } + } + } catch { + Write-Report observation integrity '' unknown 'The token integrity level could not be read; do not infer that the caller is elevated or confined.' @{ error = (Get-HResultChain $_.Exception) } + return 'unknown' + } +} + +function Get-NormalizedPath { + param([string]$Value) + $full = [System.IO.Path]::GetFullPath($Value) + $root = [System.IO.Path]::GetPathRoot($full) + if ($full.Length -le $root.Length) { return $root } + return $full.TrimEnd('\', '/') +} + +function Test-DangerousRoot { + param([string]$FullPath) + $trimmed = (Get-NormalizedPath $FullPath).TrimEnd('\') + if ($trimmed -match '^[A-Za-z]:$') { return 'drive root' } + if ($trimmed -ieq ([System.IO.Path]::GetFullPath($env:USERPROFILE).TrimEnd('\'))) { return 'user profile root' } + if ($trimmed -ieq ([System.IO.Path]::GetFullPath($env:WINDIR).TrimEnd('\'))) { return 'Windows directory' } + foreach ($entry in @( + @{ directory = $env:LOCALAPPDATA; child = 'Packages' }, + @{ directory = $env:ProgramFiles; child = 'WindowsApps' }, + @{ directory = $env:ProgramW6432; child = 'WindowsApps' } + )) { + if (-not $entry.directory) { continue } + $protected = Get-NormalizedPath (Join-Path $entry.directory $entry.child) + if ($trimmed -ieq $protected -or (Test-UnderRoot $trimmed $protected)) { return 'managed application directory' } + } + return $null +} + +function Test-UnderRoot { + param([string]$FullPath, [string]$Root) + $r = Get-NormalizedPath $Root + $c = Get-NormalizedPath $FullPath + return $c -ine $r -and $c.StartsWith($r.TrimEnd('\') + '\', [System.StringComparison]::OrdinalIgnoreCase) +} + +function Get-RepairRefusal { + param([string]$FullPath, [string]$Root) + if (-not (Test-UnderRoot -FullPath $FullPath -Root $Root)) { return "$FullPath is outside -AllowRoot" } + $danger = Test-DangerousRoot -FullPath $FullPath + if ($danger) { return "$FullPath is a $danger" } + # Checking every component also covers a junction used as AllowRoot itself. + foreach ($component in (Get-Ancestors -FullPath $FullPath)) { + $item = Get-Item -LiteralPath $component -Force + if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { + return "$FullPath traverses a reparse point: $component" + } + } + return $null +} + +function Save-AclBackup { + param([string]$FullPath, [string]$Directory, [string]$Root) + $fullDirectory = [System.IO.Path]::GetFullPath($Directory) + if (-not (Test-Path -LiteralPath $fullDirectory)) { + Invoke-ReportedOperation create_backup_directory $fullDirectory 'The requested recovery directory does not exist; create it before writing backup files.' files { + New-Item -ItemType Directory -Path $fullDirectory | Out-Null + } + } + $backup = Join-Path $fullDirectory ('acl-backup-{0}.txt' -f ([guid]::NewGuid().ToString('N'))) + $record = $backup + '.json' + # The skill's extracted directory expires with its registration. Recovery must + # remain runnable from the backup directory after that registration is gone. + $restoreScript = $backup + '.ps1' + $command = "pwsh -NoProfile -File '{0}' -Path '{1}' -AllowRoot '{2}' -Restore '{3}'" -f + $restoreScript.Replace("'", "''"), $FullPath.Replace("'", "''"), ([System.IO.Path]::GetFullPath($Root)).Replace("'", "''"), $record.Replace("'", "''") + Write-Report decision backup $FullPath selected 'All recovery files must be written successfully before attempting the ACL change.' @{ files = @($backup, $record, $restoreScript, ($backup + '.rollback.txt')) } + Invoke-ReportedOperation backup $FullPath 'Save the original DACL and an independent recovery command before changing permissions.' files { + $output = @(icacls $FullPath /save $backup 2>&1) + if ($LASTEXITCODE -ne 0) { throw "Could not back up the DACL (icacls exit $LASTEXITCODE): $($output -join "`n")" } + @{ Path = $FullPath; Dacl = (Get-Acl -LiteralPath $FullPath).GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access) } | + ConvertTo-Json | Set-Content -LiteralPath $record -Encoding utf8 + Copy-Item -LiteralPath $PSCommandPath -Destination $restoreScript + $command | Set-Content -LiteralPath ($backup + '.rollback.txt') + } + $script:recoveries.Add(@{ path = $FullPath; backup = $backup; record = $record; script = $restoreScript; command = $command; attempted = $false; restored = $false }) + Write-Report observation recovery $FullPath available 'These files can restore the saved DACL; no rollback has been executed.' $script:recoveries[-1] + Write-Line ('BACKUP {0} -> {1}' -f $FullPath, $backup) + Write-Line ('ROLLBACK {0}' -f $command) +} + +function Restore-SavedDacl { + param([string]$FullPath, [string]$Record, [string]$Root) + $refusal = Get-RepairRefusal -FullPath $FullPath -Root $Root + if ($refusal) { throw [System.ArgumentException]::new("RESTORE_REFUSED $refusal") } + $saved = Invoke-ReportedOperation read_recovery $Record 'Read the recovery record and verify it belongs to the requested path before restoring its DACL.' none { + Get-Content -LiteralPath $Record -Raw | ConvertFrom-Json + } + if ($saved.Path -isnot [string] -or $saved.Path -ine $FullPath -or $saved.Dacl -isnot [string]) { + throw [System.ArgumentException]::new('RESTORE_REFUSED backup does not describe the requested path') + } + if (-not [Dsh.TokenInfo]::HasAccess($FullPath, 0x40000)) { throw 'RESTORE_REFUSED the caller lacks WRITE_DAC' } + Invoke-ReportedOperation restore_dacl $FullPath 'Restore the requested backup DACL and inheritance protection, preserving owner and SACL.' acl { + [Dsh.TokenInfo]::SetDacl($FullPath, $saved.Dacl) + } + $actual = Invoke-ReportedOperation read_restored_dacl $FullPath 'Read the restored DACL to compare it with the saved record.' none { + (Get-Acl -LiteralPath $FullPath).GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access) + } + Write-Report verification restore $FullPath $(if ($actual -eq $saved.Dacl) { 'verified' } else { 'failed' }) 'Compare the observed DACL with the recovery record after writing it.' @{ expectedDacl = $saved.Dacl; actualDacl = $actual } + if ($actual -ne $saved.Dacl) { throw 'RESTORE_FAILED the restored DACL differs from the backup' } + $script:restored++ + Write-Line ('RESTORED {0}' -f $FullPath) +} + +function Write-CompactSummary { + param([string]$Status, [string]$NextAction, [array]$RollbackCommands) + $byPath = [ordered]@{} + foreach ($record in $script:reports) { if ($record.operation -eq 'inspect_acl') { $byPath[$record.path] = $record } } + $observations = @($byPath.Values) + $classifications = @($script:reports | Where-Object { $_.operation -eq 'classify' }) + $findings = @($observations | Where-Object { + $_.path -in $script:requestedPaths -or $_.status -ne 'read' -or + @($_.details['aces'] | Where-Object { $_ -and $_.sid -match $PACKAGE_SID -and $_.type -eq 'Allow' }).Count -gt 0 + } | ForEach-Object { + @{ path = $_.path; status = $_.status; writeDac = $_.details['writeDac']; writeOwner = $_.details['writeOwner'] + packageAllowSids = @($_.details['aces'] | Where-Object { $_ -and $_.sid -match $PACKAGE_SID -and $_.type -eq 'Allow' } | ForEach-Object { $_.sid }) + denies = @($_.details['aces'] | Where-Object { $_ -and $_.type -eq 'Deny' }); errors = @($_.details['errors']; $_.details['error']) } + }) + $details = [ordered]@{ + report = $script:reportPath; exitCode = $script:exitCode; nextAction = $NextAction + inspectedPaths = @($observations | ForEach-Object { $_.path } | Select-Object -Unique) + findings = $findings + decisions = @($classifications | ForEach-Object { @{ path = $_.path; verdict = $_.status; reason = $_.reason; packageObjects = $_.details.packageObjects } }) + actions = @($script:reports | Where-Object { $_.kind -eq 'action' -and $_.details['effect'] -eq 'acl' -and $_.status -ne 'started' } | ForEach-Object { @{ operation = $_.operation; path = $_.path; status = $_.status; reason = $_.reason } }) + verification = @($script:reports | Where-Object { $_.kind -eq 'verification' } | ForEach-Object { @{ operation = $_.operation; path = $_.path; status = $_.status } }) + rollback = $script:rollbackStatus; rollbackCommands = $RollbackCommands + errors = @($script:reports | Where-Object { $_.kind -eq 'error' -or ($_.kind -eq 'decision' -and $_.status -eq 'refused') } | ForEach-Object { @{ path = $_.path; reason = $_.reason; details = $_.details } }) + } + $record = @{ kind = 'summary'; operation = $script:mode; path = $script:currentPath; status = $Status + reason = 'Full observations, actions and reasons are in the report. Follow nextAction; a failed repair must not be followed by another repair.'; details = $details } + [Console]::Out.WriteLine('REPORT ' + ($record | ConvertTo-Json -Depth 12 -Compress)) +} + +function Get-ObjectFacts { + param([string]$FullPath, [string]$MeSid) + $facts = [ordered]@{ + Object = $FullPath + Readable = $false + Error = '' + PackageAces = @() + OtherAppContainerSids = @() + Owner = '' + OwnerIsMe = $false + MyRights = '' + HasWriteDac = $null + HasWriteOwner = $null + Aces = @() + Errors = @() + LowLabel = $null + AclLines = @() + } + try { + # Get-Acl reads the security descriptor directly. On .NET Core the + # FileSystemInfo.GetAccessControl() form is an extension method, which + # PowerShell cannot invoke with instance syntax. + $acl = Get-Acl -LiteralPath $FullPath + $facts.Readable = $true + } catch { + $facts.Error = Get-HResultChain -Exception $_.Exception + Write-Report observation inspect_acl $FullPath unreadable 'The ACL read failed; permissions and the cause of denial remain unknown.' @{ error = $facts.Error } + return $facts + } + foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) { + $sid = $rule.IdentityReference.Value + $facts.Aces += [ordered]@{ + sid = $sid; type = [string]$rule.AccessControlType; rights = [string]$rule.FileSystemRights + inherited = $rule.IsInherited; inheritance = [string]$rule.InheritanceFlags; propagation = [string]$rule.PropagationFlags + } + if ($sid -match $PACKAGE_SID -and $rule.AccessControlType -eq 'Allow') { + $facts.PackageAces += ('SID={0} INHERITED={1} INHERITABLE={2} RIGHTS={3}' -f $sid, $rule.IsInherited, $rule.InheritanceFlags, $rule.FileSystemRights) + } elseif ($sid -match '^S-1-15-' -and $sid -notmatch $PACKAGE_SID) { + $facts.OtherAppContainerSids += $sid + } + if ($sid -eq $MeSid -and $rule.AccessControlType -eq 'Allow') { + $facts.MyRights = ($facts.MyRights + ';' + [string]$rule.FileSystemRights).Trim(';') + } + } + try { + $facts.Owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value + $facts.OwnerIsMe = ($facts.Owner -eq $MeSid) + } catch { + $facts.Owner = 'unreadable' + $facts.Errors += @{ operation = 'read_owner'; error = (Get-HResultChain $_.Exception) } + } + foreach ($check in @(@{ field = 'HasWriteDac'; mask = 0x40000 }, @{ field = 'HasWriteOwner'; mask = 0x80000 })) { + try { $facts[$check.field] = [Dsh.TokenInfo]::HasAccess($FullPath, $check.mask) } + catch { $facts.Errors += @{ operation = $check.field; error = (Get-HResultChain $_.Exception) } } + } + # The mandatory label lives in the SACL. Reading the SACL needs a privilege while + # icacls prints the label without one, but icacls renders it by name, which is + # localized: try the integrity SID first and keep the English name as a fallback. + try { + $facts.AclLines = @(icacls $FullPath 2>&1 | ForEach-Object { [string]$_ }) + if ($LASTEXITCODE -ne 0) { throw "icacls exit ${LASTEXITCODE}: $($facts.AclLines -join "`n")" } + $text = $facts.AclLines -join "`n" + # An unrecognized localized label is unknown, not evidence of a Low label. + if ($text -match $LOW_LABEL_SID -or $text -match 'Mandatory Label\\Low Mandatory Level') { $facts.LowLabel = $true } + } catch { + $facts.Errors += @{ operation = 'icacls'; error = (Get-HResultChain $_.Exception) } + } + Write-Report observation inspect_acl $FullPath $(if ($facts.Errors.Count) { 'partial' } else { 'read' }) 'Read the ACL and check effective WRITE_DAC and WRITE_OWNER; observed ACEs alone do not identify which rule caused a denial.' @{ + owner = $facts.Owner; writeDac = $facts.HasWriteDac; writeOwner = $facts.HasWriteOwner + aces = $facts.Aces; nativeListing = @($facts.AclLines | ForEach-Object { $_.Trim() } | Where-Object { $_ }); lowLabel = $facts.LowLabel; errors = $facts.Errors + } + return $facts +} + +function Get-Ancestors { + param([string]$FullPath) + $list = @() + $current = Get-NormalizedPath $FullPath + while ($true) { + $list += $current + $parent = [System.IO.Path]::GetDirectoryName($current) + if ([string]::IsNullOrEmpty($parent) -or $parent -eq $current) { break } + $current = $parent + } + return $list +} + +# --- main --------------------------------------------------------------- + +$operations = [System.Collections.Generic.List[object]]::new() +$recoveries = [System.Collections.Generic.List[object]]::new() +$reports = [System.Collections.Generic.List[object]]::new() +$reportWriter = $null +$reportPath = $null +$reportFailed = $false +$requestedPaths = @() +$rollbackStatus = 'not-needed' +$fixed = 0 +$granted = 0 +$refused = 0 +$restored = 0 +$exitCode = 0 +$observationFailures = 0 +$currentPath = '' +$mode = if ($Restore) { 'restore' } elseif ($GrantFullControl) { 'grant' } elseif ($Fix) { 'fix' } else { 'diagnose' } + +try { + $requestedPaths = @($Path | ForEach-Object { [System.IO.Path]::GetFullPath($_) }) + Write-Report invocation $mode '' started 'Inspect the requested paths before deciding whether the requested operation can run.' @{ + paths = $Path; allowRoot = $AllowRoot; outputDirectory = $Out; recoveryRecord = $Restore; fix = $Fix.IsPresent; grantFullControl = $GrantFullControl.IsPresent + } + if ($Compact) { + if (-not $Out) { throw [System.ArgumentException]::new('-Compact requires -Out for the complete report') } + Invoke-ReportedOperation prepare_report $Out 'Create the requested report directory and a new JSONL report; existing reports are never overwritten.' files { + $directory = [System.IO.Path]::GetFullPath($Out) + [System.IO.Directory]::CreateDirectory($directory) | Out-Null + $script:reportPath = Join-Path $directory ('acl-report-{0}.jsonl' -f [guid]::NewGuid().ToString('N')) + $stream = [System.IO.File]::Open($script:reportPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write) + $script:reportWriter = [System.IO.StreamWriter]::new($stream, [System.Text.UTF8Encoding]::new($false)) + foreach ($record in $script:reports) { $script:reportWriter.WriteLine(($record | ConvertTo-Json -Depth 12 -Compress)) } + } + } + $identity = Get-CurrentIdentity + $meSid = $identity.User.Value + Invoke-ReportedOperation initialize '' 'Load read-only access checks and DACL-only writes before inspecting permissions.' none { Initialize-NativeApi } + $integrity = Get-IntegritySid + Write-Line ('CALLER SID={0} INTEGRITY={1}' -f $meSid, $integrity) + Write-Report observation caller '' read 'The caller token determines effective access; unconfined execution does not imply elevation.' @{ sid = $meSid; integrity = $integrity } + + if (@(@($Fix.IsPresent, $GrantFullControl.IsPresent, [bool]$Restore) | Where-Object { $_ }).Count -gt 1) { + throw [System.ArgumentException]::new('-Fix, -GrantFullControl and -Restore are separate operations; run one at a time') + } + if ($Fix -or $GrantFullControl -or $Restore) { + $requiredArguments = if ($Restore) { @('AllowRoot') } else { @('AllowRoot', 'Out') } + foreach ($required in $requiredArguments) { + if (-not (Get-Variable -Name $required -ValueOnly)) { + throw [System.ArgumentException]::new("The requested repair requires -$required") + } + } + if ($Restore -and $Path.Count -ne 1) { throw [System.ArgumentException]::new('-Restore requires exactly one -Path') } + } + + :paths foreach ($requested in $Path) { + $currentPath = $requested + $full = Get-NormalizedPath $requested + $currentPath = $full + Write-Line ('PATH={0}' -f $full) + if (-not (Test-Path -LiteralPath $full)) { + Write-Line ' MISSING' + Write-Line 'VERDICT=NOT_THIS_CLASS' + Write-Decision $mode $full skipped 'The requested path does not exist; no ACL was read or changed for it.' + if ($mode -ne 'diagnose') { $refused++; break paths } + continue + } + + $packageTargets = @() + $targetFacts = $null + foreach ($ancestor in (Get-Ancestors -FullPath $full)) { + if (-not (Test-Path -LiteralPath $ancestor)) { continue } + $f = Get-ObjectFacts -FullPath $ancestor -MeSid $meSid + if ($ancestor -eq $full) { $targetFacts = $f } + Write-Line (' OBJECT={0} READABLE={1}{2}' -f $f.Object, $f.Readable, $(if ($f.Readable) { '' } else { ' ERROR=' + $f.Error })) + if ($f.Readable) { + Write-Line (' OWNER={0} IS_CURRENT_USER={1} MY_RIGHTS=[{2}] WRITE_DAC={3} WRITE_OWNER={4} LOW_LABEL={5}' -f $f.Owner, $f.OwnerIsMe, $f.MyRights, $f.HasWriteDac, $f.HasWriteOwner, $f.LowLabel) + foreach ($ace in $f.PackageAces) { Write-Line (' PACKAGE_ACE {0}' -f $ace) } + foreach ($sid in ($f.OtherAppContainerSids | Sort-Object -Unique)) { Write-Line (' OTHER_S1_15 SID={0} (reported only)' -f $sid) } + } + if ($f.PackageAces.Count -gt 0) { $packageTargets += $f } + } + + # The precondition is judged on the requested path itself. Ancestors above the + # tree DSH grants are not part of that grant, so their ownership and rights would + # otherwise mark every healthy path as a precondition failure. + if ($null -eq $targetFacts) { throw "The requested path disappeared before its ACL could be inspected: $full" } + $unreadable = -not $targetFacts.Readable + $needsPrecondition = (-not $unreadable) -and + ((-not $targetFacts.HasWriteDac) -or (-not $targetFacts.HasWriteOwner)) + + if ($targetFacts.Errors.Count -gt 0) { + $verdict = 'INCOMPLETE' + $reason = 'Some observations failed; missing evidence is not evidence that a right is absent or a repair is safe.' + } elseif ($packageTargets.Count -gt 0) { + $blocked = @($packageTargets | Where-Object { -not $_.HasWriteDac }) + $verdict = if ($needsPrecondition -or $blocked.Count -gt 0) { 'BOTH' } else { 'CULPRIT' } + $reason = 'Package allow ACEs were observed. Required access checks determine whether the caller can perform the requested repair; other causes remain possible.' + } elseif ($unreadable) { + $verdict = 'UNREADABLE' + $reason = 'The requested ACL could not be read; its entries and the cause of denial are unknown.' + } elseif ($needsPrecondition) { + $verdict = 'PRECONDITION' + $reason = 'The caller cannot open the requested object with both WRITE_DAC and WRITE_OWNER. The reported allow and deny ACEs are evidence, not an attribution to one rule.' + } else { + $verdict = 'NOT_THIS_CLASS' + $reason = 'No package allow ACE was observed and both required rights are available. Other ACL restrictions and causes of the original failure are not ruled out.' + } + Write-Line ('VERDICT={0}' -f $verdict) + Write-Report decision classify $full $verdict $reason @{ writeDac = $targetFacts.HasWriteDac; writeOwner = $targetFacts.HasWriteOwner; packageObjects = @($packageTargets | ForEach-Object { $_.Object }) } + + if ($mode -eq 'diagnose') { + Write-Decision diagnose $full skipped 'No mutation mode was requested; all reported ACLs were left unchanged.' + continue + } + if ($targetFacts.Errors.Count -gt 0) { + Write-Decision $mode $full refused 'Required observations are incomplete; no repair was attempted for this path.' + $refused++; break paths + } + + if ($Restore) { + Restore-SavedDacl -FullPath $full -Record $Restore -Root $AllowRoot + continue + } + + if ($GrantFullControl) { + if ($unreadable) { Write-Line 'GRANT_REFUSED the object could not be read; the grant needs an unconfined caller'; Write-Decision grant $full refused 'The ACL is unreadable, so a preserving grant cannot be constructed.'; $refused++; break paths } + $refusal = Get-RepairRefusal -FullPath $full -Root $AllowRoot + if ($refusal) { Write-Line ('GRANT_REFUSED {0}' -f $refusal); Write-Decision grant $full refused $refusal; $refused++; break paths } + if ($targetFacts.HasWriteDac -and $targetFacts.HasWriteOwner) { Write-Line ('GRANT_SKIPPED {0} already carries WRITE_DAC and WRITE_OWNER' -f $full); Write-Decision grant $full skipped 'Effective WRITE_DAC and WRITE_OWNER are already available; no grant or backup is needed.'; continue } + if (-not $targetFacts.HasWriteDac) { Write-Line 'GRANT_REFUSED the caller lacks WRITE_DAC; stop for permission-policy review'; Write-Decision grant $full refused 'Effective WRITE_DAC is absent; this caller cannot change the DACL. Stop for permission-policy review; do not elevate a writable script copy.'; $refused++; break paths } + Save-AclBackup -FullPath $full -Directory $Out -Root $AllowRoot + # AddAccessRule preserves explicit denies; icacls /grant can remove a deny + # for the same principal. Write only the DACL, leaving owner and SACL intact. + $grantAcl = Get-Acl -LiteralPath $full + $grantAcl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new( + [System.Security.Principal.SecurityIdentifier]::new($meSid), + [System.Security.AccessControl.FileSystemRights]::FullControl, + [System.Security.AccessControl.AccessControlType]::Allow)) + Invoke-ReportedOperation grant_dacl $full "WRITE_OWNER is missing and WRITE_DAC is available; add a FullControl allow ACE for $meSid while preserving deny ACEs, owner and SACL." acl { + [Dsh.TokenInfo]::SetDacl($full, $grantAcl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access)) + } + $after = Get-ObjectFacts -FullPath $full -MeSid $meSid + $verified = $after.Readable -and $after.Errors.Count -eq 0 -and $after.HasWriteDac -and $after.HasWriteOwner + Write-Report verification grant $full $(if ($verified) { 'verified' } else { 'failed' }) 'The DACL write completed; recheck effective access before claiming that provisioning can succeed. On failure, restore this invocation and stop.' @{ + before = @{ writeDac = $targetFacts.HasWriteDac; writeOwner = $targetFacts.HasWriteOwner } + after = @{ writeDac = $after.HasWriteDac; writeOwner = $after.HasWriteOwner } + recovery = $recoveries[-1].command + } + if ($verified) { + Write-Line ('GRANTED {0} SID={1}' -f $full, $meSid); $granted++ + } else { + Write-Line ('GRANT_FAILED {0}; effective WRITE_DAC and WRITE_OWNER were not both confirmed; restore this invocation and stop' -f $full) + $refused++ + break paths + } + continue + } + + if (-not $Fix) { continue } + + if ($unreadable) { + Write-Line 'FIX_REFUSED the object could not be read; repair needs an unconfined caller' + Write-Decision fix $full refused 'The requested ACL could not be read; no package removal was attempted.' + $refused++ + break paths + } + if ($packageTargets.Count -eq 0) { Write-Decision fix $full skipped 'No package allow ACE was observed on the inspected objects; no ACL change or backup is needed.' } + $sources = @($packageTargets | Where-Object { + @($_.Aces | Where-Object { $_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID -and -not $_.inherited }).Count -gt 0 + }) + [array]::Reverse($sources) + foreach ($target in (@($sources) + @($packageTargets))) { + $refusal = Get-RepairRefusal -FullPath $target.Object -Root $AllowRoot + if ($refusal) { Write-Line ('FIX_REFUSED {0}' -f $refusal); Write-Decision fix $target.Object refused $refusal; $refused++; break paths } + if ($target.Errors.Count -gt 0) { Write-Decision fix $target.Object refused 'The ACL observations are incomplete; collateral changes could not be verified.'; $refused++; break paths } + } + if ($packageTargets.Count -gt 0 -and $needsPrecondition) { + Write-Decision fix $full refused 'Package removal requires verified WRITE_DAC and WRITE_OWNER on the requested path. A failed grant must not be followed by -Fix.' + $refused++; break paths + } + foreach ($target in $sources) { + if (-not $target.HasWriteDac) { + Write-Line ('FIX_REFUSED {0} needs WRITE_DAC; stop for permission-policy review' -f $target.Object) + Write-Decision fix $target.Object refused 'Effective WRITE_DAC is absent; removing a package allow ACE requires that right.' + $refused++ + break paths + } + } + Write-Report decision fix_sources $full selected 'Remove explicit package allow ACEs from their sources, ancestor first. Inherited entries are verified after source changes; inheritance stays enabled.' @{ + sources = @($sources | ForEach-Object { $_.Object }); affectedPaths = @($packageTargets | ForEach-Object { $_.Object }) + } + foreach ($target in $sources) { + Save-AclBackup -FullPath $target.Object -Directory $Out -Root $AllowRoot + $sids = @($target.Aces | Where-Object { $_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID -and -not $_.inherited } | ForEach-Object { $_.sid } | Sort-Object -Unique) + foreach ($sid in $sids) { + $repairPath = $target.Object + Invoke-ReportedOperation remove_package_allow $repairPath "Remove the observed package allow ACE for $sid as requested by -Fix; preserve deny ACEs and all other principals." acl { + $output = @(icacls $repairPath /remove:g "*$sid" 2>&1) + if ($LASTEXITCODE -ne 0) { throw "icacls removal exit ${LASTEXITCODE}: $($output -join "`n")" } + } + } + } + foreach ($target in $packageTargets) { + $before = $target.AclLines + $after = Get-ObjectFacts -FullPath $target.Object -MeSid $meSid + # icacls prefixes only the first ACE with the path, so compare entries + # without that prefix when checking for collateral changes. + $keepBefore = @($before | ForEach-Object { + $line = $_.Trim() + if ($line.StartsWith($target.Object, [System.StringComparison]::OrdinalIgnoreCase)) { $line = $line.Substring($target.Object.Length).Trim() } + $line + } | Where-Object { $_ -notmatch $PACKAGE_SID }) + $keepAfter = @($after.AclLines | ForEach-Object { + $line = $_.Trim() + if ($line.StartsWith($target.Object, [System.StringComparison]::OrdinalIgnoreCase)) { $line = $line.Substring($target.Object.Length).Trim() } + $line + } | Where-Object { $_ -notmatch $PACKAGE_SID }) + # A package deny can share the removed allow's SID. Preserve it explicitly + # even though native listing comparison omits lines naming package SIDs. + $expectedAces = @($target.Aces | Where-Object { -not ($_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID) }) + $collateral = (($keepBefore -join "`n") -eq ($keepAfter -join "`n")) -and + (($expectedAces | ConvertTo-Json -Compress) -eq ($after.Aces | ConvertTo-Json -Compress)) + $verified = $after.Readable -and $after.Errors.Count -eq 0 -and $after.PackageAces.Count -eq 0 -and $collateral + Write-Report verification fix $target.Object $(if ($verified) { 'verified' } else { 'failed' }) 'The removal command completed; verify that package allow ACEs disappeared and other ACL listing entries remained unchanged. On failure, restore this invocation and stop.' @{ + remainingPackageAces = $after.PackageAces; otherEntriesUnchanged = $collateral + } + if ($verified) { + $explicitSids = @($target.Aces | Where-Object { $_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID -and -not $_.inherited } | ForEach-Object { $_.sid } | Sort-Object -Unique) + foreach ($sid in $explicitSids) { Write-Line ('FIXED {0} SID={1}' -f $target.Object, $sid); $fixed++ } + } else { + Write-Line ('FIX_FAILED {0} collateral_change={1}' -f $target.Object, (-not $collateral)) + $refused++ + break paths + } + } + } +} catch { + $exitCode = if ($_.Exception -is [System.ArgumentException]) { 2 } else { 1 } + Write-Report error $mode $currentPath stopped 'Execution stopped after an exception. An interrupted write may have changed state; attempted repairs will be restored before the final summary.' @{ error = (Get-HResultChain $_.Exception); location = $_.InvocationInfo.PositionMessage } +} finally { + if ($refused -gt 0 -and $exitCode -eq 0) { $exitCode = 2 } + if ($exitCode -ne 0 -and -not $Restore) { + for ($i = $recoveries.Count - 1; $i -ge 0; $i--) { + $recovery = $recoveries[$i] + if (-not $recovery.attempted) { continue } + Write-Decision rollback $recovery.path selected 'The repair failed; restore every attempted DACL change from this invocation in reverse order before stopping.' + try { + Restore-SavedDacl -FullPath $recovery.path -Record $recovery.record -Root $AllowRoot + $recovery.restored = $true + $rollbackStatus = 'verified' + } catch { + $rollbackStatus = 'failed' + Write-Report error rollback $recovery.path stopped 'Recovery was not verified. Stop repairs and retain the pending recovery commands in reverse order.' @{ error = (Get-HResultChain $_.Exception) } + break + } + } + } + $pending = @($recoveries | Where-Object { $_.attempted -and -not $_.restored } | ForEach-Object { $_.command }) + [array]::Reverse($pending) + $nextAction = if ($rollbackStatus -eq 'failed') { 'restore_pending_then_stop' } elseif ($exitCode -ne 0 -or $observationFailures -gt 0 -or $Restore) { 'stop' } elseif ($mode -eq 'diagnose') { 'review_findings' } else { 'verify_original_confined_operation' } + Write-Line ('SUMMARY FIXED={0} GRANTED={1} REFUSED={2} RESTORED={3}' -f $fixed, $granted, $refused, $restored) + $status = if ($exitCode -ne 0) { 'failed' } elseif ($observationFailures -gt 0) { 'partial' } else { 'completed' } + Write-Report summary $mode $currentPath $status 'Operation completion records API execution; verification records the observed result. Only rerunning the original confined operation can confirm its failure is resolved.' @{ + exitCode = $exitCode; fixed = $fixed; granted = $granted; refused = $refused; restored = $restored + operations = @($operations.ToArray()); recoveries = @($recoveries.ToArray()); automaticRollback = $true; observationFailures = $observationFailures + rollback = $rollbackStatus; rollbackCommands = $pending; nextAction = $nextAction; report = $reportPath + } + if ($reportWriter) { $reportWriter.Dispose() } + if ($Compact) { Write-CompactSummary -Status $status -NextAction $nextAction -RollbackCommands $pending } +} +exit $exitCode diff --git a/packages/sandbox/sandbox-windows-acl/package.json b/packages/sandbox/sandbox-windows-acl/package.json index 241cf915fc..b14c87975c 100644 --- a/packages/sandbox/sandbox-windows-acl/package.json +++ b/packages/sandbox/sandbox-windows-acl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-windows-acl", "description": "Windows ACL write-restriction sandbox backend (restricted-token spawn with capability-SID write allowlist) for the DeepSeek Harness sandbox seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, @@ -29,22 +29,28 @@ "lib/index.js", "lib/runner.js", "lib/types-*.js", + "assets", "lib/types/**/*.d.ts" ], "license": "MIT", "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-skill": "workspace:*", "@deepseek-ai/dsh-subprocess": "workspace:*" }, "dependencies": { "@deepseek-ai/dsh-lazy-require": "workspace:*", "@deepseek-ai/dsh-win32-process": "workspace:*", - "koffi": "3.1.1" + "koffi": "3.1.1", + "yaml": "^2.4.2" }, "devDependencies": { + "@deepseek-ai/cordis-plugin-loader": "workspace:~", + "@deepseek-ai/cordis-plugin-include": "workspace:~", "@deepseek-ai/dsh-pwsh-local": "workspace:*", "@deepseek-ai/dsh-sandbox-local": "workspace:*", "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-skill": "workspace:*", "@deepseek-ai/dsh-subprocess": "workspace:*", "@deepseek-ai/dsh-subprocess-local": "workspace:*" } diff --git a/packages/sandbox/sandbox-windows-acl/src/acl-skill.ts b/packages/sandbox/sandbox-windows-acl/src/acl-skill.ts new file mode 100644 index 0000000000..67c5475ca7 --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/src/acl-skill.ts @@ -0,0 +1,76 @@ +/** + * Registers the bundled Windows sandbox ACL diagnosis skill. + * + * The provider owns a private filesystem copy of its resources so external + * PowerShell can execute them even when the package lives inside ASAR or SEA. + * Disposing the registration removes both the provider and its resource copy. + * + * @module @deepseek-ai/dsh-sandbox-windows-acl + */ + +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { Context } from '@deepseek-ai/cordis' +import { BUNDLED_SKILL_RANK, type SkillCandidate, type SkillProvider } from '@deepseek-ai/dsh-skill' +import { parse as parseYaml } from 'yaml' + +/** Bundled skill that diagnoses Windows sandbox ACL failures. */ +export const ACL_DIAGNOSIS_SKILL = 'diagnose-windows-sandbox-acl' + +/** Provider name this module registers the skill under. */ +const PROVIDER = 'dsh-windows-acl' + +function parseSkill(raw: string, path: string): { description: string; content: string } { + const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(raw) + if (frontmatter?.[1] === undefined) throw new Error(`dsh-sandbox-windows-acl: ${path} has no YAML frontmatter`) + const metadata: unknown = parseYaml(frontmatter[1]) + const description = typeof metadata === 'object' && metadata !== null && 'description' in metadata + ? metadata.description + : undefined + if (typeof description !== 'string' || description.length === 0) { + throw new Error(`dsh-sandbox-windows-acl: ${path} has no description`) + } + return { description, content: raw.slice(frontmatter[0].length).trim() } +} + +/** + * Register the bundled diagnosis skill with a private resource directory owned by this fiber. + * Missing or invalid packaged assets fail registration; disposal removes the directory. + * @param ctx - Context carrying the skill registry. + */ +export function registerAclDiagnosisSkill(ctx: Context): void { + const packaged = fileURLToPath(new URL(`../assets/${ACL_DIAGNOSIS_SKILL}/`, import.meta.url)) + ctx.effect(function* () { + const directory = mkdtempSync(join(tmpdir(), 'dsh-acl-skill-')) + yield async () => { await rm(directory, { recursive: true, force: true }) } + mkdirSync(join(directory, 'scripts')) + for (const path of ['SKILL.md', 'scripts/diagnose-windows-sandbox-acl.ps1']) { + writeFileSync(join(directory, path), readFileSync(join(packaged, path)), { flag: 'wx', mode: 0o600 }) + } + const locator = join(directory, 'SKILL.md') + const { description } = parseSkill(readFileSync(locator, 'utf8'), locator) + const candidate: SkillCandidate = { + name: ACL_DIAGNOSIS_SKILL, + description, + invocation: { modelInvocable: true, userInvocable: true }, + provider: PROVIDER, + source: 'bundled', + rank: BUNDLED_SKILL_RANK, + resourceBase: { kind: 'directory', path: directory }, + locator, + } + const provider: SkillProvider = { + name: PROVIDER, + list: () => Promise.resolve([candidate]), + async get(entry, options) { + const { rank: _rank, locator: entryPath, ...summary } = entry + const raw = await readFile(entryPath as string, { encoding: 'utf8', signal: options.signal }) + return { ...summary, content: parseSkill(raw, entryPath as string).content } + }, + } + yield ctx.skills.registerProvider(() => provider) + }, 'Windows ACL diagnosis skill resources') +} diff --git a/packages/sandbox/sandbox-windows-acl/src/index.ts b/packages/sandbox/sandbox-windows-acl/src/index.ts index 75dd23e7a5..5de5d47b72 100644 --- a/packages/sandbox/sandbox-windows-acl/src/index.ts +++ b/packages/sandbox/sandbox-windows-acl/src/index.ts @@ -58,6 +58,7 @@ import * as abi from './win32-abi.ts' export { AclWriteGrant } from './grant.ts' export { assertTempRootOutsideWorkspace } from './path-boundary.ts' export { tempWriteSid, workspaceWriteSid } from './workspace-sid.ts' +export { ACL_DIAGNOSIS_SKILL, registerAclDiagnosisSkill } from './acl-skill.ts' /** Construction options: the workspace/temp allowlists and their distinct SID identities. */ export interface AclSandboxOptions { /** Directories the confined child may write into (must exist and be caller-owned). */ diff --git a/packages/sandbox/sandbox-windows-acl/tests/acl-skill.spec.ts b/packages/sandbox/sandbox-windows-acl/tests/acl-skill.spec.ts new file mode 100644 index 0000000000..0a540827ed --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/tests/acl-skill.spec.ts @@ -0,0 +1,69 @@ +/** The bundled skill owns its registry candidate and extracted resource directory. */ + +import { Context } from '@deepseek-ai/cordis' +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import SkillRegistry from '@deepseek-ai/dsh-skill' +import { describe, expect, it } from 'vitest' +import { ACL_DIAGNOSIS_SKILL, registerAclDiagnosisSkill } from '../src/acl-skill.ts' + +describe('bundled Windows ACL diagnosis skill', () => { + it('registers the packaged body and removes the candidate on disposal', async () => { + const ctx = new Context() + try { + await ctx.plugin(SkillRegistry) + const fiber = await ctx.plugin({ + name: 'acl-skill-registration', + inject: ['skills'], + apply: (inner: Context) => { registerAclDiagnosisSkill(inner) }, + }) + + const catalog = await ctx.skills.list() + expect(catalog.map(skill => skill.name)).toEqual([ACL_DIAGNOSIS_SKILL]) + const entry = catalog[0]! + expect(entry.description.length).toBeGreaterThan(0) + expect(entry.description.length).toBeLessThanOrEqual(500) + expect(entry).toMatchObject({ + source: 'bundled', + provider: 'dsh-windows-acl', + invocation: { modelInvocable: true, userInvocable: true }, + }) + + const loaded = await ctx.skills.get(ACL_DIAGNOSIS_SKILL) + expect(loaded?.resourceBase).toMatchObject({ kind: 'directory' }) + if (loaded?.resourceBase?.kind !== 'directory') throw new Error('Missing physical skill resource directory') + const resourceDirectory = loaded.resourceBase.path + expect(readFileSync(join(resourceDirectory, 'scripts/diagnose-windows-sandbox-acl.ps1'), 'utf8')) + .toBe(readFileSync(new URL('../assets/diagnose-windows-sandbox-acl/scripts/diagnose-windows-sandbox-acl.ps1', import.meta.url), 'utf8')) + // The body names the bundled script — the repair path the model must use + // instead of editing ACLs by hand. + expect(loaded?.content).toContain('scripts\\diagnose-windows-sandbox-acl.ps1') + // Leave room for the tool wrapper below the default 8192-character prune threshold. + expect(loaded?.content.length).toBeLessThan(7_000) + + await fiber.dispose() + expect(await ctx.skills.list()).toEqual([]) + expect(existsSync(resourceDirectory)).toBe(false) + } finally { + await ctx.fiber.dispose() + } + }) + + it.each([ + ['missing frontmatter', '# Invalid skill', 'has no YAML frontmatter'], + ['non-object metadata', '---\nnull\n---\nBody', 'has no description'], + ['empty description', '---\ndescription: ""\n---\nBody', 'has no description'], + ])('rejects an extracted skill with %s', async (_name, raw, message) => { + const ctx = new Context() + try { + await ctx.plugin(SkillRegistry) + await ctx.plugin({ name: 'acl-skill-invalid-resource', inject: ['skills'], apply: registerAclDiagnosisSkill }) + const skill = await ctx.skills.get(ACL_DIAGNOSIS_SKILL) + if (skill?.resourceBase?.kind !== 'directory') throw new Error('Missing physical skill resource directory') + writeFileSync(join(skill.resourceBase.path, 'SKILL.md'), raw) + await expect(ctx.skills.get(ACL_DIAGNOSIS_SKILL)).rejects.toThrow(message) + } finally { + await ctx.fiber.dispose() + } + }) +}) diff --git a/packages/sandbox/sandbox-windows-acl/tests/diagnose-script.spec.ts b/packages/sandbox/sandbox-windows-acl/tests/diagnose-script.spec.ts new file mode 100644 index 0000000000..49a3d55b42 --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/tests/diagnose-script.spec.ts @@ -0,0 +1,753 @@ +/** + * The diagnosis script shipped with the `diagnose-windows-sandbox-acl` skill is a + * runnable artifact, so its classification and its two repairs are tested directly + * rather than through a model. + * + * Every case builds its own scratch directory under the system temp directory and + * never touches the user profile. The foreign package SID is synthetic: the access + * check that blocks a below-Medium caller keys on the SID class, not on a profile + * that exists, so a made-up `S-1-15-2-*` value exercises the same path. ACEs are + * written with `icacls` and the `*SID` spelling, which no account name has to + * resolve. + * + * Cleanup restores full control on the fixture directories before removing them. + */ + +import { execFileSync } from 'node:child_process' +import { mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +const isWin32 = process.platform === 'win32' + +function pwshAvailable(): boolean { + try { + execFileSync('where.exe', ['pwsh'], { stdio: 'ignore' }) + return true + } catch { + return false + } +} + +const script = fileURLToPath(new URL('../assets/diagnose-windows-sandbox-acl/scripts/diagnose-windows-sandbox-acl.ps1', import.meta.url)) +const PACKAGE_SID = 'S-1-15-2-1-2-3-4' +const CAPABILITY_SID = 'S-1-15-3-1-2-3-4' +// Process creation and Add-Type compilation share the Windows coverage budget. +const timeout = Math.max(90_000, Number(process.env.DSH_COVERAGE_TEST_TIMEOUT_MS ?? 0)) + +// Vitest's asymmetric factories return any; expected matchers are opaque values. +const containingObject = (value: Record): unknown => expect.objectContaining(value) +const containingArray = (value: unknown[]): unknown => expect.arrayContaining(value) +const excludingArray = (value: unknown[]): unknown => expect.not.arrayContaining(value) +const containingString = (value: string): unknown => expect.stringContaining(value) +const anyValue = (constructor: object): unknown => expect.any(constructor) + +interface ScriptRun { + readonly code: number + readonly output: string +} + +interface ScriptReport { + readonly kind: string + readonly operation: string + readonly path: string + readonly status: string + readonly reason: string + readonly details: Record +} + +function reports(run: ScriptRun): ScriptReport[] { + const result = run.output.split(/\r?\n/u).filter(line => line.startsWith('REPORT ')) + .map(line => JSON.parse(line.slice('REPORT '.length)) as ScriptReport) + expect(result.length).toBeGreaterThan(0) + for (const entry of result) expect(entry.reason.length).toBeGreaterThan(0) + expect(result.at(-1)).toMatchObject({ kind: 'summary', details: { exitCode: run.code } }) + return result +} + +function pwsh(command: string): string { + return execFileSync('pwsh', ['/NoLogo', '/NonInteractive', '/NoProfile', '-Command', command], { encoding: 'utf8', timeout, windowsHide: true }) +} + +function runScript(args: readonly string[]): ScriptRun { + return runPowerShell(['-File', script, ...args]) +} + +function runPowerShell(args: readonly string[]): ScriptRun { + try { + const stdout = execFileSync('pwsh', ['/NoLogo', '/NonInteractive', '/NoProfile', ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + timeout, + windowsHide: true, + }) + return { code: 0, output: stdout } + } catch (error) { + const failure = error as { status?: number | null; stdout?: string; stderr?: string } + if (failure.status === null || failure.status === undefined) throw error + return { code: failure.status ?? -1, output: `${failure.stdout ?? ''}${failure.stderr ?? ''}` } + } +} + +function icacls(path: string, ...args: readonly string[]): string { + return execFileSync('icacls', [path, ...args], { encoding: 'utf8', timeout, windowsHide: true }) +} + +function sddlOf(path: string): string { + return pwsh(`(Get-Acl -LiteralPath '${path.replaceAll("'", "''")}').Sddl`).trim() +} + +function aclLines(path: string): string[] { + return icacls(path).split(/\r?\n/u).map(line => line.trim()).filter(line => line !== '') +} + +function ownerOf(path: string): string { + return pwsh(`(Get-Acl -LiteralPath '${path}').GetOwner([System.Security.Principal.SecurityIdentifier]).Value`).trim() +} + +function normalized(path: string, lines: readonly string[]): string[] { + return lines.map((line) => { + const trimmed = line.trim() + return trimmed.startsWith(path) ? trimmed.slice(path.length).trim() : trimmed + }) +} + +describe.skipIf(!isWin32 || !pwshAvailable())('diagnose-windows-sandbox-acl script', { timeout }, () => { + let scratch!: string + let outDir!: string + let meSid!: string + + function makeDir(name: string): string { + const dir = join(scratch, name) + mkdirSync(dir, { recursive: true }) + return dir + } + + function stamp(path: string, sid: string, type: 'grant' | 'deny' = 'grant'): void { + icacls(path, `/${type}`, `*${sid}:(RX)`) + } + + beforeAll(() => { + // Windows runners may expose TEMP through an 8.3 alias; PowerShell reports long paths. + scratch = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-acl-diagnose-'))) + outDir = makeDir('out') + meSid = pwsh('[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value').trim() + // Diagnosis inspects the real ancestor chain, including above our owned root. + const baseline = reports(runScript(['-Path', scratch])).find(entry => entry.operation === 'classify') + if (JSON.stringify(baseline?.details.packageObjects) !== '[]') { + throw new Error('ACL fixtures require TEMP/TMP on a tree whose ancestors have no individual package allow ACEs; choose a clean test temp root. Host ACLs are never repaired by this suite.') + } + }, timeout) + + afterAll(() => { + // Restore what the grant case withheld before deleting the scratch tree. + pwsh( + `Get-ChildItem -LiteralPath '${scratch}' -Recurse -Force -Directory -ErrorAction SilentlyContinue | ` + + `ForEach-Object { icacls $_.FullName /grant:r "*${meSid}:(F)" | Out-Null }; ` + + `icacls '${scratch}' /grant:r "*${meSid}:(F)" | Out-Null`, + ) + rmSync(scratch, { recursive: true, force: true }) + }, timeout) + + it('names a foreign package-SID ACE as the blocker and removes only that ACE', () => { + const target = makeDir('stamped') + stamp(target, PACKAGE_SID) + const before = normalized(target, aclLines(target)) + expect(before.join('\n')).toContain(PACKAGE_SID) + + const diagnosis = runScript(['-Path', target]) + expect(diagnosis.output, diagnosis.output).toContain('VERDICT=CULPRIT') + expect(diagnosis.output, diagnosis.output).toContain(`PACKAGE_ACE SID=${PACKAGE_SID}`) + + const repair = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-Fix']) + expect(repair.output, repair.output).toContain(`FIXED ${target} SID=${PACKAGE_SID}`) + expect(repair.output, repair.output).toContain('SUMMARY FIXED=1 GRANTED=0 REFUSED=0') + expect(reports(repair)).toEqual(containingArray([ + containingObject({ kind: 'action', operation: 'remove_package_allow', path: target, status: 'started' }), + containingObject({ kind: 'action', operation: 'remove_package_allow', path: target, status: 'completed' }), + containingObject({ kind: 'verification', operation: 'fix', path: target, status: 'verified' }), + ])) + + // Exactly the foreign ACE disappears; every other line survives unchanged. + expect(normalized(target, aclLines(target))).toEqual(before.filter(line => !line.includes(PACKAGE_SID))) + }, timeout) + + it('reports a healthy directory as not this class and changes nothing', () => { + const target = makeDir('healthy') + const before = normalized(target, aclLines(target)) + + const diagnosis = runScript(['-Path', target]) + expect(diagnosis.output, diagnosis.output).toContain('VERDICT=NOT_THIS_CLASS') + expect(diagnosis.output, diagnosis.output).not.toContain('PACKAGE_ACE') + expect(reports(diagnosis)).toContainEqual(containingObject({ kind: 'decision', operation: 'diagnose', path: target, status: 'skipped' })) + + const repair = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-Fix']) + expect(repair.output, repair.output).toContain('SUMMARY FIXED=0 GRANTED=0 REFUSED=0') + expect(reports(repair)).toContainEqual(containingObject({ kind: 'decision', operation: 'fix', path: target, status: 'skipped' })) + expect(normalized(target, aclLines(target))).toEqual(before) + }, timeout) + + it('reports every inspected ancestor and locates a package ACE present only on the parent', async () => { + const parent = makeDir('parent-package-report') + const target = join(parent, 'child') + mkdirSync(target) + stamp(parent, PACKAGE_SID) + const before = [sddlOf(parent), sddlOf(target)] + + const diagnosis = runScript(['-Path', target]) + expect(diagnosis.code, diagnosis.output).toBe(0) + const entries = reports(diagnosis) + const inspected = entries.filter(entry => entry.kind === 'observation' && entry.operation === 'inspect_acl') + const expectedPaths = [target] + for (let ancestor = dirname(target); ; ancestor = dirname(ancestor)) { + expectedPaths.push(ancestor) + if (dirname(ancestor) === ancestor) break + } + expect(inspected.map(entry => entry.path)).toEqual(expectedPaths) + expect(inspected[0]).toMatchObject({ + path: target, status: 'read', + details: { aces: excludingArray([containingObject({ sid: PACKAGE_SID })]) }, + }) + expect(inspected[1]).toMatchObject({ + path: parent, status: 'read', + details: { aces: containingArray([containingObject({ sid: PACKAGE_SID, type: 'Allow', inherited: false })]) }, + }) + expect(entries).toContainEqual(containingObject({ + kind: 'decision', operation: 'classify', path: target, status: 'CULPRIT', + details: containingObject({ packageObjects: [parent] }), + })) + expect(entries.filter(entry => entry.kind === 'action' && entry.details.effect !== 'none')).toEqual([]) + expect([sddlOf(parent), sddlOf(target)]).toEqual(before) + + // The example retains both fixture paths and the fixture SID. Host-owned ACEs + // and ancestors remain covered by the assertions above instead of the snapshot. + const transcript = entries.filter(entry => entry.path === parent || entry.path === target).map((entry) => { + let details: Record = {} + if (entry.operation === 'inspect_acl') { + details = { fixturePackageAces: (entry.details.aces as { readonly sid: string }[]).filter(ace => ace.sid === PACKAGE_SID) } + } else if (entry.operation === 'classify') { + details = { packageObjects: entry.details.packageObjects } + } else if (entry.kind === 'summary') { + details = { + exitCode: entry.details.exitCode, operations: entry.details.operations, automaticRollback: entry.details.automaticRollback, + } + } + return JSON.stringify({ ...entry, details }) + }).join('\n').replaceAll(JSON.stringify(parent).slice(1, -1), '{{parent}}') + '\n' + await expect(transcript).toMatchFileSnapshot(fileURLToPath(new URL('./expected/parent-package-report.jsonl', import.meta.url))) + }, timeout) + + it('reports an inert capability SID and an inert DENY ACE without touching either', () => { + const capability = makeDir('capability') + stamp(capability, CAPABILITY_SID) + const deny = makeDir('denied-package') + stamp(deny, PACKAGE_SID, 'deny') + const before = new Map([ + [capability, normalized(capability, aclLines(capability))], + [deny, normalized(deny, aclLines(deny))], + ]) + + const capabilityDiagnosis = runScript(['-Path', capability]) + expect(capabilityDiagnosis.output, capabilityDiagnosis.output).toContain(`OTHER_S1_15 SID=${CAPABILITY_SID}`) + expect(capabilityDiagnosis.output, capabilityDiagnosis.output).not.toContain('PACKAGE_ACE') + expect(capabilityDiagnosis.output, capabilityDiagnosis.output).not.toContain('VERDICT=CULPRIT') + + const denyDiagnosis = runScript(['-Path', deny]) + expect(denyDiagnosis.output, denyDiagnosis.output).not.toContain('PACKAGE_ACE') + expect(denyDiagnosis.output, denyDiagnosis.output).not.toContain('VERDICT=CULPRIT') + expect(reports(denyDiagnosis)).toContainEqual(containingObject({ + kind: 'observation', operation: 'inspect_acl', path: deny, + details: containingObject({ aces: containingArray([containingObject({ sid: PACKAGE_SID, type: 'Deny', inherited: false })]) }), + })) + + // `pwsh -File` passes `-Path a,b` literally, so each object is diagnosed on its own. + for (const path of [capability, deny]) { + const repair = runScript(['-Path', path, '-AllowRoot', scratch, '-Out', outDir, '-Fix']) + expect(repair.output, repair.output).toContain('SUMMARY FIXED=0 GRANTED=0 REFUSED=0') + expect(normalized(path, aclLines(path))).toEqual(before.get(path)) + } + }, timeout) + + it('grants full control for a Modify-only DACL without changing the owner', () => { + const target = makeDir('missing-write-owner') + pwsh(`icacls '${target}' /inheritance:r /grant:r "*${meSid}:(M)" | Out-Null`) + const ownerBefore = ownerOf(target) + + const diagnosis = runScript(['-Path', target]) + expect(diagnosis.output, diagnosis.output).toContain('VERDICT=PRECONDITION') + + const repair = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-GrantFullControl']) + expect(repair.output, repair.output).toContain('SUMMARY FIXED=0 GRANTED=1 REFUSED=0') + expect(aclLines(target).join('\n')).toMatch(/\(F\)/u) + expect(ownerOf(target)).toBe(ownerBefore) + }, timeout) + + it('refuses a target outside -AllowRoot and refuses contradictory switches', () => { + const target = makeDir('outside-root') + const unrelated = makeDir('unrelated-root') + stamp(target, PACKAGE_SID) + + const refused = runScript(['-Path', target, '-AllowRoot', unrelated, '-Out', outDir, '-Fix']) + expect(refused.output, refused.output).toContain('FIX_REFUSED') + expect(refused.output, refused.output).toContain('is outside -AllowRoot') + expect(refused.output, refused.output).toContain('SUMMARY FIXED=0 GRANTED=0 REFUSED=1') + expect(aclLines(target).join('\n')).toContain(PACKAGE_SID) + + const contradictory = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-Fix', '-GrantFullControl']) + expect(contradictory.code).toBe(2) + expect(contradictory.output, contradictory.output).toContain('run one at a time') + expect(reports(contradictory)).toContainEqual(containingObject({ kind: 'error', status: 'stopped' })) + }, timeout) + + it.each(['-Fix', '-GrantFullControl'])('refuses ancestor junctions for %s without changing their destination', (repairSwitch) => { + const allowed = makeDir(`junction-${repairSwitch}`) + const outside = makeDir(`outside-${repairSwitch}`) + const target = join(outside, 'target') + mkdirSync(target) + icacls(target, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + stamp(target, PACKAGE_SID) + const before = sddlOf(target) + const link = join(allowed, 'link') + symlinkSync(outside, link, 'junction') + try { + for (const allowRoot of [allowed, link]) { + const repair = runScript(['-Path', join(link, 'target'), '-AllowRoot', allowRoot, '-Out', outDir, repairSwitch]) + expect(repair.output, repair.output).toContain('reparse point') + expect(repair.code).not.toBe(0) + expect(reports(repair)).toContainEqual(containingObject({ kind: 'decision', status: 'refused', reason: containingString('reparse point') })) + expect(sddlOf(target)).toBe(before) + } + } finally { + unlinkSync(link) + } + }) + + it.each(['-Fix', '-GrantFullControl'])('restores the original DACL with the rollback emitted by %s', (repairSwitch) => { + const target = makeDir(`rollback-${repairSwitch}'s-directory`) + icacls(target, '/setintegritylevel', 'L') + if (repairSwitch === '-GrantFullControl') icacls(target, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + else icacls(target, '/grant', `*${PACKAGE_SID}:(OI)(CI)(RX)`) + icacls(target, '/grant', `*${CAPABILITY_SID}:(OI)(CI)(RX)`) + const before = sddlOf(target) + const linesBefore = normalized(target, aclLines(target)).sort() + const repair = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, repairSwitch]) + expect(repair.code, repair.output).toBe(0) + expect(sddlOf(target)).not.toBe(before) + const rollback = repair.output.split(/\r?\n/u).find(line => line.startsWith('ROLLBACK ')) + expect(rollback, repair.output).toBeDefined() + const restored = pwsh(rollback!.slice('ROLLBACK '.length)) + expect(reports({ code: 0, output: restored })).toContainEqual(containingObject({ kind: 'verification', operation: 'restore', status: 'verified' })) + expect(sddlOf(target)).toBe(before) + expect(normalized(target, aclLines(target)).sort()).toEqual(linesBefore) + }, timeout) + + it('repairs a directory whose full-control ACE only applies to children', () => { + const target = makeDir('inherit-only-full-control') + icacls(target, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + icacls(target, '/grant', `*${meSid}:(OI)(CI)(IO)(F)`) + const diagnosis = runScript(['-Path', target]) + expect(diagnosis.output, diagnosis.output).toContain('VERDICT=PRECONDITION') + const repair = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-GrantFullControl']) + expect(repair.output, repair.output).toContain('SUMMARY FIXED=0 GRANTED=1 REFUSED=0') + icacls(target, '/setintegritylevel', 'L') + }) + + it('refuses a recovery record for another path or a path outside the allowed root', () => { + const target = makeDir('restore-guards') + const other = makeDir('restore-other') + const backups = makeDir('restore-backups') + icacls(target, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + const repair = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', backups, '-GrantFullControl']) + expect(repair.code, repair.output).toBe(0) + const record = join(backups, readdirSync(backups).find(file => file.endsWith('.json'))!) + const before = sddlOf(target) + const otherBefore = sddlOf(other) + const mismatch = runScript(['-Path', other, '-AllowRoot', scratch, '-Restore', record]) + expect(mismatch.code).toBe(2) + expect(mismatch.output).toContain('backup does not describe the requested path') + expect(sddlOf(other)).toBe(otherBefore) + const outside = runScript(['-Path', target, '-AllowRoot', other, '-Restore', record]) + expect(outside.code).toBe(2) + expect(outside.output).toContain('outside -AllowRoot') + expect(sddlOf(target)).toBe(before) + const link = join(other, 'link') + symlinkSync(target, link, 'junction') + try { + const throughLink = runScript(['-Path', link, '-AllowRoot', other, '-Restore', record]) + expect(throughLink.code).toBe(2) + expect(throughLink.output).toContain('reparse point') + expect(sddlOf(target)).toBe(before) + } finally { + unlinkSync(link) + } + }, timeout) + + it('does not report a successful grant when a deny ACE blocks WRITE_OWNER', async () => { + const target = makeDir('denied-write-owner') + icacls(target, '/deny', `*${meSid}:(WO)`) + const before = sddlOf(target) + try { + const diagnosis = runScript(['-Path', target]) + expect(diagnosis.output, diagnosis.output).toContain('VERDICT=PRECONDITION') + expect(reports(diagnosis)).toContainEqual(containingObject({ + kind: 'observation', operation: 'inspect_acl', path: target, + details: containingObject({ + writeOwner: false, + aces: containingArray([containingObject({ sid: meSid, type: 'Deny', rights: 'TakeOwnership', inherited: false })]), + }), + })) + const grant = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-GrantFullControl']) + expect(grant.code, `${grant.output}\n${aclLines(target).join('\n')}`).toBe(2) + expect(grant.output, grant.output).toContain('GRANT_FAILED') + expect(grant.output).toContain('GRANTED=0 REFUSED=1') + expect(aclLines(target).join('\n')).toContain('(DENY)(WO)') + expect(reports(grant)).toEqual(containingArray([ + containingObject({ kind: 'action', operation: 'grant_dacl', status: 'completed' }), + containingObject({ kind: 'verification', operation: 'grant', status: 'failed', details: containingObject({ recovery: containingString('-Restore') }) }), + containingObject({ kind: 'verification', operation: 'restore', status: 'verified' }), + containingObject({ kind: 'summary', details: containingObject({ automaticRollback: true, granted: 0, restored: 1, rollback: 'verified', rollbackCommands: [], nextAction: 'stop' }) }), + ])) + expect(sddlOf(target)).toBe(before) + const transcript = reports(grant) + .filter(entry => entry.path === target && entry.kind !== 'observation') + .map(entry => `${entry.kind} ${entry.operation} ${entry.status} path={{target}}: ${entry.reason.replaceAll(meSid, '{{caller_sid}}')}`) + .join('\n') + '\n' + await expect(transcript).toMatchFileSnapshot(fileURLToPath(new URL('./expected/denied-grant-report.txt', import.meta.url))) + } finally { + icacls(target, '/remove:d', `*${meSid}`) + } + }) + + it('reports backup errors and no ACL write when the output path is a file', () => { + const target = makeDir('backup-failure') + icacls(target, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + const outputFile = join(scratch, 'not-a-directory') + writeFileSync(outputFile, 'Existing contents') + const before = sddlOf(target) + const run = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outputFile, '-GrantFullControl']) + expect(run.code).toBe(1) + const entries = reports(run) + expect(entries).toContainEqual(containingObject({ kind: 'action', operation: 'backup', status: 'failed', details: containingObject({ error: anyValue(String) }) })) + expect(entries.filter(entry => entry.kind === 'action' && entry.details.effect === 'acl')).toEqual([]) + expect(sddlOf(target)).toBe(before) + }) + + it('reports unreadable ACLs as missing observations without inventing permissions', () => { + const target = makeDir('unreadable-observation') + const before = sddlOf(target) + const run = runPowerShell(['-Command', ` +function Get-Acl { param([string]$LiteralPath); throw [System.IO.IOException]::new('ACL observation unavailable') } +& '${script.replaceAll("'", "''")}' -Path '${target.replaceAll("'", "''")}' +exit $LASTEXITCODE +`]) + expect(run.code).toBe(0) + expect(run.output).toContain('VERDICT=UNREADABLE') + const entries = reports(run) + expect(entries).toContainEqual(containingObject({ + kind: 'observation', path: target, status: 'unreadable', + details: { error: containingString('ACL observation unavailable') }, + })) + expect(entries.at(-1)).toMatchObject({ status: 'partial', details: { observationFailures: anyValue(Number) } }) + expect(entries.filter(entry => entry.kind === 'action' && entry.details.effect !== 'none')).toEqual([]) + expect(sddlOf(target)).toBe(before) + }) + + it.each([false, true])('restores after a failed verification read and reports whether recovery was observed (recovery read fails: %s)', (recoveryReadFails) => { + const target = makeDir(`post-write-read-failure-${recoveryReadFails}`) + icacls(target, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + const before = sddlOf(target) + const quote = (value: string): string => `'${value.replaceAll("'", "''")}'` + const wrapper = join(scratch, 'fail-verification.ps1') + // The real grant runs; only its subsequent observation fails in this child. + writeFileSync(wrapper, ` +$global:targetReads = 0 +function Get-Acl { + param([string]$LiteralPath) + if ($LiteralPath -eq ${quote(target)}) { + $global:targetReads++ + if ($global:targetReads ${recoveryReadFails ? '-ge' : '-eq'} 4) { throw [System.IO.IOException]::new('verification read unavailable') } + } + Microsoft.PowerShell.Security\\Get-Acl -LiteralPath $LiteralPath +} +& ${quote(script)} -Path ${quote(target)} -AllowRoot ${quote(scratch)} -Out ${quote(outDir)} -GrantFullControl +exit $LASTEXITCODE +`) + const run = runPowerShell(['-File', wrapper]) + expect(run.code).toBe(2) + const entries = reports(run) + expect(entries).toEqual(containingArray([ + containingObject({ kind: 'action', operation: 'grant_dacl', status: 'completed' }), + containingObject({ kind: 'observation', operation: 'inspect_acl', path: target, status: 'unreadable', details: containingObject({ error: containingString('verification read unavailable') }) }), + containingObject({ kind: 'verification', operation: 'grant', status: 'failed', details: containingObject({ recovery: containingString('-Restore') }) }), + ])) + expect(sddlOf(target)).toBe(before) + expect(entries.at(-1)).toMatchObject({ details: { + rollback: recoveryReadFails ? 'failed' : 'verified', + nextAction: recoveryReadFails ? 'restore_pending_then_stop' : 'stop', + rollbackCommands: recoveryReadFails ? [containingString('-Restore')] : [], + } }) + if (recoveryReadFails) { + const commands = entries.at(-1)!.details.rollbackCommands as string[] + const recovery = runPowerShell(['-Command', commands[0]!]) + expect(recovery.code, recovery.output).toBe(0) + expect(reports(recovery)).toContainEqual(containingObject({ kind: 'verification', operation: 'restore', status: 'verified' })) + } + }, timeout) + + it('reports a missing path and an already-satisfied grant without mutating either', () => { + const missing = join(scratch, 'missing') + const run = runScript(['-Path', missing, '-AllowRoot', scratch, '-Out', outDir, '-GrantFullControl']) + expect(run.code).toBe(2) + expect(reports(run)).toContainEqual(containingObject({ kind: 'decision', path: missing, status: 'skipped', reason: containingString('does not exist') })) + const healthy = makeDir('already-satisfied') + const before = sddlOf(healthy) + const grant = runScript(['-Path', healthy, '-AllowRoot', scratch, '-Out', outDir, '-GrantFullControl']) + expect(grant.code, grant.output).toBe(0) + expect(reports(grant)).toContainEqual(containingObject({ kind: 'decision', operation: 'grant', status: 'skipped', reason: containingString('already available') })) + expect(sddlOf(healthy)).toBe(before) + }) + + it('reports well-known package groups without treating them as a removable package ACE', () => { + const target = makeDir('well-known-package-groups') + for (const sid of ['S-1-15-2-1', 'S-1-15-2-2']) stamp(target, sid) + const before = sddlOf(target) + const run = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-Fix']) + expect(run.code, run.output).toBe(0) + expect(run.output).toContain('VERDICT=NOT_THIS_CLASS') + expect(reports(run)).toContainEqual(containingObject({ + kind: 'observation', operation: 'inspect_acl', path: target, + details: containingObject({ aces: containingArray([ + containingObject({ sid: 'S-1-15-2-1', type: 'Allow' }), + containingObject({ sid: 'S-1-15-2-2', type: 'Allow' }), + ]) }), + })) + expect(sddlOf(target)).toBe(before) + }) + + it('grants missing rights before removing a package ACE when both problems are present', () => { + const target = makeDir('both') + icacls(target, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + stamp(target, PACKAGE_SID) + const ownerBefore = ownerOf(target) + const diagnosis = runScript(['-Path', target]) + expect(diagnosis.output, diagnosis.output).toContain('VERDICT=BOTH') + const grant = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-GrantFullControl']) + expect(grant.output, grant.output).toContain('SUMMARY FIXED=0 GRANTED=1 REFUSED=0') + expect(aclLines(target).join('\n')).toContain(PACKAGE_SID) + const fix = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-Fix']) + expect(fix.output, fix.output).toContain('SUMMARY FIXED=1 GRANTED=0 REFUSED=0') + expect(aclLines(target).join('\n')).not.toContain(PACKAGE_SID) + expect(ownerOf(target)).toBe(ownerBefore) + }) + + it('refuses package removal when a deny blocks the prerequisite and restores a failed grant', () => { + const target = makeDir('both-with-deny') + stamp(target, PACKAGE_SID) + icacls(target, '/deny', `*${meSid}:(WO)`) + const before = sddlOf(target) + try { + for (const mode of ['-Fix', '-GrantFullControl', '-Fix']) { + const run = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, mode, '-Compact']) + expect(run.code, run.output).toBe(2) + expect(reports(run).at(-1)).toMatchObject({ status: 'failed', details: { nextAction: 'stop', rollbackCommands: [] } }) + expect(sddlOf(target)).toBe(before) + } + } finally { + icacls(target, '/remove:d', `*${meSid}`) + } + }, timeout) + + it('removes multiple package allow SIDs before verifying the resulting ACL', () => { + const target = makeDir('multiple-packages') + const otherSid = 'S-1-15-2-4-3-2-1' + for (const sid of [PACKAGE_SID, otherSid]) stamp(target, sid) + icacls(target, '/deny', `*${PACKAGE_SID}:(WO)`) + const before = normalized(target, aclLines(target)) + const run = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', outDir, '-Fix']) + expect(run.code, run.output).toBe(0) + expect(normalized(target, aclLines(target))).toEqual(before.filter(line => line.includes('(DENY)') || (!line.includes(PACKAGE_SID) && !line.includes(otherSid)))) + expect(reports(run).filter(entry => entry.kind === 'verification' && entry.operation === 'fix')).toHaveLength(1) + }) + + it.each([false, true])('repairs inherited package entries at their sources and restores the tree (explicit child: %s)', (explicitChild) => { + const parent = makeDir(`inherited-${explicitChild}`) + const child = join(parent, 'child') + const leaf = join(child, 'leaf') + mkdirSync(leaf, { recursive: true }) + icacls(parent, '/setintegritylevel', '(OI)(CI)L') + icacls(parent, '/grant', `*${PACKAGE_SID}:(OI)(CI)(RX)`) + if (explicitChild) icacls(child, '/grant', '*S-1-15-2-4-3-2-1:(OI)(CI)(RX)') + const paths = [parent, child, leaf] + const before = paths.map(sddlOf) + // The fixture's owner is the host token's default owner, which is not the invoking user on an + // elevated runner; the repair must preserve whatever owner the fixture has. + const ownersBefore = paths.map(ownerOf) + const run = runScript(['-Path', leaf, '-AllowRoot', scratch, '-Out', outDir, '-Fix']) + expect(run.code, run.output).toBe(0) + const entries = reports(run) + expect(entries.filter(entry => entry.operation === 'remove_package_allow' && entry.status === 'completed').map(entry => entry.path)) + .toEqual(explicitChild ? [parent, child] : [parent]) + expect(entries.filter(entry => entry.kind === 'verification' && entry.operation === 'fix').map(entry => entry.path)).toEqual([leaf, child, parent]) + for (const path of paths) { + expect(aclLines(path).join('\n')).not.toContain('S-1-15-2-') + } + expect(paths.map(ownerOf)).toEqual(ownersBefore) + const commands = entries.at(-1)!.details.rollbackCommands as string[] + for (const command of commands) expect(runPowerShell(['-Command', command]).code).toBe(0) + expect(paths.map(sddlOf)).toEqual(before) + }) + + it('refuses an inherited source outside AllowRoot before touching an in-root explicit entry', () => { + const parent = makeDir('outside-inherited-source') + const root = join(parent, 'allowed') + const child = join(root, 'child') + mkdirSync(child, { recursive: true }) + icacls(parent, '/grant', `*${PACKAGE_SID}:(OI)(CI)(RX)`) + stamp(child, 'S-1-15-2-4-3-2-1') + const before = [parent, child].map(sddlOf) + const run = runScript(['-Path', child, '-AllowRoot', root, '-Out', outDir, '-Fix']) + expect(run.code).toBe(2) + expect(reports(run)).toContainEqual(containingObject({ kind: 'decision', status: 'refused', path: parent, reason: containingString('outside -AllowRoot') })) + expect(reports(run).filter(entry => entry.kind === 'action' && entry.details.effect === 'acl')).toEqual([]) + expect([parent, child].map(sddlOf)).toEqual(before) + }) + + it('restores explicit sources and inherited entries when final verification fails', () => { + const parent = makeDir('inherited-rollback') + const child = join(parent, 'child') + const leaf = join(child, 'leaf') + mkdirSync(leaf, { recursive: true }) + icacls(parent, '/grant', `*${PACKAGE_SID}:(OI)(CI)(RX)`) + icacls(child, '/grant', '*S-1-15-2-4-3-2-1:(OI)(CI)(RX)') + const paths = [parent, child, leaf] + const before = paths.map(sddlOf) + const quote = (value: string): string => `'${value.replaceAll("'", "''")}'` + const wrapper = join(scratch, 'fail-inherited-verification.ps1') + writeFileSync(wrapper, ` +$global:leafReads = 0 +function Get-Acl { + param([string]$LiteralPath) + if ($LiteralPath -eq ${quote(leaf)}) { + $global:leafReads++ + if ($global:leafReads -eq 2) { throw [System.IO.IOException]::new('final inherited verification unavailable') } + } + Microsoft.PowerShell.Security\\Get-Acl -LiteralPath $LiteralPath +} +& ${quote(script)} -Path ${quote(leaf)} -AllowRoot ${quote(scratch)} -Out ${quote(outDir)} -Fix +exit $LASTEXITCODE +`) + const run = runPowerShell(['-File', wrapper]) + expect(run.code, run.output).toBe(2) + expect(reports(run).filter(entry => entry.kind === 'verification' && entry.operation === 'restore').map(entry => entry.path)).toEqual([child, parent]) + expect(reports(run).at(-1)).toMatchObject({ details: { rollback: 'verified', nextAction: 'stop' } }) + expect(paths.map(sddlOf)).toEqual(before) + }) + + it.each(['-Fix', '-GrantFullControl', '-Restore'])('rejects equivalent spellings of AllowRoot through %s', (mode) => { + const target = makeDir(`root-spellings-${mode}`) + const backups = makeDir(`root-backups-${mode}`) + icacls(target, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + let record: string | undefined + if (mode === '-Restore') { + const grant = runScript(['-Path', target, '-AllowRoot', scratch, '-Out', backups, '-GrantFullControl']) + expect(grant.code, grant.output).toBe(0) + record = (reports(grant).at(-1)!.details.recoveries as { record: string }[])[0]!.record + } else if (mode === '-Fix') stamp(target, PACKAGE_SID) + const before = sddlOf(target) + for (const path of [target, `${target}\\`, `${target}/`, `${target}\\\\`, `${target}\\.`, target.toUpperCase()]) { + const args = ['-Path', path, '-AllowRoot', target, '-Out', backups, mode, ...(record ? [record] : [])] + const run = runScript(args) + expect(run.code, run.output).not.toBe(0) + expect(run.output).toContain('outside -AllowRoot') + expect(sddlOf(target)).toBe(before) + } + }) + + it.each(['Packages', 'WindowsApps'])('refuses managed application trees even inside AllowRoot (%s)', (directory) => { + const root = makeDir(`managed-${directory}`) + const managed = join(root, directory) + const child = join(managed, 'application') + mkdirSync(child, { recursive: true }) + stamp(child, PACKAGE_SID) + const key = directory === 'Packages' ? 'LOCALAPPDATA' : 'ProgramFiles' + const quote = (value: string): string => `'${value.replaceAll("'", "''")}'` + // PowerShell initializes ProgramFiles at startup; redirect only this child's lookup after startup. + const run = (path: string, mode: string): ScriptRun => runPowerShell(['-Command', + `$env:${key} = ${quote(root)}; & ${quote(script)} -Path ${quote(path)} -AllowRoot ${quote(root)} -Out ${quote(outDir)} ${mode}; exit $LASTEXITCODE`, + ]) + for (const path of [managed, child]) { + icacls(path, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + const before = sddlOf(path) + const grant = run(path, '-GrantFullControl') + expect(grant.code, grant.output).toBe(2) + expect(grant.output).toContain('managed application directory') + expect(sddlOf(path)).toBe(before) + } + const fix = run(child, '-Fix') + expect(fix.code, fix.output).toBe(2) + expect(fix.output).toContain('managed application directory') + expect(aclLines(child).join('\n')).toContain(PACKAGE_SID) + }) + + it('rolls back earlier paths too when a later grant fails in the same invocation', () => { + const first = makeDir('multi-grant-first') + const second = makeDir('multi-grant-second') + icacls(first, '/inheritance:r', '/grant:r', `*${meSid}:(M)`) + icacls(second, '/deny', `*${meSid}:(WO)`) + const before = [sddlOf(first), sddlOf(second)] + try { + const run = runPowerShell(['-Command', `& '${script}' -Path @('${first}', '${second}') -AllowRoot '${scratch}' -Out '${outDir}' -GrantFullControl; exit $LASTEXITCODE`]) + expect(run.code, run.output).toBe(2) + expect(reports(run).filter(entry => entry.operation === 'restore' && entry.kind === 'verification').map(entry => entry.path)).toEqual([second, first]) + expect([sddlOf(first), sddlOf(second)]).toEqual(before) + } finally { + icacls(second, '/remove:d', `*${meSid}`) + } + }, timeout) + + it('keeps all observations in unique reports and all ancestor paths in the compact summary', () => { + const parent = makeDir('compact-parent') + const target = join(parent, 'child') + mkdirSync(target) + stamp(parent, PACKAGE_SID) + const before = [sddlOf(parent), sddlOf(target)] + const reportPaths = new Set() + for (let attempt = 0; attempt < 2; attempt++) { + const run = runScript(['-Path', target, '-Out', outDir, '-Compact']) + expect(run.code, run.output).toBe(0) + const compact = reports(run) + expect(compact).toHaveLength(1) + const summary = compact[0]! + expect(summary.details).toMatchObject({ + inspectedPaths: containingArray([target, parent, dirname(parent)]), + findings: containingArray([containingObject({ path: parent, packageAllowSids: [PACKAGE_SID] })]), + nextAction: 'review_findings', + }) + const reportPath = summary.details.report as string + reportPaths.add(reportPath) + const full = readFileSync(reportPath, 'utf8').trim().split(/\r?\n/u).map(line => JSON.parse(line) as ScriptReport) + expect(full.filter(entry => entry.operation === 'inspect_acl').map(entry => entry.path)).toEqual(summary.details.inspectedPaths) + expect(full.every(entry => entry.reason.length > 0)).toBe(true) + expect(full.at(-1)).toMatchObject({ kind: 'summary', status: 'completed' }) + expect(run.output.length).toBeLessThan(5_120) + } + expect(reportPaths.size).toBe(2) + expect([sddlOf(parent), sddlOf(target)]).toEqual(before) + }, timeout) + + it('reports compact-output setup failures without changing the target or overwriting files', () => { + const target = makeDir('compact-failure') + const outputFile = join(scratch, 'existing-report-output') + writeFileSync(outputFile, 'preserve') + const before = sddlOf(target) + for (const args of [[], ['-Out', outputFile]]) { + const run = runScript(['-Path', target, '-Compact', ...args]) + expect(run.code, run.output).not.toBe(0) + expect(reports(run).at(-1)).toMatchObject({ status: 'failed', details: { nextAction: 'stop' } }) + } + expect(readFileSync(outputFile, 'utf8')).toBe('preserve') + expect(sddlOf(target)).toBe(before) + }) +}) diff --git a/packages/sandbox/sandbox-windows-acl/tests/expected/denied-grant-report.txt b/packages/sandbox/sandbox-windows-acl/tests/expected/denied-grant-report.txt new file mode 100644 index 0000000000..0e1ad3333b --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/tests/expected/denied-grant-report.txt @@ -0,0 +1,14 @@ +decision classify PRECONDITION path={{target}}: The caller cannot open the requested object with both WRITE_DAC and WRITE_OWNER. The reported allow and deny ACEs are evidence, not an attribution to one rule. +decision backup selected path={{target}}: All recovery files must be written successfully before attempting the ACL change. +action backup started path={{target}}: Save the original DACL and an independent recovery command before changing permissions. +action backup completed path={{target}}: Save the original DACL and an independent recovery command before changing permissions. +action grant_dacl started path={{target}}: WRITE_OWNER is missing and WRITE_DAC is available; add a FullControl allow ACE for {{caller_sid}} while preserving deny ACEs, owner and SACL. +action grant_dacl completed path={{target}}: WRITE_OWNER is missing and WRITE_DAC is available; add a FullControl allow ACE for {{caller_sid}} while preserving deny ACEs, owner and SACL. +verification grant failed path={{target}}: The DACL write completed; recheck effective access before claiming that provisioning can succeed. On failure, restore this invocation and stop. +decision rollback selected path={{target}}: The repair failed; restore every attempted DACL change from this invocation in reverse order before stopping. +action restore_dacl started path={{target}}: Restore the requested backup DACL and inheritance protection, preserving owner and SACL. +action restore_dacl completed path={{target}}: Restore the requested backup DACL and inheritance protection, preserving owner and SACL. +action read_restored_dacl started path={{target}}: Read the restored DACL to compare it with the saved record. +action read_restored_dacl completed path={{target}}: Read the restored DACL to compare it with the saved record. +verification restore verified path={{target}}: Compare the observed DACL with the recovery record after writing it. +summary grant failed path={{target}}: Operation completion records API execution; verification records the observed result. Only rerunning the original confined operation can confirm its failure is resolved. diff --git a/packages/sandbox/sandbox-windows-acl/tests/expected/parent-package-report.jsonl b/packages/sandbox/sandbox-windows-acl/tests/expected/parent-package-report.jsonl new file mode 100644 index 0000000000..c7b7c47d95 --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/tests/expected/parent-package-report.jsonl @@ -0,0 +1,5 @@ +{"kind":"observation","operation":"inspect_acl","path":"{{parent}}\\child","status":"read","reason":"Read the ACL and check effective WRITE_DAC and WRITE_OWNER; observed ACEs alone do not identify which rule caused a denial.","details":{"fixturePackageAces":[]}} +{"kind":"observation","operation":"inspect_acl","path":"{{parent}}","status":"read","reason":"Read the ACL and check effective WRITE_DAC and WRITE_OWNER; observed ACEs alone do not identify which rule caused a denial.","details":{"fixturePackageAces":[{"sid":"S-1-15-2-1-2-3-4","type":"Allow","rights":"ReadAndExecute, Synchronize","inherited":false,"inheritance":"None","propagation":"None"}]}} +{"kind":"decision","operation":"classify","path":"{{parent}}\\child","status":"CULPRIT","reason":"Package allow ACEs were observed. Required access checks determine whether the caller can perform the requested repair; other causes remain possible.","details":{"packageObjects":["{{parent}}"]}} +{"kind":"decision","operation":"diagnose","path":"{{parent}}\\child","status":"skipped","reason":"No mutation mode was requested; all reported ACLs were left unchanged.","details":{}} +{"kind":"summary","operation":"diagnose","path":"{{parent}}\\child","status":"completed","reason":"Operation completion records API execution; verification records the observed result. Only rerunning the original confined operation can confirm its failure is resolved.","details":{"exitCode":0,"operations":[{"id":1,"operation":"initialize","path":"","effect":"none","status":"completed"}],"automaticRollback":true}} diff --git a/packages/sandbox/sandbox-windows-acl/tests/fixtures/skill-composition/cordis.yml b/packages/sandbox/sandbox-windows-acl/tests/fixtures/skill-composition/cordis.yml new file mode 100644 index 0000000000..44222cbf73 --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/tests/fixtures/skill-composition/cordis.yml @@ -0,0 +1,4 @@ +- id: sandbox + name: '@deepseek-ai/dsh-sandbox-local' +- id: skills + name: '@deepseek-ai/dsh-skill' diff --git a/packages/sandbox/sandbox-windows-acl/tests/skill-composition.spec.ts b/packages/sandbox/sandbox-windows-acl/tests/skill-composition.spec.ts new file mode 100644 index 0000000000..5ab23646a1 --- /dev/null +++ b/packages/sandbox/sandbox-windows-acl/tests/skill-composition.spec.ts @@ -0,0 +1,63 @@ +/** The Windows-only provider registers through the shipped sandbox-local injection. */ +import { existsSync, readFileSync } from 'node:fs' +import { copyFile, mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { Context } from '@deepseek-ai/cordis' +import Loader from '@deepseek-ai/cordis-plugin-loader' +import Include from '@deepseek-ai/cordis-plugin-include' +import * as SkillRegistry from '@deepseek-ai/dsh-skill' +import * as SandboxLocal from '@deepseek-ai/dsh-sandbox-local' +import { expect, it } from 'vitest' +import { ACL_DIAGNOSIS_SKILL } from '../src/acl-skill.ts' + +// sandbox-local's POSIX suites are excluded on Windows; this owner runs in Windows coverage. +it.skipIf(process.platform !== 'win32')('loads and unloads the ACL skill through the real sandbox-local composition', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-acl-composition-')) + const ctx = new Context() + try { + // Loader persists disposal as disabled config; the committed seed stays read-only. + const configPath = join(root, 'cordis.yml') + await copyFile(new URL('./fixtures/skill-composition/cordis.yml', import.meta.url), configPath) + await ctx.plugin(Loader) + ctx.loader.builtins.include = Include + const unexpectedLoaderOperation = (): never => { throw new Error('This composition does not use Node HMR operations') } + ctx.loader.internal = { + version: 'v2', + loadCache: new Map(), + register: unexpectedLoaderOperation, + getOrCreateModuleJob: unexpectedLoaderOperation, + resolveSync: unexpectedLoaderOperation, + load: unexpectedLoaderOperation, + async import(specifier: string) { + if (specifier === '@deepseek-ai/dsh-skill') return SkillRegistry + if (specifier === '@deepseek-ai/dsh-sandbox-local') return SandboxLocal + throw new Error(`Unexpected Loader import: ${specifier}`) + }, + } + await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } }) + await ctx.loader.await() + expect((await ctx.skills.list()).map(entry => entry.name)).toEqual([ACL_DIAGNOSIS_SKILL]) + const loaded = await ctx.skills.get(ACL_DIAGNOSIS_SKILL) + expect(loaded).toMatchObject({ source: 'bundled', provider: 'dsh-windows-acl' }) + expect(loaded?.content).toContain('Stop after any failed/refused repair') + if (loaded?.resourceBase?.kind !== 'directory') throw new Error('Missing extracted skill resources') + const resources = loaded.resourceBase.path + expect(readFileSync(join(resources, 'scripts/diagnose-windows-sandbox-acl.ps1'), 'utf8')) + .toBe(readFileSync(fileURLToPath(new URL('../assets/diagnose-windows-sandbox-acl/scripts/diagnose-windows-sandbox-acl.ps1', import.meta.url)), 'utf8')) + const sandbox = [...ctx.loader.entries()].find(entry => entry.options.id === 'sandbox') + if (!sandbox?.fiber) throw new Error('Sandbox provider did not mount') + await sandbox.fiber.dispose() + expect(await ctx.skills.list()).toEqual([]) + expect(existsSync(resources)).toBe(false) + await ctx.loader.create({ + name: '@deepseek-ai/dsh-sandbox-local', + config: { runnerCommand: ['operator-runner'], runnerFailureSignatures: ['operator runner unavailable'] }, + }) + await ctx.loader.await() + expect(await ctx.skills.list()).toEqual([]) + } finally { + try { await ctx.fiber.dispose() } finally { await rm(root, { recursive: true, force: true }) } + } +}) diff --git a/packages/sandbox/sandbox-windows-acl/tsconfig.json b/packages/sandbox/sandbox-windows-acl/tsconfig.json index d6a476cce8..627fb316f1 100644 --- a/packages/sandbox/sandbox-windows-acl/tsconfig.json +++ b/packages/sandbox/sandbox-windows-acl/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../util/lazy-require" + }, + { + "path": "../../skill/skill" } ] } diff --git a/packages/sandbox/sandbox/package.json b/packages/sandbox/sandbox/package.json index a5c3d45ec1..039b9b9dcd 100644 --- a/packages/sandbox/sandbox/package.json +++ b/packages/sandbox/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox", "description": "Abstract process-sandbox seam (ctx.sandbox) for the DeepSeek Harness: same-world confinement vocabulary and the SandboxProvider contract", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/schedule/schedule/package.json b/packages/schedule/schedule/package.json index 3973e20dd4..7bb5d3de70 100644 --- a/packages/schedule/schedule/package.json +++ b/packages/schedule/schedule/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-schedule", "description": "Host-wide durable reminders with shared management and original-Session delivery", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/client/package.json b/packages/sdk/client/package.json index f1660bc8c1..d0ea69a3af 100644 --- a/packages/sdk/client/package.json +++ b/packages/sdk/client/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-client", "description": "TypeScript client SDK for driving a DeepSeek Harness runtime subprocess over stdio JSON-RPC: the DeepSeekHarness high-level turns API and the lower-level HarnessClient", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/protocol/package.json b/packages/sdk/protocol/package.json index e282f29b29..9250a440bc 100644 --- a/packages/sdk/protocol/package.json +++ b/packages/sdk/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-protocol", "description": "Shared wire protocol for the DeepSeek Harness SDK runtime: the newline-delimited JSON-RPC stdio transport and the named request, result, and notification types spoken between the runtime server and SDK clients", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/sdk/server/package.json b/packages/sdk/server/package.json index c9899da96e..59d5eb8db1 100644 --- a/packages/sdk/server/package.json +++ b/packages/sdk/server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sdk-jsonrpc-server", "description": "Stdio JSON-RPC server plugin for out-of-process DeepSeek Harness SDK clients", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-log-export/package.json b/packages/session-query/session-log-export/package.json index b9545681c5..8e400eff42 100644 --- a/packages/session-query/session-log-export/package.json +++ b/packages/session-query/session-log-export/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-export", "description": "Web Session-log export command and shared download dialog", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query-sqlite/package.json b/packages/session-query/session-query-sqlite/package.json index b4b6129c51..a0246e6306 100644 --- a/packages/session-query/session-query-sqlite/package.json +++ b/packages/session-query/session-query-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query-sqlite", "description": "Concrete ctx.sessionQuery backend with SQLite FTS5 search", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/session-query/package.json b/packages/session-query/session-query/package.json index 6710376620..9509e15dee 100644 --- a/packages/session-query/session-query/package.json +++ b/packages/session-query/session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-query", "description": "Combined session query service contract with concrete reads, traces, and filters", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session-query/tool-session-query/package.json b/packages/session-query/tool-session-query/package.json index a9e129463d..9c1bbe5b1e 100644 --- a/packages/session-query/tool-session-query/package.json +++ b/packages/session-query/tool-session-query/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-session-query", "description": "Workspace-authorized model-facing session history search, trace, and event read tools", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-checkpoint-policy/package.json b/packages/session/session-checkpoint-policy/package.json index 17b4d33d2e..25186f4004 100644 --- a/packages/session/session-checkpoint-policy/package.json +++ b/packages/session/session-checkpoint-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-checkpoint-policy", "description": "Semantic session durability checkpoints before model requests and tool side effects", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-catalog/package.json b/packages/session/session-format-catalog/package.json index 9d7138a2d0..5420961e95 100644 --- a/packages/session/session-format-catalog/package.json +++ b/packages/session/session-format-catalog/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-catalog", "description": "Build-static first-party Session format codec and migration catalog", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v0-to-v1/package.json b/packages/session/session-format-v0-to-v1/package.json index 0fa1c85273..57e30a7333 100644 --- a/packages/session/session-format-v0-to-v1/package.json +++ b/packages/session/session-format-v0-to-v1/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v0-to-v1", "description": "Frozen released-v0 Session codec and identity migration to v1", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v1-to-v2/package.json b/packages/session/session-format-v1-to-v2/package.json index 3386c8d025..775a036e07 100644 --- a/packages/session/session-format-v1-to-v2/package.json +++ b/packages/session/session-format-v1-to-v2/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v1-to-v2", "description": "Frozen released-v1 Session codec and assistant-stream migration to v2", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v2-to-v3/package.json b/packages/session/session-format-v2-to-v3/package.json index f1bf698e2c..b22eae5950 100644 --- a/packages/session/session-format-v2-to-v3/package.json +++ b/packages/session/session-format-v2-to-v3/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v2-to-v3", "description": "Streaming system-prompt, canonical-envelope and PTC migration into V3", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format-v3-to-v4/package.json b/packages/session/session-format-v3-to-v4/package.json index 36f3f45988..95ebd80378 100644 --- a/packages/session/session-format-v3-to-v4/package.json +++ b/packages/session/session-format-v3-to-v4/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format-v3-to-v4", "description": "Streaming tool-role migration and delivery validation from Session V3 to V4", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-format/package.json b/packages/session/session-format/package.json index 9dd6d13ca4..59944baf4f 100644 --- a/packages/session/session-format/package.json +++ b/packages/session/session-format/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-format", "description": "Streaming adjacent Session format migration machinery", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-log-deepseek/README.i18n.yaml b/packages/session/session-log-deepseek/README.i18n.yaml index bd4bc8f144..d604740584 100644 --- a/packages/session/session-log-deepseek/README.i18n.yaml +++ b/packages/session/session-log-deepseek/README.i18n.yaml @@ -15,8 +15,8 @@ en: e2aa29e41bc101c4 zh: 2ef629f2a444bc13 /deepseek-ai-dsh-session-log-deepseek/configuration: - en: 1e8712ec4f0fefcc - zh: 5ffe754b2cefdd21 + en: 1b899e63d8039643 + zh: d489461bbb618b5d /deepseek-ai-dsh-session-log-deepseek/request-field: en: 197814e2c48d05b5 zh: f4616fb23a3c7a32 diff --git a/packages/session/session-log-deepseek/README.md b/packages/session/session-log-deepseek/README.md index 41726b4b44..4a2d9226d7 100644 --- a/packages/session/session-log-deepseek/README.md +++ b/packages/session/session-log-deepseek/README.md @@ -27,10 +27,12 @@ Incremental canonical session-log upload for official DeepSeek LLM API requests. | Key | Default | Meaning | |---|---:|---| -| `enabled` | `true` | Register the `dsh_session_log` contribution. Set it to `false` to stop Session-log upload. | +| `enabled` | `true` | Send the `dsh_session_log` contribution. Changes apply to the next request; `false` stops Session-log upload. | | `maxBytes` | 8 MiB | Largest serialized `dsh_session_log` field, in UTF-8 bytes, that one request carries. | -Shipped profiles mount the plugin, so the default configuration registers the request field and appends the acceptance watermark; an overlay opts out with `enabled: false`. +Shipped profiles mount the plugin, so the default configuration registers the request field and appends the acceptance watermark; `enabled: false` stops the contribution. The Web switch can override bundle defaults through the active profile; home patches and command-line overlays retain precedence and reject conflicting form writes. + +The Web **Settings → General → Upload Session Log when using the official model API** switch persists `enabled` through the Host configuration. In-flight requests keep their prepared payload. Re-enabling resumes the unaccepted suffix, including events recorded while disabled. OpenTelemetry feedback uploads have a separate setting. ## Request field diff --git a/packages/session/session-log-deepseek/README.zh.md b/packages/session/session-log-deepseek/README.zh.md index 6a51bff630..3c294b24d0 100644 --- a/packages/session/session-log-deepseek/README.zh.md +++ b/packages/session/session-log-deepseek/README.zh.md @@ -27,10 +27,12 @@ kind: "package-reference" | 配置键 | 默认值 | 含义 | |---|---:|---| -| `enabled` | `true` | 注册 `dsh_session_log` 贡献。将其设为 `false` 可停止会话日志上传。 | +| `enabled` | `true` | 发送 `dsh_session_log` 贡献。修改从下一次请求生效;设为 `false` 可停止会话日志上传。 | | `maxBytes` | 8 MiB | 单次请求携带的 `dsh_session_log` 字段序列化后的最大 UTF-8 字节数。 | -随附 profile 会挂载该插件,因此默认配置会注册请求字段并追加接受水位;overlay 可用 `enabled: false` 选择退出。 +随附 profile 会挂载该插件,因此默认配置会注册请求字段并追加接受水位;`enabled: false` 会停止贡献。Web 开关可通过当前 profile 覆盖组合包默认值;home patch 和命令行 overlay 保持更高优先级,会拒绝与其冲突的表单写入。 + +Web **设置 → 通用 → 在使用官方模型 API 时上传 Session Log** 开关通过 Host 配置持久化 `enabled`。进行中的请求保留已准备的载荷。重新开启后继续上传尚未确认的后缀,包括关闭期间记录的事件。OpenTelemetry 反馈上传使用独立设置。 ## 请求字段 diff --git a/packages/session/session-log-deepseek/package.json b/packages/session/session-log-deepseek/package.json index 1560b478f9..d2d194fcb7 100644 --- a/packages/session/session-log-deepseek/package.json +++ b/packages/session/session-log-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-log-deepseek", "description": "Incremental lossless session-log request extension for the official DeepSeek LLM API", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-log-deepseek/src/index.ts b/packages/session/session-log-deepseek/src/index.ts index deba185587..8e17edd4e7 100644 --- a/packages/session/session-log-deepseek/src/index.ts +++ b/packages/session/session-log-deepseek/src/index.ts @@ -6,7 +6,7 @@ */ import { Buffer } from 'node:buffer' -import type { Context } from '@deepseek-ai/cordis' +import type { Context, Volatile } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { brandString } from '@deepseek-ai/dsh-brand' import type {} from '@deepseek-ai/dsh-deepseek-llm-api-extensions' @@ -38,18 +38,18 @@ export const inject = ['deepseekLlmApiExtensions', 'sessions'] /** Session-log request contribution configuration. */ export interface Config { /** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `true`. */ - enabled?: boolean + enabled: Volatile /** * 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 } /** Validated Session-log request contribution configuration. */ -export const Config: z = z.object({ - enabled: z.boolean().default(true), +export const Config = z.object({ + enabled: z.boolean().default(true).volatile(), maxBytes: z.number().step(1).min(1).default(8 * 1024 * 1024), }) @@ -175,16 +175,16 @@ export function acceptedThrough(session: Session): SessionSeqCursor { } /** - * Register the incremental `dsh_session_log` request contribution when enabled. + * Register the incremental request contribution; enablement is read for each request. * @param ctx - plugin context carrying Sessions and the DeepSeek request-extension registry. * @param config - validated configuration. */ export function apply(ctx: Context, config: Config): void { - if (config.enabled !== true) return // Schemastery validates and fills the defaults before `apply` runs. - const { maxBytes } = config as Required + const { maxBytes } = config ctx.deepseekLlmApiExtensions.register('dsh_session_log', { prepare: (request) => { + if (!config.enabled.get()) return undefined // TODO: Define an explicit wire result for direct or stale-session calls if they become a supported product path. if (request.sessionId === undefined) return undefined const session = ctx.sessions.get(brandString(request.sessionId)) diff --git a/packages/session/session-log-deepseek/tests/config.spec.ts b/packages/session/session-log-deepseek/tests/config.spec.ts index 04a0c81410..a6b7a5cbc1 100644 --- a/packages/session/session-log-deepseek/tests/config.spec.ts +++ b/packages/session/session-log-deepseek/tests/config.spec.ts @@ -20,9 +20,9 @@ describe('session-log upload configuration', () => { // Import under each environment to catch environment-dependent schema defaults. vi.resetModules() const { Config } = await import('../src/index.ts') - expect(Config({}).enabled).toBe(enabled) - expect(Config({ enabled: true }).enabled).toBe(true) - expect(Config({ enabled: false }).enabled).toBe(false) + expect(Config({}).enabled.get()).toBe(enabled) + expect(Config({ enabled: true }).enabled.get()).toBe(true) + expect(Config({ enabled: false }).enabled.get()).toBe(false) } finally { vi.unstubAllEnvs() vi.resetModules() diff --git a/packages/session/session-log-deepseek/tests/feedback-composition.spec.ts b/packages/session/session-log-deepseek/tests/feedback-composition.spec.ts index 9493d40209..7dc716b41f 100644 --- a/packages/session/session-log-deepseek/tests/feedback-composition.spec.ts +++ b/packages/session/session-log-deepseek/tests/feedback-composition.spec.ts @@ -32,7 +32,9 @@ afterEach(async () => { }) /** Boot the official upload route through the Loader with one session-log configuration. */ -async function boot(sessionLog: SessionLogDeepSeek.Config, mock: MockLlmServerOptions): Promise<{ ctx: Context; server: MockLlmServer }> { +async function boot( + sessionLog: { enabled: boolean; maxBytes?: number }, mock: MockLlmServerOptions, +): Promise<{ ctx: Context; server: MockLlmServer }> { root = await mkdtemp(join(tmpdir(), 'dsh-feedback-upload-')) vi.stubEnv('DSH_HOME', root) vi.stubEnv('DEEPSEEK_API_KEY', 'feedback-test-key') @@ -174,3 +176,33 @@ it('drains a Session log above the configured maxBytes across consecutive routed await handle.close() } }) + + +it('changes API log upload on the next request without replacing the plugin fiber', async () => { + const { ctx, server } = await boot({ enabled: true }, { sequence: ['success'], repeatLast: true }) + const session = ctx.sessions.create(SessionId('live-upload-setting')) + session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Hello' }], source: { kind: 'user' } }), { surfaceOp: 'append' }) + const entry = [...ctx.loader.entries()].find(entry => entry.options.name === '@deepseek-ai/dsh-session-log-deepseek')! + const fiber = entry.fiber + const request = async () => { + const chunks = [] + for await (const chunk of ctx.llm.stream({ provider: 'deepseek-official', model: 'deepseek-v4-flash', sessionId: session.id, messages: session.deriveMessages() })) chunks.push(chunk) + expect(chunks.at(-1)).toMatchObject({ type: 'finish', reason: { kind: 'stop' } }) + } + await request() + const accepted = SessionLogDeepSeek.acceptedThrough(session) + await entry.update({ config: { enabled: false } }) + await ctx.loader.await() + expect(entry.fiber === fiber).toBe(true) + recordFeedback(session, { text: 'Recorded while API upload is off' }) + await request() + expect(server.requests[1]!.body).not.toHaveProperty('dsh_session_log') + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(accepted) + await entry.update({ config: { enabled: true } }) + await ctx.loader.await() + expect(entry.fiber === fiber).toBe(true) + await request() + const resumed = (server.requests[2]!.body as { dsh_session_log: DeepSeekSessionLogExtension }).dsh_session_log + expect(resumed.afterSeq).toBe(accepted) + expect(resumed.events.some(event => event.type === 'feedback/record')).toBe(true) +}) diff --git a/packages/session/session-persistence-jsonl/package.json b/packages/session/session-persistence-jsonl/package.json index 9515d0003c..84554e1e68 100644 --- a/packages/session/session-persistence-jsonl/package.json +++ b/packages/session/session-persistence-jsonl/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence-jsonl", "description": "JSONL durable session persistence backend for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-persistence/package.json b/packages/session/session-persistence/package.json index 8bf1e24ea6..d22a09565f 100644 --- a/packages/session/session-persistence/package.json +++ b/packages/session/session-persistence/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-persistence", "description": "Abstract durable session persistence seam (ctx.sessionPersistence) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection-cache/package.json b/packages/session/session-projection-cache/package.json index 8110ff86b5..09b8a3331e 100644 --- a/packages/session/session-projection-cache/package.json +++ b/packages/session/session-projection-cache/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection-cache", "description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-projection/package.json b/packages/session/session-projection/package.json index 2b41be0fa1..054070920b 100644 --- a/packages/session/session-projection/package.json +++ b/packages/session/session-projection/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-projection", "description": "Session-projection seam: the merge-extensible projection type table, the provider contract, and the ctx.sessionProjections registry serving whole current values of log-derived per-session state", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-stats/package.json b/packages/session/session-stats/package.json index 1404453031..7aff5e081c 100644 --- a/packages/session/session-stats/package.json +++ b/packages/session/session-stats/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-stats", "description": "Whole-log conversation counts and wall times projection (sessionStats) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-telemetry-otel/package.json b/packages/session/session-telemetry-otel/package.json index 7777001d47..f6eedbb1de 100644 --- a/packages/session/session-telemetry-otel/package.json +++ b/packages/session/session-telemetry-otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry-otel", "description": "Feedback-authorized Session logs over byte-bounded OpenTelemetry HTTP requests", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-telemetry/package.json b/packages/session/session-telemetry/package.json index c5c13e08ce..1a603ed961 100644 --- a/packages/session/session-telemetry/package.json +++ b/packages/session/session-telemetry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-telemetry", "description": "SessionTelemetryBackend seam for the DeepSeek Harness: session-event capture, projection, redaction, and handoff to a reporting backend", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-all-prompts-llm/package.json b/packages/session/session-title-all-prompts-llm/package.json index 813cb80e85..31a77b78eb 100644 --- a/packages/session/session-title-all-prompts-llm/package.json +++ b/packages/session/session-title-all-prompts-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-all-prompts-llm", "description": "All-user-messages LLM provider plugin for DeepSeek Harness session titles", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-first-prompt-llm/package.json b/packages/session/session-title-first-prompt-llm/package.json index b25e65a454..c8e306543a 100644 --- a/packages/session/session-title-first-prompt-llm/package.json +++ b/packages/session/session-title-first-prompt-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-first-prompt-llm", "description": "First-message LLM provider plugin for DeepSeek Harness session titles", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title-llm/package.json b/packages/session/session-title-llm/package.json index fd5ef3ef19..de82b423e3 100644 --- a/packages/session/session-title-llm/package.json +++ b/packages/session/session-title-llm/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title-llm", "description": "Shared LLM generation policy for DeepSeek Harness session-title providers", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-title/package.json b/packages/session/session-title/package.json index f8a63ac44f..13b27ce3d0 100644 --- a/packages/session/session-title/package.json +++ b/packages/session/session-title/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-title", "description": "Log-backed session title service and provider registry for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/session/session-turn-outline/package.json b/packages/session/session-turn-outline/package.json index e5823e7736..528f24def5 100644 --- a/packages/session/session-turn-outline/package.json +++ b/packages/session/session-turn-outline/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-turn-outline", "description": "Whole-log turn outline projection (turnOutline) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/settings/settings/package.json b/packages/settings/settings/package.json index ca01061e16..7f3635542c 100644 --- a/packages/settings/settings/package.json +++ b/packages/settings/settings/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-settings", "description": "Abstract user-settings seam (ctx.settings) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-local/package.json b/packages/shell/bash-local/package.json index 5095595dd7..85b5e9346a 100644 --- a/packages/shell/bash-local/package.json +++ b/packages/shell/bash-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-local", "description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/bash-sandbox/package.json b/packages/shell/bash-sandbox/package.json index da277d54ab..3388e62015 100644 --- a/packages/shell/bash-sandbox/package.json +++ b/packages/shell/bash-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-bash-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness bash executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-local/package.json b/packages/shell/pwsh-local/package.json index 6bd8d36748..70e214b7f7 100644 --- a/packages/shell/pwsh-local/package.json +++ b/packages/shell/pwsh-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-local", "description": "Local PowerShell implementation of the DeepSeek Harness bash executor seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/pwsh-sandbox/package.json b/packages/shell/pwsh-sandbox/package.json index 3a6dcb52f0..b747d8b517 100644 --- a/packages/shell/pwsh-sandbox/package.json +++ b/packages/shell/pwsh-sandbox/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-pwsh-sandbox", "description": "Sandbox-consuming implementation of the DeepSeek Harness PowerShell executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell-env/package.json b/packages/shell/shell-env/package.json index 3192571d8f..6938e3b323 100644 --- a/packages/shell/shell-env/package.json +++ b/packages/shell/shell-env/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell-env", "description": "Tool-independent managed DSH_* shell environment registry", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/shell/package.json b/packages/shell/shell/package.json index 42dbb526c5..2d74ea858b 100644 --- a/packages/shell/shell/package.json +++ b/packages/shell/shell/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-shell", "description": "Abstract bash executor seam (ctx.shell) for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash-persistent/package.json b/packages/shell/tool-bash-persistent/package.json index b9532cf24f..2aa171b26c 100644 --- a/packages/shell/tool-bash-persistent/package.json +++ b/packages/shell/tool-bash-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash-persistent", "description": "Model-facing owner-scoped persistent Bash tool backed by the Harness PTY service", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-bash/package.json b/packages/shell/tool-bash/package.json index 79be50b55b..d019787b1e 100644 --- a/packages/shell/tool-bash/package.json +++ b/packages/shell/tool-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-bash", "description": "Model-facing bash tool with optional generic background-job and sandbox-escalation support", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh-persistent/package.json b/packages/shell/tool-pwsh-persistent/package.json index 2292298025..72ff0753e7 100644 --- a/packages/shell/tool-pwsh-persistent/package.json +++ b/packages/shell/tool-pwsh-persistent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh-persistent", "description": "Model-facing owner-scoped persistent PowerShell tool backed by the Harness PTY service", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/shell/tool-pwsh/package.json b/packages/shell/tool-pwsh/package.json index 85ea46a3bb..ab395e749d 100644 --- a/packages/shell/tool-pwsh/package.json +++ b/packages/shell/tool-pwsh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-pwsh", "description": "Model-facing pwsh tool over the bash executor seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-badge/package.json b/packages/skill/skill-badge/package.json index 4eaf8ba0cb..e558c7ce62 100644 --- a/packages/skill/skill-badge/package.json +++ b/packages/skill/skill-badge/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-badge", "description": "Bundled dsh badge skill provider for DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-filesystem/package.json b/packages/skill/skill-filesystem/package.json index 809c881236..5c86c2b724 100644 --- a/packages/skill/skill-filesystem/package.json +++ b/packages/skill/skill-filesystem/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-filesystem", "description": "Local filesystem skill provider for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill-office/package.json b/packages/skill/skill-office/package.json index d7338232cf..74a0ae9d80 100644 --- a/packages/skill/skill-office/package.json +++ b/packages/skill/skill-office/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill-office", "description": "Bundled Word, PowerPoint, and Excel workflows and structural checks", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/skill/package.json b/packages/skill/skill/package.json index 99f6d4ed21..9a421e928a 100644 --- a/packages/skill/skill/package.json +++ b/packages/skill/skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-skill", "description": "Agent skill provider registry for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/tool-skill/package.json b/packages/skill/tool-skill/package.json index 0aeb448241..787ac46bf4 100644 --- a/packages/skill/tool-skill/package.json +++ b/packages/skill/tool-skill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-skill", "description": "Model-facing skill loading tool for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/skill/tool-workspace-dependencies/package.json b/packages/skill/tool-workspace-dependencies/package.json index d053172b25..6c2a1cd9a6 100644 --- a/packages/skill/tool-workspace-dependencies/package.json +++ b/packages/skill/tool-workspace-dependencies/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-workspace-dependencies", "description": "The load_workspace_dependencies tool: absolute paths into a bundled Python, Node.js, and pnpm payload", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-local/package.json b/packages/spill/spill-local/package.json index 958bc68c4e..9378ddc36a 100644 --- a/packages/spill/spill-local/package.json +++ b/packages/spill/spill-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-local", "description": "Local-filesystem implementation of the DeepSeek Harness spill storage seam (private session-scoped files)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill-policy/package.json b/packages/spill/spill-policy/package.json index 4b172836fb..eee180923e 100644 --- a/packages/spill/spill-policy/package.json +++ b/packages/spill/spill-policy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill-policy", "description": "Token-budgeted tool-result retention with recoverable text and image paths", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/spill/spill/package.json b/packages/spill/spill/package.json index fe4314887f..d4c3fcb7e4 100644 --- a/packages/spill/spill/package.json +++ b/packages/spill/spill/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-spill", "description": "Abstract spill storage seam (ctx.spillStore) for the DeepSeek Harness — save oversized tool text and return a retrieval locator", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/fs-ssh/package.json b/packages/ssh/fs-ssh/package.json index 4531a5fa89..c41f672491 100644 --- a/packages/ssh/fs-ssh/package.json +++ b/packages/ssh/fs-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-fs-ssh", "description": "Filesystem provider over the shared POSIX SSH helper", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/sandbox-ssh/package.json b/packages/ssh/sandbox-ssh/package.json index 7ec9cc34b7..54d9789591 100644 --- a/packages/ssh/sandbox-ssh/package.json +++ b/packages/ssh/sandbox-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-sandbox-ssh", "description": "Remote POSIX sandbox argv provider over the shared SSH helper", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/ssh/package.json b/packages/ssh/ssh/package.json index e4f58cb11b..24feca58b5 100644 --- a/packages/ssh/ssh/package.json +++ b/packages/ssh/ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-ssh", "description": "Shared OpenSSH connection and versioned POSIX remote helper", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/ssh/subprocess-ssh/package.json b/packages/ssh/subprocess-ssh/package.json index 4f455f8e40..526662ec11 100644 --- a/packages/ssh/subprocess-ssh/package.json +++ b/packages/ssh/subprocess-ssh/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-ssh", "description": "Subprocess and terminal provider over the shared POSIX SSH helper", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-domain/package.json b/packages/storage/storage-domain/package.json index 685e7c2b43..b4da30b053 100644 --- a/packages/storage/storage-domain/package.json +++ b/packages/storage/storage-domain/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-domain", "description": "Domain data form (ctx.storage.domain): schema-validated, event-emitting KV domains over storage backends for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-json/package.json b/packages/storage/storage-json/package.json index c156f8db5d..54f6b652dc 100644 --- a/packages/storage/storage-json/package.json +++ b/packages/storage/storage-json/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-json", "description": "JSON file KV storage backend for the DeepSeek Harness storage hub", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage-sqlite/package.json b/packages/storage/storage-sqlite/package.json index e4beecd045..f1807500b4 100644 --- a/packages/storage/storage-sqlite/package.json +++ b/packages/storage/storage-sqlite/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage-sqlite", "description": "SQLite storage backend (kv facet) for the DeepSeek Harness storage hub", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/storage/storage/package.json b/packages/storage/storage/package.json index 6a60d174df..85cf6c4927 100644 --- a/packages/storage/storage/package.json +++ b/packages/storage/storage/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-storage", "description": "Storage hub (ctx.storage): named backend registry plus mounted data-form facilities for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-acp/package.json b/packages/subagent/subagent-acp/package.json index f4706848ac..898ba97f6e 100644 --- a/packages/subagent/subagent-acp/package.json +++ b/packages/subagent/subagent-acp/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-acp", "description": "Out-of-process ACP subagent backend: drives a child agent in a spawned subprocess over the Agent Client Protocol", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json index 649a07fd11..ae796f04b4 100644 --- a/packages/subagent/subagent-claude-code/package.json +++ b/packages/subagent/subagent-claude-code/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-claude-code", "description": "One-shot Claude Code subagent provider over the official Agent SDK", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json index 4fc23cdf2d..ff8751b52a 100644 --- a/packages/subagent/subagent-codex/package.json +++ b/packages/subagent/subagent-codex/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-codex", "description": "One-shot Codex subagent provider over the official app-server protocol", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-dsh-sdk/package.json b/packages/subagent/subagent-dsh-sdk/package.json index c4ba5dc1d0..2c18640d13 100644 --- a/packages/subagent/subagent-dsh-sdk/package.json +++ b/packages/subagent/subagent-dsh-sdk/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-dsh-sdk", "description": "Out-of-process SDK subagent backend: drives a child DeepSeek Harness runtime subprocess over stdio JSON-RPC through the TypeScript SDK client", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-fork-in-process/package.json b/packages/subagent/subagent-fork-in-process/package.json index ee011b7ecf..664fcc2be3 100644 --- a/packages/subagent/subagent-fork-in-process/package.json +++ b/packages/subagent/subagent-fork-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-fork-in-process", "description": "In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-in-process-driver/package.json b/packages/subagent/subagent-in-process-driver/package.json index 01917a47bd..c0812b2ea5 100644 --- a/packages/subagent/subagent-in-process-driver/package.json +++ b/packages/subagent/subagent-in-process-driver/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-in-process-driver", "description": "Shared in-process subagent run driver: drives a child agent on ctx.agents (used by the spawn and fork backends)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent-spawn-in-process/package.json b/packages/subagent/subagent-spawn-in-process/package.json index a5f86ccaa9..28e03528b8 100644 --- a/packages/subagent/subagent-spawn-in-process/package.json +++ b/packages/subagent/subagent-spawn-in-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent-spawn-in-process", "description": "In-process spawn subagent backend: runs a fresh child agent on ctx.agents", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index d513ad2792..0f245cdb3f 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subagent", "description": "Abstract subagent seam (ctx.subagents): named-provider registry for delegating to child agents", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent-control/package.json b/packages/subagent/tool-subagent-control/package.json index 1e8d5188b9..c4a1404c76 100644 --- a/packages/subagent/tool-subagent-control/package.json +++ b/packages/subagent/tool-subagent-control/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent-control", "description": "Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index 7c5a48e697..3a46d76e9e 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-subagent", "description": "Model-facing subagent delegation tool over the ctx.subagents seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess-local/package.json b/packages/subprocess/subprocess-local/package.json index ef8f83a8ef..30712585e6 100644 --- a/packages/subprocess/subprocess-local/package.json +++ b/packages/subprocess/subprocess-local/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess-local", "description": "Local-subprocess implementation of the DeepSeek Harness subprocess seam", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/subprocess/package.json b/packages/subprocess/subprocess/package.json index 6793690fc6..8dcdce07f9 100644 --- a/packages/subprocess/subprocess/package.json +++ b/packages/subprocess/subprocess/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-subprocess", "description": "Subprocess seam (ctx.subprocess) for the DeepSeek Harness \u2014 managed process groups, bounded spill-backed output, and escalated kills behind one abstract service", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/subprocess/win32-process/package.json b/packages/subprocess/win32-process/package.json index 988abed68b..31119dbd5c 100644 --- a/packages/subprocess/win32-process/package.json +++ b/packages/subprocess/win32-process/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-win32-process", "description": "Shared low-level Win32 process, stdio, and Job Object primitives", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/telemetry/otel/package.json b/packages/telemetry/otel/package.json index cc471ceae2..851168b172 100644 --- a/packages/telemetry/otel/package.json +++ b/packages/telemetry/otel/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-otel", "description": "Cordis service for independent ordinary-event and byte-bounded Session-log OTLP channels", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal-bash/package.json b/packages/terminal/terminal-bash/package.json index f7912edb2a..124daf59b9 100644 --- a/packages/terminal/terminal-bash/package.json +++ b/packages/terminal/terminal-bash/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal-bash", "description": "Persistent shell PTY backend over the DeepSeek Harness subprocess terminal primitive", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/terminal/package.json b/packages/terminal/terminal/package.json index 9c4ca30efb..3545eb31cc 100644 --- a/packages/terminal/terminal/package.json +++ b/packages/terminal/terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-terminal", "description": "Persistent PTY session seam for the DeepSeek Harness — owner-scoped ids, backend registry, interactive sends, reads, signals, and awaited cleanup", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/terminal/tool-terminal/package.json b/packages/terminal/tool-terminal/package.json index 61b9d9c2ec..f47d0febe6 100644 --- a/packages/terminal/tool-terminal/package.json +++ b/packages/terminal/tool-terminal/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-terminal", "description": "Six model-facing persistent PTY tools with owner isolation and generic background-job integration", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/agent-loop-testkit/package.json b/packages/test-support/agent-loop-testkit/package.json index 5fd2e7784b..f91fa627a4 100644 --- a/packages/test-support/agent-loop-testkit/package.json +++ b/packages/test-support/agent-loop-testkit/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-agent-loop-testkit", "description": "Prerequisite mounting, production AgentLoop drivers, and Inbox stubs for tests", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/client-runtime/package.json b/packages/test-support/client-runtime/package.json index 2613b7ade2..a9bdec572c 100644 --- a/packages/test-support/client-runtime/package.json +++ b/packages/test-support/client-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-client-test-runtime", "description": "Browser test runtimes: a jsdom slot bench with test-owned Session and Workspace doubles, and a whole-client tier that boots the web roster through the production bootClient over an endpoint-named Remote mock", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-mock-server/package.json b/packages/test-support/llm-mock-server/package.json index 0666980f1c..cca810aa4a 100644 --- a/packages/test-support/llm-mock-server/package.json +++ b/packages/test-support/llm-mock-server/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-mock-server", "description": "Scriptable Messages HTTP/SSE fault server for LLM recovery tests", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/llm-replay/package.json b/packages/test-support/llm-replay/package.json index afc4fae062..46de6d71b2 100644 --- a/packages/test-support/llm-replay/package.json +++ b/packages/test-support/llm-replay/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-llm-replay", "description": "Replay LLM plugin: short-circuits llm/stream with model chunks reconstructed from a recorded session JSONL (keyless snapshot tests)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/loader-smoke/package.json b/packages/test-support/loader-smoke/package.json index 65b29b3718..5a5765fefc 100644 --- a/packages/test-support/loader-smoke/package.json +++ b/packages/test-support/loader-smoke/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-loader-smoke", "description": "Shared subprocess and direct-agent harness for keyless real-Loader example smoke tests", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/remote-mock/package.json b/packages/test-support/remote-mock/package.json index 1829cbbe41..f69305d853 100644 --- a/packages/test-support/remote-mock/package.json +++ b/packages/test-support/remote-mock/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-remote-mock", "description": "Endpoint-named mock for Typert Remote traffic: unary answers and stream scripts per /, live stream control, a log, and the Connection carrier face whole-client specs install", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/test-support/session-snapshot/package.json b/packages/test-support/session-snapshot/package.json index 4a6e9ece2e..8864fb9be7 100644 --- a/packages/test-support/session-snapshot/package.json +++ b/packages/test-support/session-snapshot/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-session-snapshot", "description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index cdbc6fac26..73b9302905 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-todo", "description": "Model-facing todo_write tool over the DeepSeek Harness event-sourced session log", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/typert/generator/package.json b/packages/typert/generator/package.json index 45b5a01a2c..fd63cc5cba 100644 --- a/packages/typert/generator/package.json +++ b/packages/typert/generator/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-generator", "description": "TypeScript project analyzer and model-driven Typert artifact generator", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/typert/loader/package.json b/packages/typert/loader/package.json index 3cd19f1425..5493a19525 100644 --- a/packages/typert/loader/package.json +++ b/packages/typert/loader/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-loader", "description": "Loader integration for generated Typert package contributions", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/typert/protocol/package.json b/packages/typert/protocol/package.json index b90591280c..8fc9715306 100644 --- a/packages/typert/protocol/package.json +++ b/packages/typert/protocol/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-protocol", "description": "Compiler-independent Remote metadata and Typert provider protocols", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/typert/registry/package.json b/packages/typert/registry/package.json index e4219d99e2..6fc11cd1ed 100644 --- a/packages/typert/registry/package.json +++ b/packages/typert/registry/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-typert-registry", "description": "Runtime registry for generated package reflection and Zod schemas", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/atomic-write/package.json b/packages/util/atomic-write/package.json index 20c4adb454..f07eb4e7d2 100644 --- a/packages/util/atomic-write/package.json +++ b/packages/util/atomic-write/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-atomic-write", "description": "Zero-dependency atomic file replacement: exclusive-create random-suffix temp + rename carrying the caller-stated permissions (writeFileAtomic)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/brand/package.json b/packages/util/brand/package.json index bcca40fe79..e7254c9ac1 100644 --- a/packages/util/brand/package.json +++ b/packages/util/brand/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-brand", "description": "Stateless branded primitive types for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/chunked-list/package.json b/packages/util/chunked-list/package.json index b339c6958d..5b3ca0e055 100644 --- a/packages/util/chunked-list/package.json +++ b/packages/util/chunked-list/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-chunked-list", "description": "Persistent append-only chunked lists with bounded copying and JSON checkpoint validation", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/code-language/package.json b/packages/util/code-language/package.json index c3f220b4cc..229ba54197 100644 --- a/packages/util/code-language/package.json +++ b/packages/util/code-language/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-code-language", "description": "Single file-extension to syntax-highlighting language table shared by Client code surfaces and the Host read card", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/crypto/package.json b/packages/util/crypto/package.json index 6015ea019f..7687e64ed3 100644 --- a/packages/util/crypto/package.json +++ b/packages/util/crypto/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-crypto", "description": "Zero-dependency browser-safe UUID and byte-encoding helpers", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/deque/package.json b/packages/util/deque/package.json index f5586ee398..0fabeb62c4 100644 --- a/packages/util/deque/package.json +++ b/packages/util/deque/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-deque", "description": "Zero-dependency circular deque with amortized constant-time end operations and bounded vacant storage", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/home-paths/package.json b/packages/util/home-paths/package.json index b452b09b13..aac40fc2ac 100644 --- a/packages/util/home-paths/package.json +++ b/packages/util/home-paths/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-home-paths", "description": "Shared filesystem path helpers for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/http-proxy/package.json b/packages/util/http-proxy/package.json index 634d1f0032..f25364c5f0 100644 --- a/packages/util/http-proxy/package.json +++ b/packages/util/http-proxy/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-http-proxy", "description": "Process-wide outbound HTTP proxy policy for DeepSeek Harness: resolve it from the launch environment and install it as undici's global dispatcher", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/launch-environment/package.json b/packages/util/launch-environment/package.json index f9bb30908a..81ad2e9813 100644 --- a/packages/util/launch-environment/package.json +++ b/packages/util/launch-environment/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-launch-environment", "description": "Immutable DeepSeek Harness launch environment that records which layer supplied each value", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/lazy-require/package.json b/packages/util/lazy-require/package.json index 7ca9d0351a..9583513ed7 100644 --- a/packages/util/lazy-require/package.json +++ b/packages/util/lazy-require/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-lazy-require", "description": "Caller-relative, success-cached lazy loading for CommonJS-compatible Host dependencies", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/native-command/package.json b/packages/util/native-command/package.json index 5ab5cf918b..88c4b794f8 100644 --- a/packages/util/native-command/package.json +++ b/packages/util/native-command/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-native-command", "description": "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL handoff", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/output-retention/package.json b/packages/util/output-retention/package.json index e3984a0010..095f3bf03f 100644 --- a/packages/util/output-retention/package.json +++ b/packages/util/output-retention/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-output-retention", "description": "Zero-dependency bounded-retention primitive: ItemRetainer/TextRetainer + neutral notice helpers (what did we keep, what did we omit)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/package-manifest/package.json b/packages/util/package-manifest/package.json index ccf3ee7d64..563827e775 100644 --- a/packages/util/package-manifest/package.json +++ b/packages/util/package-manifest/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-package-manifest", "description": "Shared type declarations for package.json.dsh configuration fields", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/time/package.json b/packages/util/time/package.json index ae65d6450a..98a49b5247 100644 --- a/packages/util/time/package.json +++ b/packages/util/time/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-time", "description": "Zero-dependency time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization only, no formatting)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/timeout/package.json b/packages/util/timeout/package.json index 4a5c650b6b..17b3ca6494 100644 --- a/packages/util/timeout/package.json +++ b/packages/util/timeout/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-timeout", "description": "Zero-dependency timeout/deadline primitive: clampTimeout, deadline, timeoutOf, TimeoutReason (timing + classification only, no termination)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/values/package.json b/packages/util/values/package.json index a02ee660e9..0976fe5252 100644 --- a/packages/util/values/package.json +++ b/packages/util/values/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-values", "description": "Duplicate-install-safe value primitives for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/util/workspace-path/package.json b/packages/util/workspace-path/package.json index 292f659ef7..a577ab93b3 100644 --- a/packages/util/workspace-path/package.json +++ b/packages/util/workspace-path/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-util-workspace-path", "description": "Browser-safe Workspace path and display helpers", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/tool-web/package.json b/packages/web/tool-web/package.json index 90f01bfac6..73b158d531 100644 --- a/packages/web/tool-web/package.json +++ b/packages/web/tool-web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-web", "description": "Model-facing web tools (web_search, web_fetch) over the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-fetch-http/package.json b/packages/web/web-fetch-http/package.json index 9fa333a22d..fb1fb2854c 100644 --- a/packages/web/web-fetch-http/package.json +++ b/packages/web/web-fetch-http/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-fetch-http", "description": "Anonymous public HTTP(S) fetch provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-deepseek/package.json b/packages/web/web-search-deepseek/package.json index 6123f01e50..958d511813 100644 --- a/packages/web/web-search-deepseek/package.json +++ b/packages/web/web-search-deepseek/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-deepseek", "description": "DeepSeek-backed search provider (native web_search via the Anthropic-compatible API) for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-exa/package.json b/packages/web/web-search-exa/package.json index b936386ecb..72cc15a785 100644 --- a/packages/web/web-search-exa/package.json +++ b/packages/web/web-search-exa/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-exa", "description": "Exa-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web-search-perplexity/package.json b/packages/web/web-search-perplexity/package.json index c8c4869d5b..314659bca5 100644 --- a/packages/web/web-search-perplexity/package.json +++ b/packages/web/web-search-perplexity/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web-search-perplexity", "description": "Perplexity-backed search provider for the DeepSeek Harness web capability seam (ctx.web)", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/web/web/package.json b/packages/web/web/package.json index ac614c7e62..a53538fabb 100644 --- a/packages/web/web/package.json +++ b/packages/web/web/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-web", "description": "Abstract web access capability seam (ctx.web) for the DeepSeek Harness — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/webhook/webhook-github/package.json b/packages/webhook/webhook-github/package.json index 0bb1d633bc..9966c546d6 100644 --- a/packages/webhook/webhook-github/package.json +++ b/packages/webhook/webhook-github/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook-github", "description": "Signed GitHub HTTP webhook adapter for the DeepSeek Harness webhook runtime", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/webhook/webhook/package.json b/packages/webhook/webhook/package.json index aa3d1d4b8c..30de21f7d8 100644 --- a/packages/webhook/webhook/package.json +++ b/packages/webhook/webhook/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-webhook", "description": "Fire-and-forget webhook rule runtime that creates Workspace-backed DeepSeek Harness Sessions", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-ralph/package.json b/packages/workflow/tool-ralph/package.json index f91d109947..4846878f24 100644 --- a/packages/workflow/tool-ralph/package.json +++ b/packages/workflow/tool-ralph/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-ralph", "description": "Model-facing fresh-agent Ralph loop over the workflow and subagent seams", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/tool-workflow/package.json b/packages/workflow/tool-workflow/package.json index 011e984ecf..d108bff825 100644 --- a/packages/workflow/tool-workflow/package.json +++ b/packages/workflow/tool-workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-tool-workflow", "description": "Model-facing workflow tool: run a JavaScript orchestration script over ctx.workflowEngine", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow-ptc/package.json b/packages/workflow/workflow-ptc/package.json index 4f2e310f18..9c5135461b 100644 --- a/packages/workflow/workflow-ptc/package.json +++ b/packages/workflow/workflow-ptc/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow-ptc", "description": "Workflow orchestration in the shared sandboxed Node PTC runtime", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workflow/workflow/package.json b/packages/workflow/workflow/package.json index eae65f23f1..b3ee76034f 100644 --- a/packages/workflow/workflow/package.json +++ b/packages/workflow/workflow/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workflow", "description": "Workflow capability seam: ctx.workflowEngine service, run vocabulary, and workflow/* events", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/packages/workspace/workspace/package.json b/packages/workspace/workspace/package.json index 5d38b1cdac..4ab308b794 100644 --- a/packages/workspace/workspace/package.json +++ b/packages/workspace/workspace/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-workspace", "description": "Workspace entity registry (ctx.workspaceRegistry): durable workspace records with validated session attachment over the domain data form for the DeepSeek Harness", - "version": "0.1.7-rc.2", + "version": "0.2.0-rc.1", "publishConfig": { "access": "public" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b379a8c21..14a4b54546 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -789,6 +789,9 @@ importers: '@deepseek-ai/dsh-launch-environment': specifier: workspace:* version: link:../../packages/util/launch-environment + '@deepseek-ai/dsh-llm-mock-server': + specifier: workspace:* + version: link:../../packages/test-support/llm-mock-server '@deepseek-ai/dsh-pwsh-local': specifier: workspace:* version: link:../../packages/shell/pwsh-local @@ -2503,6 +2506,9 @@ importers: '@deepseek-ai/dsh-client-ui-settings-plugins': specifier: workspace:* version: link:../../client/ui-settings-plugins + '@deepseek-ai/dsh-client-ui-settings-session-log': + specifier: workspace:* + version: link:../../client/ui-settings-session-log '@deepseek-ai/dsh-client-ui-settings-shell': specifier: workspace:* version: link:../../client/ui-settings-shell @@ -2726,9 +2732,6 @@ importers: specifier: ^11.0.0 version: 11.0.4 devDependencies: - '@deepseek-ai/dsh-otel': - specifier: workspace:* - version: link:../../telemetry/otel '@deepseek-ai/cordis': specifier: workspace:~ version: link:../../../vendor/cordis @@ -2744,6 +2747,9 @@ importers: '@deepseek-ai/dsh-fs-local': specifier: workspace:* version: link:../../fs/fs-local + '@deepseek-ai/dsh-otel': + specifier: workspace:* + version: link:../../telemetry/otel '@deepseek-ai/dsh-sandbox-policy': specifier: workspace:* version: link:../../sandbox/sandbox-policy @@ -2944,18 +2950,12 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: - '@deepseek-ai/dsh-otel': - specifier: workspace:* - version: link:../../telemetry/otel - '@deepseek-ai/cordis-plugin-loader': - specifier: workspace:~ - version: link:../../../vendor/loader - '@deepseek-ai/dsh-settings': - specifier: workspace:* - version: link:../../settings/settings '@deepseek-ai/cordis': specifier: workspace:~ version: link:../../../vendor/cordis + '@deepseek-ai/cordis-plugin-loader': + specifier: workspace:~ + version: link:../../../vendor/loader '@deepseek-ai/dsh-api-gateway': specifier: workspace:* version: link:../../api/gateway @@ -2974,9 +2974,15 @@ importers: '@deepseek-ai/dsh-llm': specifier: workspace:* version: link:../../llm/llm + '@deepseek-ai/dsh-otel': + specifier: workspace:* + version: link:../../telemetry/otel '@deepseek-ai/dsh-session': specifier: workspace:* version: link:../../core/session + '@deepseek-ai/dsh-settings': + specifier: workspace:* + version: link:../../settings/settings packages/client/resources: devDependencies: @@ -5009,6 +5015,51 @@ importers: specifier: ^18.2.0 version: 18.3.1 + packages/client/ui-settings-session-log: + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:~ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:* + version: link:../../api/remotes + '@deepseek-ai/dsh-client-locale': + specifier: workspace:* + version: link:../locale + '@deepseek-ai/dsh-client-store': + specifier: workspace:* + version: link:../store + '@deepseek-ai/dsh-client-test-runtime': + specifier: workspace:* + version: link:../../test-support/client-runtime + '@deepseek-ai/dsh-client-ui-layout': + specifier: workspace:* + version: link:../ui-layout + '@deepseek-ai/dsh-client-ui-primitives': + specifier: workspace:* + version: link:../ui-primitives + '@deepseek-ai/dsh-client-ui-renderer': + specifier: workspace:* + version: link:../ui-renderer + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:* + version: link:../ui-settings + '@deepseek-ai/dsh-client-ui-slots': + specifier: workspace:* + version: link:../ui-slots + '@types/react': + specifier: ~18.3.1 + version: 18.3.31 + '@types/react-dom': + specifier: ~18.3.0 + version: 18.3.7(@types/react@18.3.31) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + packages/client/ui-settings-shell: devDependencies: '@deepseek-ai/cordis': @@ -10284,16 +10335,28 @@ importers: koffi: specifier: 3.1.1 version: 3.1.1 + yaml: + specifier: ^2.4.2 + version: 2.9.0 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ version: link:../../../vendor/cordis + '@deepseek-ai/cordis-plugin-include': + specifier: workspace:~ + version: link:../../../vendor/include + '@deepseek-ai/cordis-plugin-loader': + specifier: workspace:~ + version: link:../../../vendor/loader '@deepseek-ai/dsh-pwsh-local': specifier: workspace:* version: link:../../shell/pwsh-local '@deepseek-ai/dsh-sandbox-local': specifier: workspace:* version: link:../sandbox-local + '@deepseek-ai/dsh-skill': + specifier: workspace:* + version: link:../../skill/skill '@deepseek-ai/dsh-subprocess': specifier: workspace:* version: link:../../subprocess/subprocess diff --git a/scripts/benchmark-npm-resolution.ts b/scripts/benchmark-npm-resolution.ts index 8ee0d7fa92..c886056b1a 100644 --- a/scripts/benchmark-npm-resolution.ts +++ b/scripts/benchmark-npm-resolution.ts @@ -417,12 +417,14 @@ async function runNpm( * @param index - Package metadata exposed through the local registry. * @param dependencies - Root dependencies whose install layout npm computes. * @param timeoutMs - Hard wall-clock limit for the npm child process. + * @param timeoutSubject - What the limit guards, named in the timeout diagnostic. * @returns The package lock plus timing and registry-request observations. */ export async function resolveNpmPackageLock( index: RegistryIndex, dependencies: Readonly>, timeoutMs: number, + timeoutSubject = 'npm resolution', ): Promise { let registryRequests = 0 let archiveRequests = 0 @@ -471,7 +473,7 @@ export async function resolveNpmPackageLock( dependencies, }, null, 2)}\n`) const result = await runNpm(consumer, registry, timeoutMs) - if (result.timedOut) throw new Error(`npm resolution exceeded ${String(timeoutMs)} ms`) + if (result.timedOut) throw new Error(`${timeoutSubject} exceeded ${String(timeoutMs)} ms`) return { durationMs: result.durationMs, registryRequests, diff --git a/scripts/build-exe-for-python-sdk-assets.spec.ts b/scripts/build-exe-for-python-sdk-assets.spec.ts index 4debf6fdc5..0a963f3f1e 100644 --- a/scripts/build-exe-for-python-sdk-assets.spec.ts +++ b/scripts/build-exe-for-python-sdk-assets.spec.ts @@ -23,6 +23,7 @@ describe('Python runtime executable assets', () => { expect(result.status).toBe(0) expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*') expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*') + expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-sandbox-windows-acl/assets/**/*') expect(result.stdout).not.toContain('node_modules/**/*.py') }) }) diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index ba4f2bc726..e2000c960b 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -62,6 +62,8 @@ const ASSET_GLOBS = [ 'node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*', // skill-badge resolves both Markdown and image resources through import.meta.url. 'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*', + // The diagnosis provider extracts its PowerShell script for an external interpreter. + 'node_modules/@deepseek-ai/dsh-sandbox-windows-acl/assets/**/*', ] const PLATFORMS = ['linux', 'macos', 'win'] as const diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index faa254fa8b..ac888a98ce 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -198,7 +198,7 @@ const packageFileExtras: Readonly> = { // The argv-prefix runner entry ships beside the lib as its own bundle; // sandbox-local resolves it through the package's ./runner export. tsdown // also shares its generated FFI code through a hashed runtime chunk. - '@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js', 'lib/types-*.js'], + '@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js', 'lib/types-*.js', 'assets'], '@deepseek-ai/dsh-skill-badge': ['assets'], '@deepseek-ai/dsh-skill-office': ['assets'], '@deepseek-ai/dsh-subprocess': ['lib/control.js'], diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 3e691d0b35..57ba922462 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -575,7 +575,7 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'skill', title: 'Skill provider registry', mode: 'seam', - implementations: ['skill-badge', 'skill-filesystem', 'skill-office'], + implementations: ['sandbox-windows-acl', 'skill-badge', 'skill-filesystem', 'skill-office'], consumers: ['tool-skill'], note: 'Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies.', }, diff --git a/scripts/oxlint-contract.spec.ts b/scripts/oxlint-contract.spec.ts index 0325cc8a05..549405a617 100644 --- a/scripts/oxlint-contract.spec.ts +++ b/scripts/oxlint-contract.spec.ts @@ -205,7 +205,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + expect(result.error).toBeUndefined() expect(result.status, normalizedOutput(result)).toBe(0) - }) + }, 90_000) it('keeps repository lint workflows Oxlint-only', async () => { const packageJson: unknown = JSON.parse(await readFile(join(repositoryRoot, 'package.json'), 'utf8')) @@ -356,7 +356,7 @@ export function unrelatedRead(): void { expect(result.error).toBeUndefined() expect(result.status, normalizedOutput(result)).toBe(0) - }) + }, 90_000) it('keeps staged validation project-free while preserving source rules', async () => { const configPath = join(repositoryRoot, '.oxlintrc.staged.json') @@ -392,7 +392,7 @@ export function unrelatedRead(): void { } finally { await rm(path, { force: true }) } - }) + }, 90_000) it('preserves successful fix output channels', async () => { const suffix = randomUUID() @@ -416,7 +416,7 @@ export function unrelatedRead(): void { } finally { await rm(path, { force: true }) } - }) + }, 90_000) it('prints only the final diagnostics when a fix retry still fails', async () => { const suffix = randomUUID() @@ -440,7 +440,7 @@ export function unrelatedRead(): void { } finally { await rm(path, { force: true }) } - }) + }, 90_000) it.each(['--fix', '--fix-suggestions', '--fix-dangerously'])( 'converges overlapping staged stylistic fixes through Oxlint under %s', diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index 9d7a929446..4631258c16 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -770,6 +770,7 @@ describe('Node 24 lane ownership', () => { expect(subject.find(item => item.id === 'built-bin-smoke')?.args).toEqual( expect.arrayContaining([ 'apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts', + 'apps/desktop/tests/acl-skill.built.e2e.ts', 'packages/subprocess/subprocess-local/tests/spawn-runner-built.e2e.ts', 'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts', 'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts', diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index d0c4773c66..e1e2de9c21 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -855,6 +855,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate { 'apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts', 'apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts', 'apps/cli/tests/built-bin.e2e.ts', + 'apps/desktop/tests/acl-skill.built.e2e.ts', 'packages/host/directory-picker-native/tests/built-worker.e2e.ts', 'packages/sdk/server/tests/built-scope-carrier.e2e.ts', 'packages/deliverables/tool-present/tests/built-errors.e2e.ts', diff --git a/scripts/verify-npm-install-layout.spec.ts b/scripts/verify-npm-install-layout.spec.ts index 0d9dde0e4b..3f5ae5441b 100644 --- a/scripts/verify-npm-install-layout.spec.ts +++ b/scripts/verify-npm-install-layout.spec.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from 'vitest' import type { NpmPackageLock, RegistryIndex } from './benchmark-npm-resolution.ts' import { assertDualDshInstallLayout, + assertResolutionWorkBudget, buildDualDshRegistry, + MAX_RESOLUTION_WORK_UNITS, } from './verify-npm-install-layout.ts' function validLayout(): NpmPackageLock { @@ -113,3 +115,30 @@ describe('npm install layout verifier', () => { ) }) }) + +describe('resolution work budget', () => { + it('accepts the measured dual-release graph', () => { + expect(assertResolutionWorkBudget({ dshPackagesPerVersion: 277, checkedDshEdges: 2524 })) + .toBe(699_148) + }) + + it('accepts a graph exactly at the budget and rejects one internal edge above it', () => { + const packages = 350 + const edgesAtBudget = Math.floor(MAX_RESOLUTION_WORK_UNITS / packages) + + expect(packages * edgesAtBudget).toBe(MAX_RESOLUTION_WORK_UNITS) + expect(assertResolutionWorkBudget({ dshPackagesPerVersion: packages, checkedDshEdges: edgesAtBudget })) + .toBe(MAX_RESOLUTION_WORK_UNITS) + expect(() => assertResolutionWorkBudget({ + dshPackagesPerVersion: packages, + checkedDshEdges: edgesAtBudget + 1, + })).toThrow(`= ${String(MAX_RESOLUTION_WORK_UNITS + packages)} unit(s), budget ${String(MAX_RESOLUTION_WORK_UNITS)} unit(s)`) + }) + + it('accepts the headroom and rejects a runaway graph with its own measured counts', () => { + expect(assertResolutionWorkBudget({ dshPackagesPerVersion: 300, checkedDshEdges: 2800 })) + .toBe(840_000) + expect(() => assertResolutionWorkBudget({ dshPackagesPerVersion: 400, checkedDshEdges: 3000 })) + .toThrow('400 package(s) per release x 3000 internal edge(s) = 1200000 unit(s), budget 875000 unit(s)') + }) +}) diff --git a/scripts/verify-npm-install-layout.ts b/scripts/verify-npm-install-layout.ts index 4e2a084d8a..abc8f10f1f 100644 --- a/scripts/verify-npm-install-layout.ts +++ b/scripts/verify-npm-install-layout.ts @@ -15,7 +15,39 @@ const CORDIS_PACKAGE = '@deepseek-ai/cordis' const NESTED_DSH_ALIAS = 'dsh-previous' const NESTED_DSH_PATH = `node_modules/${NESTED_DSH_ALIAS}` const DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const -const TIMEOUT_MS = 300_000 + +/** + * Resolution work accepted from the dual-release graph, in + * `dshPackagesPerVersion * checkedDshEdges` units. + * + * npm's hoisted placement re-checks every internal edge against the incoming + * edges of its peer target (`canPlacePeers` calls `checkCanPlaceNoCurrent`, + * which walks `depValid` over that target's edges), so resolution time grows + * with this product and not with the package count alone. Measured on an idle + * M-series host: 277 package(s) x 2524 internal edge(s) = 699,148 unit(s) cost + * 180.7 s of npm time out of 185.1 s end to end. The budget adds 25% headroom + * for ordinary product growth, so a graph that grows here fails with its own + * measured counts instead of failing on whichever runner happens to be slow. + * Raising it requires re-measuring the resolution and recording the new counts. + */ +export const MAX_RESOLUTION_WORK_UNITS = 875_000 + +/** + * Measured npm seconds per resolution work unit on the reference host: 180.66 s + * for the 699,148 units of the graph the budget was derived from. + */ +const SECONDS_PER_WORK_UNIT = 2.6e-4 + +/** + * Wall-clock guard for the npm child process. This is a hang guard, not the + * gate's growth criterion: {@link MAX_RESOLUTION_WORK_UNITS} decides whether the + * graph is too large to verify, and a runner up to four times slower than the + * measured host cannot decide this gate's outcome. Derived from the budget so a + * raised budget cannot silently shrink the guard's headroom. + */ +const NPM_HANG_GUARD_MS = Math.round( + 4 * MAX_RESOLUTION_WORK_UNITS * SECONDS_PER_WORK_UNIT * 1000, +) /** Synthetic incompatible versions used to expose cross-release placement errors. */ export const SYNTHETIC_DSH_VERSIONS = ['0.1.0', '0.2.0'] as const @@ -186,6 +218,27 @@ export function assertDualDshInstallLayout(packageLock: NpmPackageLock): DshInst return { dshPackagesPerVersion: rootNames.size, checkedDshEdges } } +/** + * Reject a verified dual-release graph that exceeds the resolution work budget. + * @param summary - Counts returned by {@link assertDualDshInstallLayout}. + * @returns The verified resolution work units. + */ +export function assertResolutionWorkBudget(summary: DshInstallLayoutSummary): number { + const units = summary.dshPackagesPerVersion * summary.checkedDshEdges + if (units > MAX_RESOLUTION_WORK_UNITS) { + throw new Error( + 'dual-release graph exceeds the resolution work budget: ' + + `${String(summary.dshPackagesPerVersion)} package(s) per release x ` + + `${String(summary.checkedDshEdges)} internal edge(s) = ${String(units)} unit(s), ` + + `budget ${String(MAX_RESOLUTION_WORK_UNITS)} unit(s)\n` + + ' npm re-checks every internal edge against the incoming edges of its peer target, so the resolution\n' + + ' cost grows with this product. Measure the new cost and raise MAX_RESOLUTION_WORK_UNITS in\n' + + ' scripts/verify-npm-install-layout.ts.', + ) + } + return units +} + function workspaceVersion(root: string): string { const manifest = JSON.parse(readFileSync(resolve(root, 'apps/cli/package.json'), 'utf8')) as { version?: unknown } if (typeof manifest.version !== 'string') throw new Error('apps/cli/package.json has no string version') @@ -199,12 +252,14 @@ async function main(): Promise { const result = await resolveNpmPackageLock(index, { [DSH_PACKAGE]: rootVersion, [NESTED_DSH_ALIAS]: `npm:${DSH_PACKAGE}@${nestedVersion}`, - }, TIMEOUT_MS) + }, NPM_HANG_GUARD_MS, 'npm resolution hang guard') if (result.archiveRequests !== 0) throw new Error(`npm requested ${String(result.archiveRequests)} package archive(s)`) const summary = assertDualDshInstallLayout(result.packageLock) + const units = assertResolutionWorkBudget(summary) console.log( `verify-npm-install-layout: ${String(summary.dshPackagesPerVersion)} DSH package(s) per release and ` + `${String(summary.checkedDshEdges)} internal edge(s) verified in ${(result.durationMs / 1000).toFixed(2)} s; ` + + `${String(units)} of ${String(MAX_RESOLUTION_WORK_UNITS)} budgeted resolution work unit(s); ` + `both releases share one Cordis installation; ${String(result.unknownPackages.length)} unavailable optional ` + 'package name(s) ignored by npm.', ) diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index da3427d48c..2ca3653361 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -133,6 +133,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/client/ui-permission-presets': { kind: 'indirect', reason: 'The picker submits the host /permission command; the knob events it appends own the model-visible effect through the sandbox/approval consumers.' }, 'packages/client/ui-settings-plugins': { kind: 'none', reason: 'Browser-side settings surface; registers no model surface.' }, 'packages/client/ui-settings-shell': { kind: 'none', reason: 'Browser-side settings surface; registers no model surface.' }, + 'packages/client/ui-settings-session-log': { kind: 'none', reason: 'Browser preference controls request metadata, not model input.' }, 'packages/client/ui-settings-agent-loop': { kind: 'none', reason: 'Browser-side settings surface; registers no model surface.' }, 'packages/client/ui-settings-subagent': { kind: 'none', reason: 'Browser-side settings surface; registers no model surface.' }, 'packages/client/ui-settings-web-search': { kind: 'none', reason: 'Browser-side settings surface; registers no model surface.' }, diff --git a/snapshots/session/headless.snapshot.ts b/snapshots/session/headless.snapshot.ts index 5135ae4748..c5524284ac 100644 --- a/snapshots/session/headless.snapshot.ts +++ b/snapshots/session/headless.snapshot.ts @@ -475,6 +475,11 @@ async function seedWorkspace(scenario: HeadlessScenario, cwd: string): Promise Promise> = { + async 'windows-acl-skill'(cwd) { + const target = join(cwd, '.dsh', 'skills', 'diagnose-windows-sandbox-acl', 'SKILL.md') + await mkdir(dirname(target), { recursive: true }) + await copyFile(join(repoRoot, 'packages/sandbox/sandbox-windows-acl/assets/diagnose-windows-sandbox-acl/SKILL.md'), target) + }, async 'office-skills'(cwd) { await cp(join(repoRoot, 'packages/skill/skill-office/assets'), join(cwd, 'office-skills'), { recursive: true }) await symlink(process.execPath, join(cwd, 'office-node')) diff --git a/snapshots/session/windows-acl-skill/session.v4.jsonl b/snapshots/session/windows-acl-skill/session.v4.jsonl new file mode 100644 index 0000000000..6b1e51078b --- /dev/null +++ b/snapshots/session/windows-acl-skill/session.v4.jsonl @@ -0,0 +1,23 @@ +{"type":"session","version":4,"id":"{{session:1}}","createdAt":1783654655599,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Load the diagnose-windows-sandbox-acl skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Load the diagnose-windows-sandbox-acl skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n\n- `diagnose-windows-sandbox-acl`: Use on Windows for unexpected DSH sandbox access denials: workspace writes or listing fail, or an ordinarily readable path cannot be read. The bundled script inspects the path and every ancestor, reports observations and action reasons, and supports scoped, backed-up ACL repairs. Expected confinement denials need no ACL repair.\n\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\nA user may also invoke a skill directly; its block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.\n"}],"source":{"kind":"skill-catalog","form":"catalog","entries":[{"name":"diagnose-windows-sandbox-acl","description":"Use on Windows for unexpected DSH sandbox access denials: workspace writes or listing fail, or an ordinarily readable path cannot be read. The bundled script inspects the path and every ancestor, reports observations and action reasons, and supports scoped, backed-up ACL repairs. Expected confinement denials need no ACL repair."}]},"role":"user","id":"{{message:4}}"},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"session/title","data":{"title":"Load the diagnose-windows-sandbox-acl sk","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"diagnose-windows-sandbox-acl\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5},"stream":[{"type":"chunk","time":1788699430688,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699430688,"index":0,"dt":[],"texts":["Load the requested skill."]},{"type":"chunk","time":1788699430688,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788699430688,"index":1,"dt":[],"id":"call_skill_load","name":"skill","args":["{\"name\":\"diagnose-windows-sandbox-acl\"}"]},{"type":"chunk","time":1788699430688,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Load the requested skill."}}},{"type":"chunk","time":1788699430688,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"diagnose-windows-sandbox-acl\"}"}}},{"type":"chunk","time":1788699430688,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}}},{"type":"chunk","time":1788699430688,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"diagnose-windows-sandbox-acl\"}"}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_skill_load"},"toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/diagnose-windows-sandbox-acl\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\n# Diagnose Windows sandbox ACL failures\n\n**Stop after any failed/refused repair or failed verification.** A completed DACL write is not a successful repair. Never continue from a failed grant to `-Fix`, repeat the grant, or remove a deny ACE. The script automatically restores attempted changes from the failed invocation; verify its rollback result. Restore earlier successful invocations in reverse order using their exact recovery commands, then stop. A later backup alone cannot undo earlier repairs.\n\n## Decide whether diagnosis applies\n\nDiagnose unexpected writes/listing denied inside a `workspace-write` workspace, or reads the signed-in user should plainly have. Root-only failure and uniform failure are both eligible.\n\nStop and explain expected denials: writes outside the workspace, any write in `read-only`, piped grandchild `spawn EPERM`, or ConstrainedLanguage errors for .NET/COM/reflection. Request an approval for the one call you still need; do not repair ACLs for these cases.\n\n## Read the report before choosing a repair\n\nResolve the script from this skill's resource directory. Run it in the current sandbox first: classification completes under `workspace-write` with a writable `-Out` directory, while `read-only` cannot write one and refuses. When a denial or an unwritable report directory prevents the call, request one escalation of that same call through the normal approval path instead of assuming the diagnosis cannot run; if approval is refused, unavailable to the session, or forbidden by policy, report the path as undiagnosed and stop. Unconfined does not elevate the Windows token. Use one path per invocation; do not repeat `-Path` or pass a comma-separated string to `pwsh -File`.\n\n```powershell\n& '\\scripts\\diagnose-windows-sandbox-acl.ps1' -Path '' -Out '' -Compact\nexit $LASTEXITCODE\n```\n\nSubstitute full, quoted paths. Choose a persistent, user-owned `-Out` directory, preferably beside the failing workspace within the authorized tree. Never put reports or backups in the skill resource directory: it is deleted when the skill unloads. `-Compact` saves a unique full JSONL report and prints one `REPORT` summary. Diagnosis changes no ACL or existing content; it only writes this requested report. The summary includes all `inspectedPaths`, findings, decisions, ACL actions, verification, rollback state and `nextAction`. Use the summary to decide; read only specific records from the saved report when evidence is missing, without dumping the whole report or reading the implementation to rediscover these instructions.\n\nEach full record has `kind`, `operation`, `path`, `status`, `reason`, `details`. Unknown observations remain unknown. A deny ACE's presence alone does not establish causation. DSH provisions `S-1-4-*` workspace grants and an Everyone `DeleteSubdirectoriesAndFiles` deny; these are expected confinement entries, not package conflicts or automatically unresolved faults. Coexisting package ACEs may still block the confined child even when provisioning fails first; do not dismiss either finding. `completed` actions only confirm API execution. Check verification and the final summary. Missing summary means unconfirmed completion: inspect the report and recovery artifacts before doing anything else.\n\n| Verdict | Next step, only with complete observations |\n|---|---|\n| `CULPRIT` | Explain the affected paths and individual package allow SIDs; select `-Fix`. |\n| `PRECONDITION` | Explain missing effective `WRITE_DAC`/`WRITE_OWNER`; select `-GrantFullControl` if the caller has `WRITE_DAC`. |\n| `BOTH` | Grant on the affected object first. Only after verified success may a separate invocation use `-Fix`. Failure ends this sequence. |\n| `UNREADABLE`, `INCOMPLETE`, `NOT_THIS_CLASS` | Report observations and stop; do not infer a safe repair. |\n\n## Run the selected repair\n\nExplain the finding and cost first. `-Fix` removes individual `S-1-15-2-*` allow ACEs at their explicit sources, ancestor first, then verifies inherited entries disappeared; well-known groups ending in 1 or 2 are preserved. This removes those packages' access. `-GrantFullControl` adds a current-user allow ACE; it cannot cancel an explicit deny. Both preserve owner, inheritance and SACL, including Low integrity labels. Low executable labels and their effects outside DSH are outside this repair's scope. Full control supplies `WRITE_DAC` and `WRITE_OWNER`; taking ownership alone does not.\n\nAppend `-AllowRoot ''` and **exactly one** of `-Fix` or `-GrantFullControl` to the diagnostic command, before `exit`. Keep `-Compact -Out`. Every explicit source must be strictly inside that root; reparse paths and managed application trees (`LocalAppData\\Packages`, `ProgramFiles\\WindowsApps`) are refused. Never widen the authorized root to reach an ancestor. Missing effective `WRITE_DAC` in the unconfined caller requires stopping and reporting the path and missing rights for permission-policy review. Extracted scripts and recovery copies are user-writable: never supply commands to run them elevated, initiate UAC/`runas`, or run the whole agent elevated.\n\nFollow `nextAction`: `stop` ends repairs; `restore_pending_then_stop` requires the reported recovery commands in their listed order, then stop. Verified rollback does not turn a failed repair into success. Keep full report/backup paths and exact recovery commands for every invocation; never abbreviate executable paths. On recovery failure, stop and report the remaining commands without more repairs.\n\nFor `verify_original_confined_operation`, repeat the original failed operation in the original confined context. If it still fails, restore this workflow's successful repairs in reverse order, report the error and stop. Do not start another repair cycle.\n\nUse the user's language for progress and final reporting. Concisely report observations and analyzed paths (including relevant ancestors), actions and reasons, verification, and the next step; link full reports. Label any authorized unconfined fallback as such, not as successful repair. Explain that another tool may recreate a removed package ACE. Never write probe files, edit ACLs by hand, change owners, erase denies, or replace child permissions recursively. Do not provide manual ACL-edit commands for the user either: only bundled repairs and generated recovery commands are supported; unresolved denies require permission-policy review or moving the workspace.\n\n"}],"isError":false,"id":"{{message:6}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The skill is loaded."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4},"stream":[{"type":"chunk","time":1788699430709,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788699430709,"index":0,"dt":[],"texts":["The skill is loaded."]},{"type":"chunk","time":1788699430709,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788699430709,"index":1,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1788699430709,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The skill is loaded."}}},{"type":"chunk","time":1788699430709,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788699430709,"chunk":{"type":"usage","usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}}},{"type":"chunk","time":1788699430709,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/windows-acl-skill/snapshot.yml b/snapshots/session/windows-acl-skill/snapshot.yml new file mode 100644 index 0000000000..fd9c8d4202 --- /dev/null +++ b/snapshots/session/windows-acl-skill/snapshot.yml @@ -0,0 +1,9 @@ +version: 1 +scenario: windows-acl-skill +profile: headless +composition: default +recording: authored +header: + class: skill +workspace: + setup: windows-acl-skill diff --git a/tsconfig.base.json b/tsconfig.base.json index 750f340b12..52c4822158 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -282,6 +282,7 @@ "@deepseek-ai/dsh-client-ui-settings-plugins": ["./packages/client/ui-settings-plugins/src"], "@deepseek-ai/dsh-client-ui-settings-shell": ["./packages/client/ui-settings-shell/src"], "@deepseek-ai/dsh-client-ui-settings-agent-loop": ["./packages/client/ui-settings-agent-loop/src"], + "@deepseek-ai/dsh-client-ui-settings-session-log": ["./packages/client/ui-settings-session-log/src"], "@deepseek-ai/dsh-client-ui-settings-subagent": ["./packages/client/ui-settings-subagent/src"], "@deepseek-ai/dsh-client-ui-settings-web-search": ["./packages/client/ui-settings-web-search/src"], "@deepseek-ai/dsh-client-ui-jobs": ["./packages/client/ui-jobs/src"], diff --git a/tsconfig.client.json b/tsconfig.client.json index 926d792743..738ed0c5b4 100644 --- a/tsconfig.client.json +++ b/tsconfig.client.json @@ -122,6 +122,7 @@ { "path": "./packages/client/ui-settings-plugins" }, { "path": "./packages/client/ui-settings-shell" }, { "path": "./packages/client/ui-settings-agent-loop" }, + { "path": "./packages/client/ui-settings-session-log" }, { "path": "./packages/client/ui-settings-subagent" }, { "path": "./packages/client/ui-settings-web-search" }, { "path": "./packages/client/ui-user-questions" }, diff --git a/tsconfig.host.json b/tsconfig.host.json index aa021af6b9..3942b72b1b 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -68,6 +68,8 @@ "apps/web/tests/plugin-install-github.e2e.ts", "apps/web/tests/client-plugin-live.e2e.ts", "apps/web/tests/settings-chrome.e2e.ts", + "apps/web/tests/session-log-upload.e2e.ts", + "apps/web/tests/caption-overlays.e2e.ts", "apps/web/tests/settings-appearance.e2e.ts", "apps/web/tests/menu-material.e2e.ts", "apps/web/tests/desktop-locale.e2e.ts",