diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml index 04a53081d6..dc596b4fae 100644 --- a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.i18n.yaml @@ -9,8 +9,8 @@ en: 386864a282e5ad58 zh: a4ea75ec70eb7778 /agent-note-shared-native-cua-process-with-agent-scoped-state/decision: - en: ebf231a6aad109bb - zh: 53b553e679569da1 + en: 11bab6870c3d802f + zh: a22114f2217a3a41 /agent-note-shared-native-cua-process-with-agent-scoped-state/alternatives-considered: en: 883f71387f599160 zh: 4b5f1450be799397 diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md index 1bc795dd52..8e986aaa2f 100644 --- a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md +++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.md @@ -12,6 +12,8 @@ On macOS 26.6.2, replayd derives capture-client identity from the executable pat One user shares a native service per executable content digest across Agents and Hosts. A version lock elects the service; an additional canonical-executable-path lock prevents different builds published at one path from running together. Locks remain held until process exit. The managed startup command hands service lifetime to its client connections, and the last connection's completed cleanup ends the service. The diagnostic stdin/stdout entry proxies to this same service. +The service runs as a signed application launched through LaunchServices. Directly spawning the executable inherits its launcher’s TCC responsibility, so it cannot give the helper independent system permissions. A launch lock serializes discovery and startup. Complete signed applications use immutable content-addressed directories: replacing an executable used by a live process would violate both path-lock ownership and code-signature integrity. Ad-hoc builds can require fresh grants when their code hash changes; a stable bundle identifier alone does not preserve those grants. Installed packages retain their distributed signatures. + Each Agent connection owns its application handles, AX snapshots, limits and cancellation state. An app handle grants no exclusive use of the application. A FIFO orders complete desktop operations, including clipboard transactions, while response writes are independent. Agent disposal retires only its connection. The [object API decision](2026-09-17-experimental-cua-object-api.md) retains evaluator independence and coordinate semantics; the [consent decision](2026-09-26-cua-application-consent.md) retains application authorization. Cancellation and timeout first stop one request and wait for acknowledgement. A timed-out screenshot retains the single system-RPC slot until the real callback completes; new screenshots fail with `CAPTURE_BUSY` instead of accumulating requests. Late pixels are discarded, while other AX and input operations remain available. Ending an await or creating a wrapper does not prove that the system connection has recovered. An operation or cleanup that cannot stop can invalidate the process, making every old handle unusable. Uncertain input is never replayed automatically. diff --git a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md index c40d277837..371ae5cc3a 100644 --- a/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-09-cua-shared-native-helper.zh.md @@ -12,6 +12,8 @@ Status: implemented 同一用户的多个 Agent 和 Host 按可执行文件内容摘要共享原生服务。版本锁选出服务实例,规范化可执行路径上的另一把锁阻止发布在同一路径的不同构建同时运行。进程退出才释放锁。受管启动命令将服务生命期交给客户端连接,最后一条连接清理完成后服务退出。诊断用 stdin/stdout 入口也代理到同一个服务。 +服务通过 LaunchServices 以已签名应用运行。直接启动可执行文件会继承启动器的 TCC 责任,无法让 helper 独立取得系统权限。启动锁串行处理发现与启动。完整签名应用保存在不可变的内容寻址目录中,替换运行中进程的可执行文件会破坏路径锁的归属与代码签名完整性。临时签名构建的代码哈希变化后可能需要重新授权,固定 Bundle ID 不能保证保留授权。安装版保留分发时的签名。 + 每条 Agent 连接独立持有应用句柄、AX 快照、资源限制和取消状态。持有 app 句柄不代表独占应用。FIFO 队列串行执行包含剪贴板事务的完整桌面操作,响应写入独立进行。Agent 销毁只释放自己的连接。[对象接口决策](2026-09-17-experimental-cua-object-api.zh.md)继续负责执行器独立性及坐标语义,[授权决策](2026-09-26-cua-application-consent.zh.md)继续负责应用许可。 取消或超时先停止单次请求并等待确认。截图超时后,唯一的系统 RPC 槽位保留到真实回调结束;新截图返回 `CAPTURE_BUSY`,不继续堆积请求。迟到图片被丢弃,其他 AX 与输入操作仍可执行。结束 await 或创建包装对象不能证明系统连接已恢复。操作或清理无法停止时,才可能使进程失效,届时所有旧句柄均不可用。结果不确定的输入不会自动重放。 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 98b9eecb42..cd33be628c 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 @@ -9,14 +9,14 @@ en: f62d87b7623a9904 zh: 48bbb7c35298f96c /agent-note-windows-sandbox-rung-raw-acl-restricted-tokens-over-mxc-and-appcontainer/decision: - en: 125dea7427047966 - zh: 1e459b1106597695 + en: 10e9cfb7972cff03 + zh: 8406ba64d72a1fcc /agent-note-windows-sandbox-rung-raw-acl-restricted-tokens-over-mxc-and-appcontainer/acl-diagnosis-and-recovery: en: a5bcdf2a6133abfe zh: 477d4915d61945e3 /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 + en: 1ba89a61a733b371 + zh: 4fa134ad1b2059a1 /agent-note-windows-sandbox-rung-raw-acl-restricted-tokens-over-mxc-and-appcontainer/alternatives-considered: en: 6119e2aff3b6be5a zh: 4237c596c10e1ee9 @@ -38,3 +38,6 @@ /agent-note-windows-sandbox-rung-raw-acl-restricted-tokens-over-mxc-and-appcontainer/related: en: dd16f0c5815df991 zh: df3c1cf511ee52bf +/agent-note-windows-sandbox-rung-raw-acl-restricted-tokens-over-mxc-and-appcontainer/follow-up-2026-10-26-lua-token-is-dropped: + en: 5262178f0035b215 + zh: 10505c394ea33ee3 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 a108e42626..6cda3f1970 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 @@ -10,7 +10,7 @@ The original [historical sandbox decision](../../archived/feature/2026-07-06-san ## Decision -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. +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` is not used) 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 @@ -22,7 +22,7 @@ Both extracted and recovery scripts are user-writable, so this workflow never re ## 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. +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` disables every privilege except `SeChangeNotifyPrivilege`; `LUA_TOKEN` is not used, so the derived token keeps the caller's group SIDs. 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. ## Alternatives considered @@ -51,3 +51,7 @@ The product-visible Windows roster flip is win32-only, so keyless snapshots that The [pwsh executor decision](../../archived/feature/2026-08-01-pwsh-tool-and-executor.md) owns the pwsh-sandbox/tool-pwsh dialect split this rung consumes. The [delete-confinement decision](2026-09-19-windows-acl-mandatory-integrity-confinement.md) extends this rung with the Low integrity layer and the ambient-delete deny; it supersedes only the delete route documented here. + +## Follow-up (2026-10-26): `LUA_TOKEN` is dropped + +`CreateRestrictedToken` keeps `DISABLE_MAX_PRIVILEGE | WRITE_RESTRICTED` and no longer passes `LUA_TOKEN`: with that flag, the built-in Administrator user cannot start any process, so every confined launch failed. The write restriction still comes from the restricting SIDs and the Low label. 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 63d97f85a8..1f2848d31c 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 @@ -10,7 +10,7 @@ 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 机制实现该档:把调用者令牌复制为 `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 诊断与恢复 @@ -23,7 +23,7 @@ Status: implemented ## 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),但受限读的令牌需要逐路径的读授权——恰好重新引入身份路线付出的代价——而沙盒词汇表从不要求读隔离。 +身份路线靠"**谁**在跑子进程"来限制,本档靠"令牌派生"来限制。身份路线(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` 禁用除 `SeChangeNotifyPrivilege` 外的所有特权;不采用 `LUA_TOKEN`,因此派生令牌保留调用者的组 SID。同一原语其实也能限制读(`SidsToDisable` 把 SID 变为 deny-only),但受限读的令牌需要逐路径的读授权——恰好重新引入身份路线付出的代价——而沙盒词汇表从不要求读隔离。 ## 考虑过的替代方案 @@ -52,3 +52,7 @@ landstrip 评估在实现前已被否决(未经实战检验;自建 launcher [pwsh 执行器决策](../../archived/feature/2026-08-01-pwsh-tool-and-executor.md)拥有本档所消费的 pwsh-sandbox/tool-pwsh 方言划分。 [删除约束决策](2026-09-19-windows-acl-mandatory-integrity-confinement.zh.md)以 Low 完整性层与环境性删除拒绝扩展本档;它只取代本文档所述的删除路径。 + +## 后续追记(2026-10-26):去掉 `LUA_TOKEN` + +`CreateRestrictedToken` 仍传 `DISABLE_MAX_PRIVILEGE | WRITE_RESTRICTED`,不再传 `LUA_TOKEN`:带上该标志时,内置 Administrator 用户无法启动任何进程,受限启动全部失败。写限制仍来自 restricting SIDs 与 Low 标签。 diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml b/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml index f50ce14bc9..96e5042c3e 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml @@ -15,8 +15,8 @@ en: db0a2deb9193a00d zh: dab5a65b88957464 /agent-note-experimental-auto-review-before-each-tool-call/decision/one-complete-reviewer-request: - en: 7bd9fef90e8639b9 - zh: e2a096885c9c1e83 + en: 45ac7b4e759b29ad + zh: 5374cc89e587f076 /agent-note-experimental-auto-review-before-each-tool-call/decision/result-and-cancellation: en: 647fe68c52b907e0 zh: 267a739bad44ad65 diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.md b/.agents/notes/implemented/feature/2026-08-28-auto-review.md index 9434c92f41..547dd3a2cf 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.md +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.md @@ -41,6 +41,8 @@ The integration uses only the latest `request/header.config` provider/model and Each item is its own message and keeps the same text in later reviews: the pending call becomes a historical call, and the integration remembers, for each live Session, the schema each reviewed call showed so that it reappears at the same position. A review's messages therefore begin with all messages of the previous review. Providers that cache only at message ends, such as OpenAI implicit breakpoints on GPT-5.6 and later, or at a final marker with lookback, such as Anthropic `cache_control`, reuse the previous review; one growing user message let them reuse only the policy. The schema memory is process-local, so the first review after a restart omits earlier schemas and misses the cache once. +The request also carries the reviewed Session's `sessionId` and `purpose: 'auto-review'`. Routes that require a per-conversation identity or key prompt caching on one, such as the `x-opencode-session` header OpenCode requires and OpenAI `prompt_cache_key`, derive it from that id; without it OpenCode rejects every review, and Auto fails every call. The [DeepSeek Session-log upload](../../../../packages/session/session-log-deepseek/README.md#request-field) applies the same eligibility and registered exclusions to reviews as to other requests. The uploaded data is the canonical Session log; review prompts and decisions remain outside that log. + The reviewer builds the history and the pending call from the Session's complete action history: an authorization is the earlier call the `REVIEW_POLICY` source rules make eligible, and a duplicate or conflicting identity has to be visible anywhere in the log rather than inside a recent window, so neither the Session projections nor a bounded read serves the decision. The read is the deprecated synchronous `snapshotEvents()` under a line-scoped `typescript/no-deprecated` waiver. The main agent's V3 `system/message` nodes, assistant text/reasoning, and tool results are excluded. The current call must belong to the open step recorded by `step/start`; missing step ownership fails closed. It appears only as the final message; an unstarted sibling has no historical call fact. Native schema comes from the latest request header. PTC captures a frozen schema at binding construction and passes it through the scheduler into `ToolExecution`; descriptions and parameter schemas never enter start/settle events or the Session/SDK wire. Missing, inconsistent, or ambiguous action facts reject the call without consulting the live registry. An oversized request fails closed without summarization, truncation, another compaction pass, or a small output-token budget. diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md b/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md index 5ba7a8b39c..bc833d3fc3 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md @@ -41,6 +41,8 @@ Integration 只使用最新 `request/header.config` 的 provider/model 与 shi 每个条目各占一条消息,并在后续评审中保持相同文本:待审调用会变成历史调用,integration 为每个存活 Session 记住每次已审调用展示的 schema,使其在同一位置再次出现。因此一次评审的消息以上一次评审的全部消息开头。只在消息结尾缓存的 provider(如 GPT-5.6 及以后的 OpenAI 隐式断点),或在末尾标记处向前回溯的 provider(如 Anthropic `cache_control`),都能复用上一次评审;单条不断增长的 user 消息只能让它们复用策略。Schema 记忆只在进程内,因此重启后的第一次评审会缺少先前的 schema,并有一次缓存未命中。 +请求还携带被评审 Session 的 `sessionId` 与 `purpose: 'auto-review'`。需要按对话标识路由或以其为 prompt 缓存键的路由(例如 OpenCode 要求的 `x-opencode-session` header 与 OpenAI 的 `prompt_cache_key`)由该 id 派生;缺少它时 OpenCode 会拒绝每次评审,Auto 会让每个调用失败。[DeepSeek session-log 上传](../../../../packages/session/session-log-deepseek/README.zh.md#request-field)对评审和其他请求应用相同的上传资格与注册排除条件。上传的是权威 Session 日志;评审提示词和决策仍不进入该日志。 + reviewer 从 Session 的完整动作历史构建历史与待审调用:授权是符合 `REVIEW_POLICY` 来源规则的那个更早调用,而重复或冲突的身份必须在整份日志的任何位置都可见,而不是只在近期窗口内可见,因此 Session 投影与有界读取都无法支撑该判定。这次读取使用已废弃的同步 `snapshotEvents()`,并带有行级 `typescript/no-deprecated` 豁免。 主 agent 的 V3 `system/message` 节点、assistant 正文/reasoning 与 tool results 全部排除。当前调用必须属于 `step/start` 记录的开放 step;缺少 step 归属时拒绝执行。该调用只作为最后一条消息出现;尚未开始的 sibling 没有历史调用事实。原生 schema 来自最新 request header。PTC 在 binding 构造时捕获冻结 schema,经由调度器传入 `ToolExecution`;描述与参数 schema 不进入开始/结算事件或 Session/SDK wire。动作事实缺失、不一致或有歧义时拒绝调用,不查询 live registry。超窗请求直接拒绝,不做摘要、截断、额外 compaction 或设置小型输出 token 预算。 diff --git a/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.i18n.yaml b/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.i18n.yaml new file mode 100644 index 0000000000..fc9f88a600 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.i18n.yaml @@ -0,0 +1,55 @@ +# Bilingual-pair consistency record for 2026-09-13-liveshare-protocol-and-owner-surfaces.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/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.md +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague: + en: e73b246e19c9b412 + zh: c87594ad5eccbfb5 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/problem: + en: 17bc15d382139909 + zh: 3eff34b5ddea951e +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision: + en: a15c7c13f167e5ac + zh: a15c7c13f167e5ac +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/what-a-share-is: + en: ff1dbb0f7c2355e4 + zh: 3de92b2e6c5e9e15 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/what-a-reader-can-do: + en: 5652ed7f515a2a5d + zh: 993378f78f5a4120 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/two-driving-tiers: + en: 0b9303333bf482ae + zh: 6dfc68ad9e2924c3 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/default-visibility: + en: 754705367c350b31 + zh: e10b6541458c4a45 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/how-shared-work-stays-inside-its-grant: + en: fa3ab42895a16ce1 + zh: 429be63cfb8e55a6 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/keeping-one-branch-across-retries: + en: 6b0369dafe34a5aa + zh: aea9465216204122 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/who-the-owner-is: + en: 7e6d1305782d2822 + zh: 66fb2cbef0f49d47 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/what-the-reader-sees-at-the-top: + en: 44d02a91cf0c5715 + zh: dabbf6c59ff08e4a +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/when-the-connection-breaks: + en: 36ee6e090797806b + zh: 21809a0cf990d905 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/how-the-served-page-is-exercised: + en: d9cbb8277862587c + zh: 867fa6c4db95d09c +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/what-the-package-exposes: + en: d13a0df787f212d4 + zh: 24794e8bf4d92492 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/alternatives-considered: + en: 2daeba6e763383e4 + zh: fe0fe3f7f4d546e7 +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/verification: + en: 76cc786336c5daf5 + zh: 98bd2b9cc7d1791c +/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/consequences: + en: 8a6e06a9fba917e2 + zh: 68a9d19b15119345 diff --git a/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.md b/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.md new file mode 100644 index 0000000000..a4d1c6af81 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.md @@ -0,0 +1,156 @@ +# Agent Note: Liveshare — sharing a DSH session with a colleague + +Status: implemented + +English | [中文](2026-09-13-liveshare-protocol-and-owner-surfaces.zh.md) + +## Problem + +A DSH session runs on your own computer. A colleague sometimes needs to watch one while you work — to follow a debugging session, or to see how a long task is going. Today there is no way to do that. Sending the session log is not the same thing: the colleague would need your plugins, configuration, and sandbox to render it, and the session only makes sense while it is running on your machine. + +So a share needs three things at once. A **page the colleague opens in a browser** that shows the session running on your machine, without connecting them to it. A **limit on what that page can do**, starting with "read without writing" — anything the page offers beyond what you agreed is a security hole, not a convenience. And an **answer to "who shared this?"**, because a page that cannot say who its owner is cannot be trusted by whoever opens it. + +This note covers the half that runs inside the owner's DSH — the share dialog, the owner-side service behind it, the package's browser face, and the page a reader opens — plus the protocol package both sides speak. The Liveshare server, the shared service in the middle, is its own package (`@deepseek-ai/dsh-experimental-liveshare-relay`) and is reviewed and deployed separately. + +## Decision + +### What a share is + +An owner creates a share for one session from DSH itself. The share is published to the Liveshare server, a service both sides can reach, and DSH returns a link. Opening that link shows the session rendered by the owner's own DSH with its own plugins, so the reader never needs the owner's configuration. The reader's list contains the source Session and any children that reader created through the share. Workspace names, membership, and ordering are trimmed to those Sessions; unrelated Sessions and workspaces stay hidden. + +A share created from the DSH Share dialog is an **organization link** share: it carries `accessMode: 'open'`, so anyone in the organization who has the link is admitted. The deployment's `LIVESHARE_ALLOWED_SUBJECTS` is unioned into every share's ACL: it narrows an `open` share to the subjects it lists, and on a stored `selected` record it adds those subjects beside the record's own selection. + +**Specific colleagues (`selected`) and the older `approval` spelling are legacy stored records.** The Host still reads them with their recorded ACL and shows their access summary, and the Liveshare server still admits their viewers by that ACL, but the shipped Share dialog offers no way to create one. `liveshareShares.create` keeps its `accessMode` and `allowedSubjectIds` fields for those records and for callers that invoke the Remote directly; the shipped dialog sends neither. + +### What a reader can do + +The reader's page is the normal DSH client, not a cut-down copy. The sidebar, the session list, expanding, collapsing, and selecting are all there, because browsing is part of reading. What is missing is every action the reader is not allowed to perform: creating an unrelated session, renaming, archiving, deleting, and managing a workspace. Branching follows the share's own fork switch, prompting is available in both tiers and runs inside that tier's sandbox, and attachment upload follows the Sessions the reader can see; management permissions remain independent of the tier and the fork switch. + +These actions are not hidden with CSS. One access snapshot decides what the page may do: the remote-access client follows the reader's own `remoteAccess/follow`, and each button and menu entry reads its endpoint's answer on the global resource, a Session, or a Workspace through `useRemoteAccess`, `useAccess` or `useSessionAccess`, or `useWorkspaceAccess`, rendering again whenever the answer changes. The owner's DSH computes the snapshot from this share's `share-viewer` policy, so an ability the owner withdraws closes without reloading the page — and the server refuses those actions anyway; the page only avoids offering what would be refused. The served index carries the reader's opening snapshot, so no entry appears before the page knows it is allowed, and after the snapshot stream fails every answer is refused. The owner's own page opens with the owner baseline and behaves exactly like an ordinary DSH. Per-Session refusal reasons are not part of the snapshot: the page states the page-level reason (share mode, stop, expiry, lost access) and generic read-only copy. A page that cannot read the Session at all — offline owner, stopped or expired share, lapsed sign-in — states that condition at the top and gives the reader an action, instead of leaving an empty shell or a bare code. + +### Two driving tiers + +Every share carries a `ViewerSessionMode` with two values, and both drive the shared Session: + +- **view** — the reader drives the shared Session's loop through the normal prompt path, inside a read-only sandbox in the granted workspace, so the Session's tools never write a file. The read and search surface (`read`, `read_image`, `glob`, `grep`, `present`), the tools with no local file effect (`todo_write`, `skill`, `web_search`, `web_fetch`). Every declared path is checked before the tool runs, and a tool whose call cannot be checked is refused. +- **write** — the reader drives the same loop in a bounded writable sandbox that also modifies files inside the granted workspace, under the Host's write ceiling. + +The sandbox is installed on the Session, but it confines only the turns a share viewer starts: `agent/pre-step` records whether an admitted turn's entering input names a share viewer, and the guard and the replacements read that answer for each execution. A turn the owner starts keeps the Session's tools; an owner's steering input inside a viewer's turn stays confined with it. The marker lives in Host memory, is recomputed for each turn, and never enters the Session log. Stopping every share, or letting them all expire, disposes the layer and gives the Session its own tools back. + +The earlier vocabulary had three tiers — `view` watched, `fork-readonly` branched, and `write` could write. `view` and `fork-readonly` differed only in whether the reader could branch and drive a child, which the share's own `allowsFork` switch already decides; a third mode duplicated that switch and let the two answers disagree, so the two driving tiers replaced it. + +Forking is not a tier. The share carries its own `allowsFork` switch, independent of the tier: a `view` share may allow it and a `write` share may refuse it. A fork creates a child the viewer owns with its own read-only or writable ceiling and leaves the source untouched. + +Attachment upload is not a tier or a switch. `fileUploads/upload` rides the read role, so every Session a share serves the reader accepts files; visibility alone decides it, and the owner has no separate attachment switch. + +A mode this version does not recognize, and a persisted record whose mode the host cannot establish, are not driven: the share stays readable, and the page reports what it could not establish. The withdrawn `fork-readonly` name is not read back as `view`; a persisted owner record that names it is rejected when the owner state loads. + +The sandbox is a property of the Session, but it applies per turn: the Host records, in memory, whether the admitted input of the running turn names a share viewer, and confines that turn while leaving an owner-started turn on the Session's native tools. The earlier shipped design confined every turn, including the owner's; it needed no turn read, but a share changed how the owner's own work ran. Persisting a turn's requester was rejected: a committed-message projection had to be folded, re-read for every execution, and kept in step with the share that admitted it, and it still had to fail closed on mixed or unestablished requesters. The in-memory marker keeps the guard and the replacements reading one answer per execution without adding a Session event; a restart recomputes it from the next admitted step, and a rejected or never-admitted step leaves no marker. + +### Default visibility + +The package ships as a runtime dependency of every dsh installation, but a default installation must show it nowhere in the plugin manager: neither under **Built-in plugin settings**, nor under **Experimental plugins**, nor on the **More** page. A person enables it deliberately — by inserting the bundle's rows in the active profile's `cordis.patch.yml`, or by selecting the bundle in `dsh.profile.bundles`; the [package README](../../../../packages/experimental/liveshare/README.md) owns both routes. The package must still ship with the release and stay installable, and users who enabled it before this rule keep seeing it. + +The Host's bundle `enabled` flag states whether the profile selected the bundle, which is not the same as whether the bundle's plugins run: inserting the bundle's rows through `cordis.patch.yml` starts its plugins while leaving the bundle unselected, so a visibility rule built on `enabled` would hide a bundle the person had just enabled and would strand the documented enable path. + +The package declares the hiding intent in its own bundle manifest as `dsh.bundle.listedByDefault: false`; the manager keeps one generic visibility rule rather than a client-side name set. A bundle whose manifest does not declare `listedByDefault: false` is listed; one that declares it is listed only when the profile enables it, the installation carries it selected, or one of the rows its bundle patch declares is running (`pkg.rows.some(row => row.enabled)`). The filter applies before both lists are derived, so the name is absent from the main Experimental plugins group, the More page, Built-in plugin settings, and the Installed plugins group until one of those holds. The Host's `listBundles` contract, the profile format, and the installed package set are unchanged. `BundleInfo.rows` already joins each declared row with the Loader entry currently carrying it, and the client's `PackageView` carries whether that entry runs, so row state is the effective answer for both enable routes: removing the rows, disabling them, or deselecting the bundle stops them and the card leaves both lists again. + +A default-hidden bundle is not addressable by ID while its card is hidden: `openPkg` resolves only the listed inventory, so `pluginNavigation.openBundle('@deepseek-ai/dsh-experimental-liveshare')` renders no details until the same enablement conditions list it. + +### How shared work stays inside its grant + +The Owner, not the viewer page or the Liveshare server, authorizes each Session operation. A child receives its real scoped tool restrictions before it can accept a prompt, including after a cold resume: the host installs the limit from the base `agent/created` event. That listener runs after the agent entered the registry, but creation has not resolved, AgentLoop holds the agent's queued input, and a throw fails creation, so a refused limit still prevents any prompt. A missing host service or failed installation refuses the child; a configuration flag, a tool name, or a promise to install later is not enforcement. + +A shared source Session can still serve its owner's own work. Every reader call is dispatched through the owner's own Connection and Gateway as that share-and-reader pair's own Peer, whose `share-viewer` principal (the share id and the server-asserted identity triple) is fixed by in-process owner code when the Peer is created; no wire field can change it. The Gateway decides the Peer's calls, and the Session notifications it forwards to the Peer, through remote access: this package registers the `share-viewer` policy, which binds a served reader to read roles on the shared Session, its own forks, and the Workspaces holding them, and to fork and write roles whose conditions apply the share mode; it binds nothing else, so every other endpoint and notification is refused, and it binds nothing once the share stops serving the reader. The owner invalidates the reader's access on every grant, revocation, mode change, and expiry, and disposes the reader's Peer when the grant ends or the share stops or expires. A reader's prompt enters the Session as a `user-rpc` message whose `source.principal` names that reader; owner messages carry no principal. While any share over that Session is active, the owner host mounts its own confined replacements for the read and search surface and the no-effect tools — plus bounded `edit` and `write` when some share may write — in the Session's own scope, and a viewer-started turn may run nothing else. An owner-started turn keeps the Session's native tools, so a share never narrows the owner's own work. The pre-step records which kind of turn is running, in memory only. Each operation re-reads the shares' current Relay grants and checks the resolved target against the granted root; the pre-step re-checks every entering message's viewer against the Relay's admission before a model step, so reader input queued before a revocation never drives the model. A reader's stop of the shared Session is dispatched through the Gateway like a prompt: the policy admits it while the share's tier is established and the Session is confined, and the owner's own Session controller stops whatever turn that Session is running, the owner's included. The general Peer, principal, and remote-access design is in [Peer principals and remote access](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md). + +A confined file tool resolves every path its own arguments declare, checks that the granted root contains each result, and hands the deployment's tool that canonical path; a pathless search uses the Session's working directory, which the replacement checks like a named path. A bounded edit or write performs the same containment check and then edits or writes through the filesystem backend with the Session's current sandbox policy and the normal observation policy. + +### Keeping one branch across retries + +One fork request binds its viewer, share, source, operation identity, and completed prefix. The Owner reserves that operation durably before creating a child, and records the child the owner's fork answer returned; a retry of a recorded operation replays that answer instead of forking again. A different prefix under the same identity is refused. The owner's `SessionForkRequest` mints the child id, so the Host sends no child identity: a host that dies after the child was created but before the operation was recorded cannot name that child on retry and creates a new one. Closing that window needs a Session Controller request that accepts a caller-supplied child identity. The inherited prefix stops before unrelated queued input; only immediately following request metadata is retained. Revoked children keep their logs but cannot resume with broader access, and removing a share does not expose the source Session's unrestricted tools. + +### Who the owner is + +A share can only be created by a verified owner. The dialog shows the identity the owner's DSH read back — name, expiry, when it will renew — and uses a label derived from the identity when there is no display name. The code that creates the share refuses an unverified owner as well, so the question is not settled by whether a button was clickable. + +Signing in happens once, in a window the page opens while the owner clicks. The DSH creates a short-lived transaction tied to the Liveshare server address, to the Liveshare server instance, and to the page allowed to finish it; the page opens the sign-in page the DSH names, accepts one one-time code only from that exact origin, window, and transaction, and hands it back to its own DSH. + +The Owner redeems the code with the verifier it retained, stores the credential with owner-only permissions, and sends it only to the Liveshare server address that issued it. New registration requires that verified credential; existing-share reconnect uses the share's management token, and explicit identity claim preserves the share's ACL, mode, expiry, and token. A share's named colleagues are the owner's stored selection plus the deployment's own allowlist; nothing is derived from a display identity. + +### What the reader sees at the top + +The served page reuses the sidebar's existing brand row instead of adding a bar on top of the app: the usual Harness mark, plus a LiveShare entry that names who shared the session and which DSH version they run, with the Liveshare server's own release labeled separately. Clicking it returns to the page listing the shares you can open. + +### When the connection breaks + +A reader's feed can fail: a stream disappears, the Liveshare server cancels a request, a read times out. The page retries only the failures that are defined as repeatable, and stops for good on authentication, permission, and missing-share errors, showing the reason instead of reconnecting forever. + +### How the served page is exercised + +The page a viewer opens is the owner's own Web page. The Liveshare server asks the owner for its rendered index document, places a short carrier script at its top, and passes the page's shell assets and plugin bundles through from the owner, so the page runs every owner plugin at the owner's version. The carrier only turns the page's calls and streams into reads over the Liveshare server's channel; the Liveshare plugin itself recognizes a viewer page and mounts the viewer surfaces instead of the Share action. The owner renders that index with the reader's own access snapshot in place of the owner baseline, and the page then follows the reader's `remoteAccess/follow` snapshot; no other read carries a reader's grants. + +The viewer page is tested in-process: the test assembles the page from the carrier, the shell, and the client plugins including Liveshare's, stands in for the Liveshare server's read channel, and answers the page's reads from a scripted owner, so the assertions see the real shell, carrier, and presentation plugins. + +### What the package exposes + +The package ships one Host program and one browser face. Its public entries are the Host export (`lib/index.js`), `./client` (`lib/client.js`, a closure-factory bundle), `./viewer-declarations`, the share-management Remote's `./typert` and `./remote` faces, `./types`, the bundle patch `./cordis.patch.yml`, and the locale JSON files. The manifest declares the browser face as `dsh.client` with `platform: web` and the client services it needs; that face reads `__DSH_LIVESHARE_VIEWER__`, mounts the viewer session plugin and its brand, model, permission, session-row, and share-path surfaces on a served viewer page, and otherwise mounts the owner's generated Remote before the Session-header Share action. + +Share management is one `liveshareShares` Typert Remote with twelve methods — `create`, `list`, `stop`, `updatePermissions`, `setCapabilities`, `capabilities`, `relay`, `setRelay`, `identity`, `audit`, `testRelay`, and `testPublish` — over the operations the owner host runs. The only same-origin HTTP routes left are the four owner sign-in and credential routes: `/api/liveshare.ownerLoginStart`, `/api/liveshare.ownerLoginRedeem`, `/api/liveshare.ownerLoginCancel`, and `/api/liveshare.ownerCredential`. + +The outgoing gateway is a supervised child process whose source entry is `src/owner-gateway/cli.ts` and whose published entry is `lib/owner-gateway/cli.js`; the directory name keeps it distinct from the DSH API gateway. + +## Alternatives considered + +- **Build a separate read-only page instead of serving the normal client.** Rejected: reading needs browsing, grouping, and selecting, so a second page would have to rebuild all of it and would quietly lose the navigation the product keeps. +- **Decide each button's availability where the button is.** Rejected: every entry reads one access snapshot the owner computes, so a screen added later cannot quietly reintroduce an action the reader may not perform. +- **Let the reader's page decide its own access.** Rejected: only the owner and the service decide what a share grants; the page states what it was told. +- **Treat an older share with no mode as writable.** Rejected: that would hand a reader access the owner never configured, so a share without a mode is read-only. +- **Put the sign-in code in the page address, or read the credential from the popup's cookies.** Rejected: addresses leak through history, referrers, and logs, and a page should not hold credentials. +- **Show the service's own error text when the page cannot explain a refusal.** Rejected: that text may contain a file path, a credential, or another reader's session, so the page shows the stable reason it can vouch for. +- **Put execution restrictions only in editable configuration or a sandbox mode.** Rejected: a child can change configuration, and per-call execution choices can outrank a mode. The real operation needs a monotonic guard and a verified implementation. +- **Check a model-supplied path only before dispatch, or globally constrain every filesystem read.** Rejected: a pre-dispatch check is separated from the tool call by other listeners, while a global read rule would also constrain unrelated Owner work. The containment check runs inside the authorized Session's own read and write. +- **Reuse the deployment's default root or copy the entire source tail into a child.** Rejected: both can include files or pending input the viewer was never granted. +- **Use a deployment-wide secret or self-reported display fields as owner identity.** Rejected: neither identifies the logged-in person who approved this publication. +- **Keep three tiers.** Rejected: `view` and `fork-readonly` differ only in whether the reader can branch and drive a child, which is already the independent fork switch. A third mode duplicates that switch and lets the two answers disagree. +- **Keep a per-share attachment-upload switch.** Rejected: an attachment is part of a prompt on a Session the reader can already see. The separate switch duplicated visibility and could only subtract from it, while the upload role already rides the read role. +- **Attribute every turn to a requester and exempt the owner's own turns.** Rejected after it shipped, then narrowed: persisting a turn's requester needed a Host-only projection over message sources, a turn-boundary read per execution, and a re-check of the share that admitted each turn, and it still had to fail closed on mixed or unestablished requesters. The shipped design keeps only an in-memory marker for the running turn — `agent/pre-step` reads the entering principals and the guard reads that answer — so an owner-started turn keeps the Session's native tools without adding a Session event. The accepted cost is that an owner's steering input inside a viewer's turn stays confined; a viewer message that reaches a later step narrows that turn. +- **Let each tool name its own tier or sandbox.** Rejected: the tier belongs to the share. A per-call choice would let one execution outrank the grant that admitted it. +- **Test the bundle's `enabled` flag directly.** Rejected: it would keep the code smallest, but the patch route starts the bundle's plugins while leaving the bundle unselected, so the card would stay hidden exactly when its plugins started, contradicting the product rule. +- **Derive effective enablement in the Host.** Rejected: `listBundles` could report a new or redefined effective state, but that would change a cross-package contract for one client-only presentation rule and invite every bundle consumer to reinterpret `enabled`, which must keep meaning "selected in the profile". +- **Ship LiveShare outside the installation's optional bundles.** Rejected: that would make it visible nowhere by construction, but the package must still ship as an npm package with the release and be installable, and it would also remove the bundle from the Host's list entirely, taking the details entry with it. +- **Keep the colleague picker, or replace it with a directory-backed selector.** Deferred: naming a colleague needs an identity directory the Liveshare server does not provide, so the dialog cannot offer a choice it can verify. Stored `selected` and `approval` records keep working through their ACL, and the creation entry point stays retired until a directory contract exists; `liveshareShares.create` keeps its fields for those records and direct Remote callers, so this defers the API rather than deleting it. + +## Verification + +- A reader's page keeps the normal navigation, and no action the reader may not perform appears on the shared session or on any branch. +- A DSH without the Liveshare bundle, and the owner's own page, behave exactly as before. +- A share uses one of two tiers and that tier's sandbox; unknown modes, unreadable answers, and shares without a mode stay readable but are never driven. +- A share cannot be created without a verified owner identity. +- A share created from the dialog carries `open`; a stored `selected` or `approval` record keeps its recorded ACL and access summary, and `create` still stores a requested `selected` mode with its subjects. +- Sign-in accepts one one-time code from the transaction's origin, window, and state, and nothing else; a failed or abandoned attempt discards the transaction, and no credential, cookie, or code reaches a URL, storage, or a log. +- Real Controller, ToolRuntime, filesystem, and profile tests cover fork identity, cold enforcement, bounded execution, revocation, and normal cancellation. The built viewer must use its real POST carrier and Owner authorization handler; deterministic display fixtures do not replace execution or persistence tests. +- Generated API documentation and both SDK projections remain aligned with the `user-rpc` source principal, which only the `agent/pre-step` admission check consumes. +- `ViewerSessionMode` has two members, and a persisted `fork-readonly` record is rejected when the owner state loads. +- A `view` share drives the source Session, refuses file writes and out-of-grant reads, and can fork when `allowsFork` is set. +- `fileUploads/upload` is allowed on every Session the reader can see, for every tier and fork combination. +- Source tool decisions read only the Session's mounted layer: the guard permits this installation's confined replacements for the read and search surface and the no-effect tools, plus bounded `edit` and `write` on a writable Session, and refuses every other name, with no turn, requester, or origin consulted. +- Unit and real-runtime tests cover the tier policy, confined reads and writes, fork ceilings, the layer's installation and release, and the pre-step admission check over the durable message source. +- The plugin-manager component tests hold the bundle out of both lists while its declared rows are off, list it once a declared row runs, and still open it by bundle ID while it is hidden; the recorded goldens and the refresh card count move with that rule. + +## Consequences + +The reader's page can only be as accurate as what the service tells it. Capability answers, the owner's identity status, the session mode, the owner's DSH version, and the sign-in endpoints all come from the owner's DSH and the Liveshare server; until those are deployed together with this layer, the page stays at its most conservative setting — read-only, with a sign-in that cannot complete — rather than guessing. + +The page is not a security boundary. The owner's DSH and the Liveshare server refuse a reader's writes no matter what the page does; the access snapshot exists so the page does not offer actions that would only be refused. + +Existing ACLs are retained. A record without a mode is readable but not driven, and contradictory mode fields are refused. Unattributed shares are shown as unverified history. + +Owner reconnect state is atomically replaced and queued within one process, but multiple processes sharing one file can still lose updates or compete for a gateway. Cross-process execution ownership is unresolved; atomic replacement alone is not a transaction or a route claim. Containment is checked by path: a local process that replaces a directory inside the grant with a symlink between the check and the open can redirect a viewer's read, or a write elsewhere within the sandbox's writable roots. Operations anchored to a directory handle would close this window and are left to a separate change. Final viewer-artifact and SDK snapshot evidence remains required before release. + +A `view` share is not watch-only: it runs the reader's prompts and tool calls inside the read-only sandbox. A deployment that wants a passive reader has no separate mode and must rely on the reader not sending prompts rather than on a tier name. Fork reach is the only independently switchable action; attachment upload cannot be disabled separately. + +The sandbox reaches the owner's own turns too. While any share over a Session is active the owner can only read inside the granted workspace, and can write only when some share allows writing; a share that is merely published — even one nobody is using — therefore changes the owner's own tool surface until it is stopped or expires. Cancellation has no attribution either: a reader's stop ends whatever turn the shared Session is running. + +The default cards list, the More page, and their recorded goldens lose one card, and the manager's refresh card count drops by one. A person who only knows the plugin page cannot discover LiveShare or switch it on from that page; the package README owns the two enable routes instead. A bundle enabled through the patch route shows its card with the switch off while its rows run, because the switch reflects the profile's bundle selection and writes that selection; the documentation states the two routes and this difference. The rule covers one explicitly named bundle rather than a manifest flag, so adding another hidden-until-running bundle, or retiring this one, is a one-line set edit with a test and docs update. + +Shipped creation produces organization-link shares only: the Share dialog sends neither `accessMode` nor `allowedSubjectIds`, so every new share is `open`, and the deployment's `LIVESHARE_ALLOWED_SUBJECTS` is the only subject-level narrowing a new share can carry. Stored `selected` and `approval` records keep working and keep their access summaries, and the create fields stay on the Remote for those records and direct callers while no UI exposes them. diff --git a/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.zh.md b/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.zh.md new file mode 100644 index 0000000000..871eb3fe91 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.zh.md @@ -0,0 +1,156 @@ +# Agent Note: Liveshare —— 把 DSH 会话分享给同事看 + +Status: implemented + +[English](2026-09-13-liveshare-protocol-and-owner-surfaces.md) | 中文 + +## Problem + +DSH 会话跑在你自己的电脑上。同事有时需要在你工作的同时看某个会话——跟着一起排查问题,或者看看一个长任务进展到哪了。现在没有这个能力。把会话日志发过去并不等价:对方要有你的插件、配置和沙箱才能渲染出来,而且会话只有在你机器上运行时才有意义。 + +所以一次分享要同时解决三件事:一个**同事用浏览器打开就能看的页面**,看到你机器上正在跑的会话,但不需要连到你的机器;一个**对页面能做什么的限制**,从"能读、不能写文件"开始——页面多给一个你没同意的操作就是安全漏洞,不是便利;以及一个**"这是谁分享的"的答案**,因为说不清归属的页面,打开它的人无从信任。 + +这条 Note 覆盖 owner 自己 DSH 里运行的那一半——分享对话框、背后的 owner 端服务、本包的浏览器端,以及读者打开的页面——以及双方共用的协议包。中间的 Liveshare 服务端是独立的包(`@deepseek-ai/dsh-experimental-liveshare-relay`),单独审查与部署。 + +## Decision + +### 一次分享是什么 + +owner 在 DSH 里为某一个会话创建分享。分享发布到 Liveshare 服务端(双方都能访问的服务)后,DSH 返回一个链接。打开链接看到的是 owner 自己的 DSH 用自己插件渲染出的会话,读者不需要 owner 的配置。读者列表包含源会话,以及该读者通过分享创建的子会话。工作区名称、成员和顺序裁剪为这些会话;无关会话和工作区保持不可见。 + +从 DSH 分享弹窗新建的分享是**组织内持链接**分享:它携带 `accessMode: 'open'`,组织内拿到链接的人都能看。部署级 `LIVESHARE_ALLOWED_SUBJECTS` 会并入每份分享的 ACL:对 `open` 分享,它把准入收窄到列出的主体;对已存储的 `selected` 记录,它把列出的主体加在记录自己的选择旁边。 + +**指定同事(`selected`)与更早的 `approval` 拼写是旧版持久记录。** Host 仍按记录里的 ACL 读取它们并展示访问摘要,Liveshare 服务端仍按该 ACL 接纳它们的查看者,但随版本发布的分享弹窗不提供新建入口。`liveshareShares.create` 的 `accessMode` 与 `allowedSubjectIds` 字段保留给这些记录以及直接调用该 Remote 的调用方;随版本发布的弹窗两个都不发送。 + +### 读者能做什么 + +读者页面就是正常的 DSH 客户端,不是精简版:侧栏、会话列表、展开折叠、选择都在,因为浏览本来就是"看"的一部分。缺的只是读者无权执行的动作——创建无关会话、重命名、归档、删除、管理工作区。分支遵循分享自己的 fork 开关;发送消息在两档里都可用,并在该档的沙箱内运行;附件上传跟随读者可见的会话。管理权限与档位和 fork 开关都保持独立。 + +这些动作不是用 CSS 藏起来的。页面能做什么由一份访问快照决定:remote-access 客户端跟随读者自己的 `remoteAccess/follow`,每个按钮和菜单项经 `useRemoteAccess`、`useAccess` 或 `useSessionAccess`、`useWorkspaceAccess` 读取其端点在全局资源、某个会话或某个工作区上的答案,答案变化时重新渲染。owner 的 DSH 依据该分享的 `share-viewer` 策略计算快照,因此 owner 收回某项能力时,对应入口不用重新加载页面就会关闭——而且服务端本来就会拒绝这些操作,页面只是不去提供注定被拒绝的按钮。读者收到的 index 已带上读者的首份快照,因此页面在确认允许之前不会显示任何入口;快照流失败后,所有答案都为拒绝。owner 自己的页面以 owner 基线打开,行为与普通 DSH 完全一致。快照不包含逐会话的拒绝原因:页面陈述页面级原因(分享模式、停止、过期、失去访问权)和通用的只读文案。拿不到分享正文时,页面顶部直接陈述该状态,并且每一档都给出读者自己能做的动作(重试、重新登录、回到分享列表,或明确指引联系分享者),不会停在空白外壳或错误码上。 + +### 两档驱动 + +每个分享带一个 `ViewerSessionMode`,只有两个值,且两档都会驱动共享会话: + +- **view** —— 读者通过普通 prompt 路径驱动共享会话的 loop,在授予工作区的只读沙箱里运行,因此会话的工具绝不写文件。读取与检索面(`read`、`read_image`、`glob`、`grep`、`present`)、无本地文件副作用的工具(`todo_write`、`skill`、`web_search`、`web_fetch`)都限制在授权范围内:工具运行前检查其声明的每个路径,无法检查的调用一律拒绝。 +- **write** —— 读者在受宿主写入上限约束的可写沙箱里驱动同一个 loop,沙箱也会修改授予工作区内的文件。 + +沙箱安装在会话上,但只约束分享查看者发起的轮次:`agent/pre-step` 记录一个已准入轮次的进入输入是否带有 share-viewer principal,守卫与替代定义对该轮次的每次执行读取这一结论。所有者自己发起的轮次保留会话自己的工具;所有者在查看者轮次中追加的输入仍留在该受限轮次内。该标记只存在于 Host 内存,按轮次重算,不写入会话日志。所有分享停止或全部过期后执行层被释放,会话恢复自己的工具。 + +较早的词汇有三档——`view` 只观看、`fork-readonly` 分支、`write` 可写。`view` 与 `fork-readonly` 只差在读者能否分支并驱动子会话,而这已经由分享自己的 `allowsFork` 开关决定;第三档重复了该开关,还会让两处答案互相矛盾,因此由两档驱动取代。 + +fork 不是一档。分享自带独立的 `allowsFork` 开关,与档位无关:`view` 分享可以允许 fork,`write` 分享也可以拒绝。fork 创建的子会话归读者所有,带有自己的只读或可写上限,源会话保持不变。 + +附件上传既不是一档,也不是一个开关。`fileUploads/upload` 随读角色授予,因此分享向读者提供的每条会话都接受文件;只由可见性决定,所有者没有单独的附件开关。 + +本版本不认识的模式、以及宿主无法确定模式的持久记录,都不会被驱动:分享仍可读取,页面会说明它无法确定什么。已退场的 `fork-readonly` 名称不会读回为 `view`;持久化的 owner 记录若写有该值,owner 状态加载时会直接拒绝。 + +沙箱是会话的属性,但按轮次生效:Host 在内存中记录运行中轮次的已准入输入是否带有 share-viewer principal,只约束该轮次,把所有者发起的轮次留在会话原生工具上。此前上线的实现限制每个轮次(包括所有者的),虽不需要读取轮次,却让分享改变了所有者自己的工作方式。持久化轮次请求者被否决:那需要从已提交消息来源折叠的投影、每次执行读取 turn boundary、并与接纳该轮次的分享保持同步,而且在混合或无法确立请求者时仍必须默认拒绝。内存标记让守卫与替代定义对每次执行读取同一个结论,且不新增会话事件;重启后从下一个准入步骤重新计算,被拒绝或从未准入的步骤不留下标记。 + +### 默认可见性 + +本包作为运行时依赖随每个 dsh 安装一同交付,但默认安装必须在插件管理器里完全不显示它:**内置插件配置**下不显示,**实验性插件**下不显示,**更多**页也不显示。用户主动启用它——在当前 profile 的 `cordis.patch.yml` 中插入组合包的行,或在 `dsh.profile.bundles` 中选中该组合包;两条启用路径由[包 README](../../../../packages/experimental/liveshare/README.zh.md)负责。该包仍必须随版本发布、保持可安装,且在此规则之前已启用的用户继续能看到它。 + +Host 的组合包 `enabled` 标志表示 profile 是否选中了这个组合包,这与它的插件是否在运行并不等价:通过 `cordis.patch.yml` 插入组合包的行会让插件运行,而组合包本身仍未选中;因此以 `enabled` 为准的可见性规则会把用户刚刚启用的组合包隐藏起来,使写入文档的启用路径失效。 + +因此管理器维护一个 `HIDDEN_UNTIL_RUNNING` 组合包名集合——当前只有 `@deepseek-ai/dsh-experimental-liveshare`——集合中的组合包只有在它 patch 声明的行至少有一条在运行时才会被列出(`pkg.rows.some(row => row.enabled)`)。过滤发生在两个列表派生之前,因此在满足条件前,它不会出现在主"实验性插件"组、**更多**页、**内置插件配置**和"已安装插件"组中。该集合是 `PluginManagerPage.tsx` 中的客户端数据;Host 的 `listBundles` 契约、profile 格式和已安装包集合都保持不变。`BundleInfo.rows` 已把每条声明的行与当前承载它的 Loader 条目关联起来,客户端的 `PackageView` 携带该条目是否在运行,因此行状态就是两条启用路径的有效答案:删除这些行、把它们置为 disabled,或取消选中组合包,都会让它们停止运行,卡片随之从两个列表消失。 + +卡片隐藏期间组合包 ID 仍可按 ID 打开:`openPkg` 会从未过滤的 Host 组合包列表中解析隐藏的组合包,因此 `pluginNavigation.openBundle('@deepseek-ai/dsh-experimental-liveshare')` 仍能渲染其详情。其它按 ID 链接组合包的界面因此继续可用,组合包页也像其它未选中组合包的页面一样列出各行及其关闭状态。 + +### 分享工作如何限制在授权范围内 + +Owner 而不是查看页或 Liveshare 服务端授权每次会话操作。子会话必须先安装真实作用域工具限制,才能接受消息,冷恢复后也一样:宿主机从基础 `agent/created` 事件安装该限制。该监听器运行时智能体已登记进注册表,但创建尚未完成,AgentLoop 会扣住其排队输入,且监听器抛错会使创建失败,因此被拒绝的限制仍能阻止任何消息。宿主机服务缺失或安装失败都会拒绝子会话;配置开关、工具名或稍后安装的承诺都不是执行限制。 + +共享源会话仍可服务所有者自己的工作。每次读者调用都以「该分享与该读者」专属的 Peer 经所有者自己的 Connection 与 Gateway 分派,该 Peer 的 `share-viewer` principal(分享 ID 与服务端断言的身份三元组)由所有者进程内代码在创建 Peer 时确定,任何线路字段都无法改变它。Gateway 经 remote access 判定该 Peer 的调用以及转发给它的会话通知:本包注册 `share-viewer` 策略,把受服务的读者绑定到共享会话、其自己的 fork 及包含它们的工作区上的读取角色,以及条件按分享模式判定的 fork 与写入角色;除此之外不做任何绑定,因此其它端点和通知一律被拒绝,分享不再服务该读者后也不再有任何绑定。所有者在每次授权、撤权、模式变化与过期时使读者的访问失效,并在授权结束、分享停止或过期时释放读者的 Peer。读者的提示词以 `user-rpc` 消息进入会话,其 `source.principal` 指明该读者;所有者的消息不带 principal。只要该会话还有分享活跃,所有者 Host 就在会话自身作用域内挂载读取与检索面与无副作用工具的受限替代,以及(某个分享允许写入时)有界 `edit` 与 `write`,查看者发起的轮次只能运行这些。所有者自己发起的轮次保留会话原生工具,因此分享不会收窄所有者自己的工作。pre-step 只在内存中记录当前轮次属于哪一类。每次操作都会重新读取分享当前的 Relay 授权,并按授予根目录检查解析后的目标;pre-step 在模型步骤前按 Relay 准入重新检查每条进入消息的查看者,因此撤权前排队的读者输入永远不会驱动模型。读者对共享会话的停止与提示词一样经 Gateway 分派:策略在分享档位确立且会话受沙箱限制时放行,随后由所有者自己的会话控制器停止该会话当前运行的轮次,包括所有者发起的轮次。通用的 Peer、principal 与 remote access 设计见 [Peer principal 与 remote access](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md)。 + +受限文件工具解析其自身参数声明的每个路径,检查获授根目录包含解析结果,再把规范路径交给部署的工具;无路径的搜索使用会话工作目录,由替代定义按命名路径一样检查。受限编辑或写入执行同样的包含检查,然后按会话当前沙箱策略和正常观察策略经文件系统后端编辑或写入。 + +### 重试时保留同一个分支 + +一次 fork 请求绑定查看者、分享、源会话、操作身份和已完成前缀。Owner 在创建子会话前持久化预留该操作,并记录 owner 的 fork 应答返回的子会话;重试已记录的操作会重放该应答,而不是再次 fork。同一身份指定不同前缀会被拒绝。owner 的 `SessionForkRequest` 自己生成子会话 id,因此 host 不发送子会话身份:若 host 在子会话已创建、但操作尚未记录时退出,重试无法指名该子会话,会再创建一个。要关闭这一窗口,需要 Session Controller 的请求接受调用方提供的子会话身份。继承前缀停在无关排队输入之前;只保留紧随其后的请求元数据。被撤销的子会话保留日志,但不能以更宽权限恢复;移除分享不会暴露源会话不受限制的工具。 + +### owner 是谁 + +只有已验证的 owner 才能创建分享。对话框显示 owner 的 DSH 回读到的身份——名字、有效期、何时续期——没有显示名时用身份派生出来的标签。创建分享的代码本身也会拒绝未验证的 owner,不是只看按钮能不能点。 + +登录只做一次,且发生在 owner 点击时页面预留的窗口里。DSH 生成一个短寿命事务,绑定 Liveshare 服务端地址、Liveshare 服务端实例,以及允许完成它的页面;页面打开 DSH 指定的登录页,只接受来自该 origin、该窗口、该事务的一次性 code,并把它交回自己的 DSH。 + +Owner 使用自己保留的校验值兑换代码,以仅所有者可读写的权限保存凭据,并只向签发它的 Liveshare 服务端地址发送。首次登记需要已验证凭据;已有分享重连使用分享管理 token,显式身份认领保留该分享的 ACL、模式、过期时间和 token。分享指定的同事是 owner 的存储选择加上部署自身的允许列表;不会从显示身份派生任何内容。 + +### 读者页面顶部显示什么 + +被服务的页面复用侧栏原有的品牌行,而不是在应用上再压一条横条:原本的 Harness 标记,加上一个 LiveShare 入口,写明是谁分享的、对方跑的是哪个 DSH 版本(Liveshare 服务端自己的 release 另行标注)。点击它回到"我能看的分享"列表页。 + +### 连接断开时 + +读者的数据流可能失败:流消失、Liveshare 服务端取消请求、读取超时。页面只重试定义上可重复的失败;遇到认证、权限与分享不存在的错误就直接停下并显示原因,不会无休止地重连。 + +### 服务出去的页面怎么测 + +读者打开的页面就是 owner 自己的 Web 页面。Liveshare 服务端向 owner 要渲染好的 index 文档,在最前面插入一段很短的 carrier 脚本,页面的 shell 资源和插件 bundle 也从 owner 透传过来,所以页面跑的是 owner 版本的全部插件。carrier 只把页面的调用和流转成 Liveshare 服务端通道上的读请求;由 Liveshare 插件自己识别 viewer 页,挂上 viewer 界面,而不是分享按钮。owner 渲染这份 index 时用读者自己的访问快照取代 owner 基线,页面随后跟随读者的 `remoteAccess/follow` 快照;其他读请求都不夹带读者的授权。 + +读者页面在进程内测试:测试用 carrier、shell 和包括 Liveshare 在内的 client 插件拼出页面,顶替 Liveshare 服务端的读通道,并用脚本化的 owner 回答页面的读请求,因此断言看到的是真实 shell、carrier 与呈现插件。 + +### 本包对外提供什么 + +本包交付一个 Host 程序和一个浏览器端。公开入口包括 Host 导出(`lib/index.js`)、`./client`(`lib/client.js`,closure-factory bundle)、`./viewer-declarations`、分享管理 Remote 的 `./typert` 与 `./remote` 两个面、`./types`、组合包 patch `./cordis.patch.yml`,以及 locale JSON 文件。manifest 以 `dsh.client` 声明浏览器端,`platform` 为 `web`,并列出它需要的客户端服务;该入口读取 `__DSH_LIVESHARE_VIEWER__`:在被服务的查看页上挂载查看页 session 插件及其品牌、模型、权限、会话行与分享路径界面,否则先挂载 owner 生成的 Remote,再挂载 Session 头部操作。 + +分享管理是一个 `liveshareShares` Typert Remote,共十二个方法——`create`、`list`、`stop`、`updatePermissions`、`setCapabilities`、`capabilities`、`relay`、`setRelay`、`identity`、`audit`、`testRelay` 与 `testPublish`——背后是 owner 宿主执行的操作。仅存的同源 HTTP 路由是四条 owner 登录与凭据路由:`/api/liveshare.ownerLoginStart`、`/api/liveshare.ownerLoginRedeem`、`/api/liveshare.ownerLoginCancel` 与 `/api/liveshare.ownerCredential`。 + +出站网关是受监督的子进程,源码入口为 `src/owner-gateway/cli.ts`,发布入口为 `lib/owner-gateway/cli.js`;目录名让它与 DSH API gateway 明确区分。 + +## Alternatives considered + +- **单独做一个只读页面,不服务正常客户端。** 否决:阅读需要浏览、分组与选择,另做一个页面就得把这些全部重建,还会悄悄丢掉产品要保留的导航。 +- **每个按钮各自判断能不能点。** 否决:每个入口都读取 owner 计算的同一份访问快照,后加的界面才不会悄悄重新引入读者无权执行的动作。 +- **让读者页面自己决定权限。** 否决:分享授予什么,只由 owner 与服务端决定;页面只陈述被告知的结果。 +- **把没有模式字段的旧分享当作可写。** 否决:那会把 owner 从未配置的权限交给读者,因此没有模式的分享一律只读。 +- **把登录 code 放进页面地址,或从弹窗的 cookie 读凭据。** 否决:地址会经浏览历史、referrer 和日志泄漏,页面也不应该持有凭据。 +- **页面解释不了某个拒绝时,直接展示服务端的错误原文。** 否决:那段文本可能包含文件路径、凭据或别的读者的会话,所以页面只显示它能担保的稳定原因。 +- **只在可编辑配置或沙箱模式中写执行限制。** 否决:子会话能够改变配置,而逐调用执行选择可能优先于模式。真实操作必须经过单调拒绝的守卫和已验证实现。 +- **只在分派前检查模型路径,或全局限制所有文件系统读取。** 否决:分派前检查与工具调用之间还隔着其他监听器,而全局读取规则还会限制无关 Owner 工作。包含检查在已授权会话自身的读写中执行。 +- **复用部署默认根目录,或把源会话整个尾部复制到子会话。** 否决:两者都可能包含查看者未获授的文件或待处理输入。 +- **以部署共享密钥或自报显示字段作为所有者身份。** 否决:两者都不能识别批准本次发布的已登录人员。 +- **保留三档。** 否决:`view` 与 `fork-readonly` 只差在读者能否分支并驱动子会话,而这已经是独立的 fork 开关。第三档重复了该开关,还会让两处答案互相矛盾。 +- **保留分享级的附件上传开关。** 否决:附件是读者已经能看到的会话上的一条 prompt 的一部分。单独的开关重复了可见性,且只能从可见性里做减法,而上传角色本来就随读角色授予。 +- **按轮次归属请求者并让所有者轮次豁免。** 上线后否决,之后缩小范围:持久化轮次请求者需要仅 Host 可见的消息来源投影、每次执行读取 turn boundary、重新检查接纳该轮次的分享,而且在混合或无法确立请求者时仍必须默认拒绝。现在的实现只为运行中轮次保留内存标记——`agent/pre-step` 读取进入消息的 principal,守卫读取该结论——因此所有者发起的轮次保留会话原生工具,且不新增会话事件。接受的代价是:所有者在查看者轮次中追加的输入仍留在受限轮次内;查看者消息若进入后续步骤,会立即收窄该轮次。 +- **让每个工具自行声明档位或沙箱。** 否决:档位属于分享。逐调用选择会让某次执行凌驾于批准它的授权之上。 +- **严格的 manifest 选中判断。** 否决:直接判断 `pkg.enabled` 代码最少,但默认安装启用 LiveShare 的唯一文档路径是编辑 `cordis.patch.yml`,那不会让组合包被选中。卡片会在插件恰好开始运行时仍然隐藏,与产品规则矛盾。 +- **在 Host 派生有效启用状态。** 否决:`listBundles` 可以以新字段或改写字段报告有效状态,但这会为一条纯客户端的展示规则改动跨包契约,并诱使每个组合包消费方重新解释 `enabled`——它必须继续表示"profile 已选中"。 +- **把 LiveShare 移出安装随附的可选组合包。** 否决:将它从可选集合移除即可从构造上让它处处不可见,但该包仍需随版本作为 npm 包发布并可安装,而且这会把组合包从 Host 列表中完全移除,详情入口也会随之消失。 +- **保留选人器,或换成由目录支撑的选择器。** 推迟:点名同事需要身份目录,而 Liveshare 服务端目前不提供;弹窗无法给出它能验证的选择。已存储的 `selected` 与 `approval` 记录仍按 ACL 工作,在有目录契约之前新建入口保持退役;`liveshareShares.create` 的字段保留给这些记录与直接调用方,所以这是推迟 API,而不是删除它。 + +## Verification + +- 读者页面保留正常导航,且在共享会话与任何分支上都不会出现读者无权执行的动作。 +- 没有 Liveshare bundle 的 DSH 以及 owner 自己的页面,行为与以前完全一致。 +- 分享使用两档之一及该档的沙箱;未知模式、读不懂的应答、没有模式的分享仍可读取,但绝不会被驱动。 +- 没有已验证的 owner 身份,就无法创建分享。 +- 弹窗创建的分享携带 `open`;已存储的 `selected` 或 `approval` 记录保留记录中的 ACL 与访问摘要,`create` 仍会存储调用方请求的 `selected` 模式及其主体。 +- 登录只接受来自该事务 origin、窗口与 state 的一次性 code,别的都不接受;失败或放弃都会丢弃事务,凭据、cookie 与 code 都不进入 URL、存储或日志。 +- 真实 Controller、ToolRuntime、文件系统和 profile 测试覆盖 fork 身份、冷恢复限制、有界执行、撤权和正常取消。已构建查看页必须使用真实 POST 载体和 Owner 授权处理器;确定性显示 fixture(测试前置数据)不替代执行或持久化测试。 +- 生成的 API 文档和两种 SDK 投影与 `user-rpc` 来源 principal 保持一致;该字段只由 `agent/pre-step` 准入校验消费。 +- `ViewerSessionMode` 只有两个成员;持久化的 `fork-readonly` 记录在 owner 状态加载时被拒绝。 +- `view` 分享驱动源会话,拒绝文件写入与越界读取,并在设置 `allowsFork` 时可以 fork。 +- 对读者可见的每条会话,无论档位与 fork 开关如何组合,`fileUploads/upload` 都被允许。 +- 源会话的工具决策只读取会话上挂载的执行层:守卫放行本次安装的读取与检索面与无副作用工具的受限替代,可写会话下另有有界 `edit` 与 `write`,拒绝其它所有工具名,不读取轮次、请求者或来源。 +- 单元测试与真实运行时测试覆盖档位策略、受限读写、fork 上限、执行层的安装与释放,以及基于持久消息来源的 pre-step 准入校验。 +- 插件管理器的组件测试在声明行全部关闭时把该组合包排除在两个列表之外,在声明行有一条运行后列出它,并在它隐藏时仍能按组合包 ID 打开;录制的 golden 与刷新卡片数随该规则变化。 + +## Consequences + +读者页面只能准确到服务端告诉它的程度。能力回答、owner 身份状态、会话模式、owner 的 DSH 版本、登录端点,都由 owner 的 DSH 与 Liveshare 服务端提供;在与本层一起部署之前,页面保持最保守的状态——只读,且登录无法完成——而不是去猜。 + +页面不是安全边界。无论页面做什么,owner 的 DSH 与 Liveshare 服务端都会拒绝读者的写入;这份访问快照的作用只是让页面不去提供注定被拒绝的按钮。 + +已有 ACL 保持不变。无模式记录仍可读取,但不会被驱动;互相矛盾的模式字段被拒绝。没有归属的分享显示为“未验证历史”。 + +Owner 重连状态以原子替换写入,并在单进程内排队,但多个进程共享一个文件仍可能丢失更新或争用网关。跨进程执行归属尚未解决;原子替换本身不是事务或路由认领。包含关系按路径检查:若本地进程在检查与打开之间把获授范围内的目录换成符号链接,可以让查看者的读取改指别处,或让写入落到沙箱可写根内的其他位置。基于目录句柄锚定的操作可以消除这一窗口,留待单独变更引入。发布前仍需完成查看页产物和 SDK 快照验证。 + +`view` 分享不是"只看":它会在只读沙箱里运行读者的 prompt 与工具调用。想要被动观看的部署没有单独模式,只能依赖读者不发送 prompt,而不能依赖档位名称。fork 是唯一可独立关闭的动作;附件上传无法单独关闭。 + +沙箱同样作用于所有者自己的轮次。只要某会话还有分享活跃,所有者也只能在授予工作区内读取,且只有在某个分享允许写入时才能写入;因此一条仅仅发布、并没有人在用的分享也会改变所有者自己的工具面,直到它停止或过期。取消同样没有归属:读者的一次停止会结束共享会话正在运行的任何轮次。 + +默认卡片列表、**更多**页及其录制 golden 各少一张卡片,管理器的刷新卡片数也减一。只认识插件页的人无法从该页发现 LiveShare 或直接开启它;启用路径由包 README 负责。通过 patch 路径启用的组合包,其卡片显示为关闭状态而行在运行,因为开关反映并写入 profile 的组合包选中状态;文档会说明两条路径及其差异。该规则覆盖一个显式命名的组合包而不是 manifest 标志,因此新增另一个"运行前隐藏"的组合包或退役这一个,都只是一行集合改动,并配一条测试和文档更新。 + +随版本发布的创建流程只产生组织链接分享:分享弹窗不发送 `accessMode` 与 `allowedSubjectIds`,因此新分享一律为 `open`,部署级 `LIVESHARE_ALLOWED_SUBJECTS` 是新分享唯一能获得的主体级收窄。已存储的 `selected` 与 `approval` 记录继续工作并保留访问摘要;`liveshareShares.create` 的字段仍留在 Remote 上供这些记录与直接调用方使用,而界面不暴露它们。 diff --git a/.agents/notes/implemented/feature/2026-10-09-forge-panel.i18n.yaml b/.agents/notes/implemented/feature/2026-10-09-forge-panel.i18n.yaml index 5a92144328..6f3f674ec6 100644 --- a/.agents/notes/implemented/feature/2026-10-09-forge-panel.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-10-09-forge-panel.i18n.yaml @@ -9,14 +9,14 @@ en: 344bcc7e9825bbc8 zh: c1d0f4b54f5b3786 /agent-note-change-request-panel-and-worktree-sessions/decision: - en: 494b10799ca74ab7 - zh: fd55bf793ee6c643 + en: 0b159e4e33409515 + zh: 2bf1eafd644f2d26 /agent-note-change-request-panel-and-worktree-sessions/deferred: en: 32ef8af85f4d2f66 zh: b82b67f94346399e /agent-note-change-request-panel-and-worktree-sessions/alternatives-considered: - en: aa2ae6ca5d297127 - zh: b7eba2e7deffe245 + en: e471e16b6b6450ca + zh: 5fbc9242edc8d112 /agent-note-change-request-panel-and-worktree-sessions/consequences: en: 97976f073b9f65e0 zh: 95be3f7f375b0876 diff --git a/.agents/notes/implemented/feature/2026-10-09-forge-panel.md b/.agents/notes/implemented/feature/2026-10-09-forge-panel.md index c581b0626a..7792f8caf9 100644 --- a/.agents/notes/implemented/feature/2026-10-09-forge-panel.md +++ b/.agents/notes/implemented/feature/2026-10-09-forge-panel.md @@ -10,7 +10,7 @@ English | [中文](2026-10-09-forge-panel.zh.md) ## Decision -[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.md) is a two-face package. Its Host half, the `forgePanel` Remote, assembles everything the panel shows for one Session into one streamed state; the forge registry and the binding service keep only their domain operations. Its Client half is a right-sidebar tab, a Session header button that shows or hides it, a New Session option, a creation card above the composer, and a toast. Every surface calls a bound item a change request, the forge registry's term, whatever its code host calls it. The [forge bundle](../../../../packages/experimental/forge-bundle/README.md) ships the panel, the forge registry, the GitHub provider, the bindings, and an isolated worktree service as one default-disabled Plugins page entry. +[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.md) is a two-face package. Its Host half, the `forgePanel` Remote, assembles everything the panel shows for one Session into one streamed state; the forge registry and the binding service keep only their domain operations. Its Client half is a right-sidebar tab, a Session header button that shows or hides it, a New Session option, a creation card above the composer, a toast, and the [Session list icons and hover card section](2026-10-20-forge-session-list.md). Every surface calls a bound item a change request, the forge registry's term, whatever its code host calls it. The [forge bundle](../../../../packages/experimental/forge-bundle/README.md) ships the panel, the forge registry, the GitHub provider, the bindings, and an isolated worktree service as one default-disabled Plugins page entry. The Remote inspects the checkout through the Session's subprocess provider under the Session's read-only sandbox policy, so it reads what the Session's commands see and cannot write. Inspection reads the top-level, Git, and common Git directories, the branch or the abbreviated HEAD commit, the upstream, the remotes, and the size of the branch's changes against its fork point. It runs when a Session is followed, after each `working-directory/change`, `tool/result`, and `sandbox/mode` event, and on refresh, one at a time with one trailing rerun. @@ -18,7 +18,7 @@ Status polling follows panel subscriptions: a Session's bindings are watched onl An expanded card marks a draft ready for review and merges a change request the host would merge now. The forge seam carries both as provider actions that return a typed failure, and every snapshot says whether the host would merge the change request now for the signed-in user and which methods the repository allows, so the panel offers only actions that can succeed. A merge carries the head commit the card showed, and the host refuses it once new commits arrived. The panel acts only on change requests bound to the Session. -A new Session starts in a worktree through a request, not an immediate creation. Checking the option in the New Session row, a `conversation.hero.options` entry, appends a `plugin:client-ui-forge/worktree` record with the base branch; changing the base or clearing the option appends another, and nothing touches the repository until the user sends. The option is offered only where creation can succeed: under `workspace-write`, the checkout and its common Git directory must lie inside the writable root, which excludes a project that is itself a linked worktree. The worktree must exist before the first message's turn starts, so the turn runs there and its first request already names the new directory. When the message enters the Session's inbox, the panel's `agent/inbox/inserted` listener starts maintenance of the Agent before the delivery wakes it; the Agent latches the wake until the maintenance ends, and the panel creates and enters the worktree inside it. The creation's stage streams to a card above the composer, where Cancel stops the attempt. After a stopped or failed attempt the message keeps waiting in the inbox, and the card keeps the failure and offers another attempt with a new branch name or Continue without a worktree, which withdraws the request and lets the message go on. An `agent/pre-step` listener still creates the worktree at the first step proposal of a Session whose request is open, because a message that enters while the Agent runs a turn or other maintenance gets no hold; it moves the working-directory notice into the proposed messages, and a failure ends the turn. The `forgeWorktree` projection ignores requests after the first `turn/start` and creations after the first `step/start`, so a running Session never moves. The tab's first row names the branch the Session works on. +A new Session starts in a worktree through a request, not an immediate creation. Checking the option in the New Session row, a `conversation.hero.options` entry, appends a `plugin:client-ui-forge/worktree` record with the base branch; changing the base or clearing the option appends another, and nothing touches the repository until the user sends. The option is offered only where creation can succeed: under `workspace-write`, the checkout and its common Git directory must lie inside the writable root, which excludes a project that is itself a linked worktree. The worktree must exist before the first message's turn starts, so the turn runs there and its first request already names the new directory. When the message enters the Session's inbox, the panel's `agent/inbox/inserted` listener starts maintenance of the Agent before the delivery wakes it; the Agent latches the wake until the maintenance ends, and the panel creates and enters the worktree inside it. The creation's stage streams to a card above the composer, where Cancel stops the attempt. After a stopped or failed attempt the message keeps waiting in the inbox, and the card keeps the failure and offers another attempt with a new branch name or Continue without a worktree, which withdraws the request and lets the message go on. An `agent/pre-step` listener still creates the worktree at the first step proposal of a Session whose request is open, because a message that enters while the Agent runs a turn or other maintenance gets no hold; it moves the working-directory notice into the proposed messages, and a failure ends the turn. The `forgeWorktree` projection ignores requests after the first `turn/start` and creations after the first `step/start`, so a running Session never moves. The tab's first row names the branch the Session works on, and the Session list marks a Session that works in its created worktree. The worktree a Session works in can be removed outside it. Each inspection checks whether the created worktree's directory still exists, and every message that enters while the Agent is idle starts a hold that checks again, so the message waits without starting its turn while the directory is missing. The check must come before the turn: while it assembles a step's context, the working-directory service moves a Session whose directory is gone back to its project, before any pre-step listener runs. Restore checks the branch out again at the recorded directory, or creates the branch again at its base commit when it is gone; Continue in the project moves the Session to the source checkout. Either runs inside the hold and lets the waiting message go on. @@ -26,6 +26,8 @@ The panel does not move a Session between existing worktrees. Removing a change The panel also shows the changes behind a Session's work in a changes view, a resource tab type of this package. The Remote parses every diff on the Host into files and hunks, so the browser draws them without a patch parser, and bounds each read with `maxDiffBytes` and `maxUntrackedFiles`. Local changes come from confined `git diff` in the Session's execution world, in a scope the user picks: the branch against its fork point by default, or the uncommitted, unstaged, or staged changes; untracked files are read through the Session's filesystem and shown as new files. A change request's changes come from its code host at the head of the binding's latest status, through two provider reads the forge seam gained, `diff` and `fileText`. The Host keeps the patch it read last for each change request, keyed by the head commit and base branch and bounded by `maxCachedDiffBytes`, so reopening, refreshing, and navigating at the same head download nothing again; a host or provider that refuses an answer over its size limit reports the forge failure `too-large`, and the view then offers the change request's web page instead of a retry. Unchanged lines between hunks expand from the file's new side, which is the working tree, the index, or the head commit, and only while that text still matches the hunks. Each inspection also sizes the branch's changes for a local changes card, shown while no open change request carries the checked-out branch; its Create change request sends the Session a user message, so the Agent creates the change request with its own tools and the binding follows from `gh pr create` as for any other. +Review comments capture the file, side, range, complete hunk, and reviewed head when the user selects code. Agent references enter the existing composer quote mechanism, so the user controls batching and the sent message records exactly what the model receives. GitHub writes belong to the optional forge provider operation: preflight checks the current head, the write addresses that commit, and an ambiguous outcome blocks another write until the user reconciles it. Review drafts and inline records belong to a browser tab occurrence, not to the Session log or a GitHub pending review. Switching Sessions retains that occurrence; closing it discards the local state and rejects late write completions. Published GitHub comments remain external records. + ## Deferred A left-navigation review page next to Plugins and Scheduled tasks, listing the user's pull requests or merge requests on the selected provider, is deferred. It attaches without changing this package's Session surfaces: @@ -67,7 +69,7 @@ The changes view offers no scope for one commit or for the last turn yet; both w **The creation card in the transcript.** Transcript nodes accept no plugin kinds; the input dock above the composer is the nearest list slot, and it renders on the New Session screen. -**A Session-row hover section and manual URL tracking.** Both duplicated the tab. Bindings come from `gh pr create`, the `gh pr` commands that act on one change request, pushes, and the branch the checkout tracks, and `track` remains for Undo. +**Manual URL tracking.** It duplicated the bindings the Session's own commands record. Bindings come from `gh pr create`, the `gh pr` commands that act on one change request, pushes, and the branch the checkout tracks, and `track` remains for Undo. **Parsing patches in the browser.** The Remote would carry unbounded patch text, and the browser would need a patch parser; the Host bounds the read and parses it once. @@ -79,6 +81,12 @@ The changes view offers no scope for one commit or for the last turn yet; both w **Creating the change request from the card.** The panel would need to push the branch and compose the title and description; a message lets the Agent do both with its tools and the user's instructions, and keeps one way to bind change requests. +**Sending each line comment immediately to the Agent.** It starts a turn before the user can collect related comments or add a shared instruction; composer references preserve that choice. + +**Keeping review drafts after their tab closes.** Reopening the same PR would restore abandoned review state. A tab occurrence gives closing a clear discard meaning while preserving Session switching. + +**Automatically retrying a review write.** A lost response does not mean that GitHub rejected the request. Retaining the outgoing request and requiring reconciliation prevents a transport failure from silently duplicating comments. + ## Consequences The panel shows current status only while it is open; checks that finish while nothing is mounted produce no notification. Changes made outside the Session appear after Refresh or the next tool result. Every inspection spawns up to eleven short Git processes in the Session's execution world with one remote, and two more for each further remote it tries for a fork point. A worktree exists only for a Session that requested it before its first step, and a failed or cancelled attempt can leave its branch or checkout behind. A message that enters while the Agent runs a turn or other maintenance gets no hold, so its worktree is created within the first turn and a removed worktree is not checked. While a message waits, the Agent stays idle in maintenance: the composer offers no stop, and callers waiting for the Agent to become idle wait as well. Restoring loses changes that were never committed. A base branch that requires a merge queue cannot merge from the panel. With Git Worktrees also enabled, two independent worktree services exist; the model's tool never sees the panel's service. A local or change request diff over `maxDiffBytes`, or one the host refuses, shows no files. Expanding unchanged lines of a change request reads each file from the code host once. diff --git a/.agents/notes/implemented/feature/2026-10-09-forge-panel.zh.md b/.agents/notes/implemented/feature/2026-10-09-forge-panel.zh.md index 4c03bc7763..ea7847b96e 100644 --- a/.agents/notes/implemented/feature/2026-10-09-forge-panel.zh.md +++ b/.agents/notes/implemented/feature/2026-10-09-forge-panel.zh.md @@ -10,7 +10,7 @@ Status: implemented ## Decision -[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.zh.md) 是一个双端包。宿主端的 `forgePanel` Remote 把面板为一个 Session 展示的全部内容组装成一条流式状态;forge 注册表与绑定服务只保留各自的领域操作。客户端包括一个右侧边栏标签页、一个显示或隐藏它的 Session 标题栏按钮、一个新会话选项、一张位于输入框上方的创建卡片,以及一个 toast。所有界面都把已绑定的条目称为变更请求(forge 注册表的术语),不随代码托管平台的叫法变化。[forge 套件](../../../../packages/experimental/forge-bundle/README.zh.md)把面板、forge 注册表、GitHub 提供方、绑定服务与一个隔离的 worktree 服务作为插件页中默认禁用的一个条目发布。 +[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.zh.md) 是一个双端包。宿主端的 `forgePanel` Remote 把面板为一个 Session 展示的全部内容组装成一条流式状态;forge 注册表与绑定服务只保留各自的领域操作。客户端包括一个右侧边栏标签页、一个显示或隐藏它的 Session 标题栏按钮、一个新会话选项、一张位于输入框上方的创建卡片、一个 toast,以及[会话列表中的图标与悬浮卡片部分](2026-10-20-forge-session-list.zh.md)。所有界面都把已绑定的条目称为变更请求(forge 注册表的术语),不随代码托管平台的叫法变化。[forge 套件](../../../../packages/experimental/forge-bundle/README.zh.md)把面板、forge 注册表、GitHub 提供方、绑定服务与一个隔离的 worktree 服务作为插件页中默认禁用的一个条目发布。 Remote 通过 Session 的子进程提供方、在 Session 的只读沙箱策略下检查检出,因此它读到的与 Session 命令看到的相同,且无法写入。检查读取顶层目录、Git 目录与公共 Git 目录、分支或缩写的 HEAD 提交、上游与远端,以及分支相对于其分叉点的变更规模。它在 Session 被关注时、每个 `working-directory/change`、`tool/result` 与 `sandbox/mode` 事件之后以及刷新时运行,一次只运行一个,最多再追加一次。 @@ -18,7 +18,7 @@ Remote 通过 Session 的子进程提供方、在 Session 的只读沙箱策略 展开的卡片可以把草稿标记为可评审,也可以合并主机现在就会合并的变更请求。forge seam 把两者作为提供方操作承载,失败时返回带类型的结果;每个快照都说明主机现在是否会为已登录用户合并该变更请求、仓库允许哪些合并方式,因此面板只提供能够成功的操作。合并带上卡片显示的 head 提交,有新提交到达后主机会拒绝。面板只操作绑定到该 Session 的变更请求。 -新 Session 通过请求而不是立即创建来以 worktree 开始。勾选新会话那一行中的选项(一个 `conversation.hero.options` 条目)会追加一条带基准分支的 `plugin:client-ui-forge/worktree` 记录;更换基准或取消勾选会再追加一条;用户发送之前不会改动仓库。只有创建能够成功时才提供该选项:在 `workspace-write` 下,检出及其公共 Git 目录必须位于可写根之内,这排除了本身就是链接 worktree 的项目。worktree 必须在第一条消息的这一轮开始之前就存在,这样这一轮才会在其中运行,第一次请求也已经写明新目录。消息进入 Session 的收件箱时,面板的 `agent/inbox/inserted` 监听器在这次投递唤醒 Agent 之前启动 Agent 的维护任务;Agent 会暂存唤醒直到维护任务结束,面板在维护任务内创建并进入 worktree。创建的阶段以流的形式推送给输入框上方的卡片,卡片上的“取消”停止这次尝试。尝试被停止或失败后,消息继续在收件箱中等待,卡片保留失败信息,并提供用新分支名再试一次,或“不使用工作树”:撤回请求并让消息继续。一个 `agent/pre-step` 监听器仍会在请求未完成的 Session 的第一个步骤提议中创建 worktree,因为 Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留;它把工作目录通知移入本步提议的消息,失败时这一轮结束。`forgeWorktree` 投影忽略第一个 `turn/start` 之后的请求以及第一个 `step/start` 之后的创建,因此运行中的 Session 永远不会移动。标签页顶部一行写出 Session 所在的分支。 +新 Session 通过请求而不是立即创建来以 worktree 开始。勾选新会话那一行中的选项(一个 `conversation.hero.options` 条目)会追加一条带基准分支的 `plugin:client-ui-forge/worktree` 记录;更换基准或取消勾选会再追加一条;用户发送之前不会改动仓库。只有创建能够成功时才提供该选项:在 `workspace-write` 下,检出及其公共 Git 目录必须位于可写根之内,这排除了本身就是链接 worktree 的项目。worktree 必须在第一条消息的这一轮开始之前就存在,这样这一轮才会在其中运行,第一次请求也已经写明新目录。消息进入 Session 的收件箱时,面板的 `agent/inbox/inserted` 监听器在这次投递唤醒 Agent 之前启动 Agent 的维护任务;Agent 会暂存唤醒直到维护任务结束,面板在维护任务内创建并进入 worktree。创建的阶段以流的形式推送给输入框上方的卡片,卡片上的“取消”停止这次尝试。尝试被停止或失败后,消息继续在收件箱中等待,卡片保留失败信息,并提供用新分支名再试一次,或“不使用工作树”:撤回请求并让消息继续。一个 `agent/pre-step` 监听器仍会在请求未完成的 Session 的第一个步骤提议中创建 worktree,因为 Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留;它把工作目录通知移入本步提议的消息,失败时这一轮结束。`forgeWorktree` 投影忽略第一个 `turn/start` 之后的请求以及第一个 `step/start` 之后的创建,因此运行中的 Session 永远不会移动。标签页顶部一行写出 Session 所在的分支,会话列表标出在所创建 worktree 中工作的 Session。 Session 所在的 worktree 可能在 Session 之外被删除。每次检查都会确认所创建 worktree 的目录是否还在;Agent 空闲时进入的每条消息都会启动一次暂留并再确认一次,目录缺失期间消息等待、不开始这一轮。这次确认必须发生在这一轮之前:组装一个步骤的上下文时,工作目录服务会把目录已不存在的 Session 移回其项目,而这发生在任何 pre-step 监听器运行之前。“恢复”在记录的目录重新检出分支;分支也已删除时,在其基准提交重新创建分支。“在项目目录继续”把 Session 移回源检出。两者都在暂留内运行,之后放行等待中的消息。 @@ -26,6 +26,8 @@ Session 所在的 worktree 可能在 Session 之外被删除。每次检查都 面板还通过变更视图展示 Session 工作背后的变更;变更视图是本包的一个资源标签页类型。Remote 在宿主上把每个 diff 解析成文件与 hunk,因此浏览器无需补丁解析器即可绘制,每次读取都受 `maxDiffBytes` 与 `maxUntrackedFiles` 约束。本地变更来自 Session 执行环境中受限的 `git diff`,范围由用户选择:默认是分支相对于分叉点的变更,也可以是未提交、未暂存或已暂存的变更;未跟踪文件经 Session 的文件系统读取,显示为新文件。变更请求的变更来自代码托管平台,取自绑定最新状态中的 head,经 forge seam 新增的两个提供方读取操作 `diff` 与 `fileText` 获得。Host 为每个变更请求保留最近读到的补丁,按 head 提交与目标分支区分,总量受 `maxCachedDiffBytes` 约束,因此在同一 head 下重新打开、刷新与导航都不会再次下载;托管平台或提供方因结果超过其大小上限而拒绝时报告 forge 失败 `too-large`,视图随之提供变更请求的网页而不是重试。hunk 之间未变更的行从文件的新版本展开,即工作树、暂存区或 head 提交,并且只在该文本仍与 hunk 吻合时展开。每次检查还会为变更卡片统计分支的变更规模;没有打开中的变更请求以当前检出分支为源分支时才显示该卡片。卡片上的“创建变更请求”向 Session 发送一条用户消息,由 Agent 用自己的工具创建变更请求,绑定像其他变更请求一样来自 `gh pr create`。 +评审评论在用户选择代码时固定文件、侧、行范围、完整 hunk 与所评审的 head。Agent 引用进入既有输入框引用机制,由用户决定批量发送,发送后的消息记录模型实际收到的内容。GitHub 写入属于 forge 提供方的可选操作:预检核对当前 head,写入明确指向该提交;结果不明时阻止再次写入,直到用户核对。评审草稿与行内记录归浏览器页签实例所有,不进入 Session 日志,也不创建 GitHub 待提交评审。切换 Session 保留该实例;关闭时丢弃本地状态并拒绝迟到的写入结果。已发布的 GitHub 评论仍保留在外部平台。 + ## Deferred 在左侧导航“插件”“自动化任务”旁边新增代码审查页、列出用户在所选提供方上的 pull request 或 merge request,这项工作推迟。它的接入不需要改动本包面向 Session 的界面: @@ -67,7 +69,7 @@ Session 所在的 worktree 可能在 Session 之外被删除。每次检查都 **把创建卡片放进对话记录。** 对话记录的节点不接受插件定义的类型;输入框上方的停靠区是最近的列表型 slot,而且它在新会话页也会渲染。 -**Session 行悬停分区与手动粘贴 URL 跟踪。** 二者都与标签页重复。绑定来自 `gh pr create`、操作单个变更请求的 `gh pr` 命令、推送以及检出所跟踪的分支,`track` 保留给“撤销”使用。 +**手动粘贴 URL 跟踪。** 它与 Session 自己的命令记录下的绑定重复。绑定来自 `gh pr create`、操作单个变更请求的 `gh pr` 命令、推送以及检出所跟踪的分支,`track` 保留给“撤销”使用。 **在浏览器中解析补丁。** Remote 将传输不受限的补丁文本,浏览器也需要补丁解析器;宿主约束读取并只解析一次。 @@ -79,6 +81,12 @@ Session 所在的 worktree 可能在 Session 之外被删除。每次检查都 **直接在卡片上创建变更请求。** 面板需要推送分支并撰写标题与描述;发送消息让 Agent 用自己的工具和用户的指示完成这两件事,绑定变更请求的方式也只保留一种。 +**把每条行内评论立即发送给 Agent。** 用户还没来得及收集相关评论或补充共同指令,就会开始一轮;输入框引用保留用户的选择。 + +**关闭页签后仍保留评审草稿。** 重新打开同一个 PR 会恢复已放弃的评审状态。页签实例让关闭明确表示丢弃,同时保留切换 Session 的状态。 + +**自动重试评审写入。** 丢失响应不代表 GitHub 拒绝了请求。保留发出的请求并要求核对,可以避免传输失败悄悄产生重复评论。 + ## Consequences 面板只在打开时显示当前状态;没有任何界面挂载时检查结束不会有通知。Session 之外的改动要等到刷新或下一个工具结果才会出现。只有一个远端时,每次检查最多在 Session 的执行环境中启动十一个短暂的 Git 进程;为寻找分叉点每多尝试一个远端,就多启动两个。只有在第一步之前提出请求的 Session 才会有 worktree,失败或被取消的尝试可能留下它的分支或检出。Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留,因此它的 worktree 在第一轮之内创建,已删除的 worktree 也不会被检查。消息等待期间,Agent 以空闲状态处于维护任务中:输入框不提供停止,等待 Agent 空闲的调用方也会一并等待。恢复会丢失从未提交的修改。要求合并队列的基准分支无法从面板合并。同时启用 Git Worktrees 时会存在两个互不影响的 worktree 服务;模型的工具看不到面板的服务。本地或变更请求的 diff 超过 `maxDiffBytes`,或被托管平台拒绝时,不显示任何文件。展开变更请求中未变更的行时,每个文件只从托管平台读取一次。 diff --git a/.agents/notes/implemented/feature/2026-10-20-forge-session-list.i18n.yaml b/.agents/notes/implemented/feature/2026-10-20-forge-session-list.i18n.yaml new file mode 100644 index 0000000000..2f6f65eb10 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-20-forge-session-list.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-20-forge-session-list.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/feature/2026-10-20-forge-session-list.md +/agent-note-forge-state-in-the-session-list: + en: b5876f7873857921 + zh: 56fac53370de206d +/agent-note-forge-state-in-the-session-list/problem: + en: 07ba0c3bf73a2446 + zh: 2413783a6697133e +/agent-note-forge-state-in-the-session-list/decision: + en: e1d03cf4e1afd5aa + zh: b2386cb7c17c58dd +/agent-note-forge-state-in-the-session-list/alternatives-considered: + en: 3b08440ee2bf7809 + zh: a90e3fcb89cf3f74 +/agent-note-forge-state-in-the-session-list/consequences: + en: 25a17eac43beb705 + zh: c138f693ff07c91d diff --git a/.agents/notes/implemented/feature/2026-10-20-forge-session-list.md b/.agents/notes/implemented/feature/2026-10-20-forge-session-list.md new file mode 100644 index 0000000000..cec6faa265 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-20-forge-session-list.md @@ -0,0 +1,39 @@ +# Agent Note: Forge state in the Session list + +Status: implemented + +English | [中文](2026-10-20-forge-session-list.zh.md) + +## Problem + +The [forge panel](2026-10-09-forge-panel.md) named a Session's branch in a chip beside the Session title. The header's action row does not shrink, so a long branch overlapped other header entries such as the background task label. Finding which Sessions work in their own worktrees, and which of their change requests need attention, also meant opening each Session's tab. The Session list offers seats for row icons and a hover card section, `sidebar.session.row.trailing` and `sidebar.session.row.hover`, whose occupants must not load or bind Sessions. A row needs two kinds of fact: what the Session log records, which is the worktree the panel created and entered and the change requests the Session binds, and what only polling knows, which is each change request's state and checks. + +## Decision + +The chip beside the Session title is gone, and the tab's first row names the branch. A Session row shows, after its time, a branch glyph while the Session works in the worktree the panel created for it, and the glyph of its most urgent change request: one needing attention first, then open, draft, not yet polled, merged, and closed. An open change request's glyph keeps the row's subdued color with a dot for its checks; merged and closed ones keep their state colors. The row's hover card section names the worktree branch and what it started from, then up to three change requests with their state and checks, and how many more there are. + +Log facts reach the list as projection client views, which the Session summary carries. The `forgeWorktree` projection also records the Session's directory from its header and each `working-directory/change`, and its view is the created worktree while that directory is the worktree or lies below it, else null. The `forgeChangeRequests` view lists the bindings without status, as the [binding decision](2026-10-09-session-change-request-bindings.md) intended. The projection cache keeps both for Sessions that are not loaded. + +Polled states reach the list through the `forgePanel.changeRequestStates` stream. One Host run serves every stream and stops with the last one. It lists Sessions through `sessionQuery` without loading them, takes the visible bindings of loaded Sessions from `ctx.forgeSession` and those of other Sessions from their cached summary, follows binding changes of loaded Sessions, and subscribes to each bound change request through the shared `ctx.forge.watch`; closed requests use the idle polling cadence, and merged requests remain subscribed for explicit refresh. Every changed state is stored in the `forge_panel_change_requests` storage domain under the SHA-256 digest of its URL, so a restart shows the last known states before the next poll, and unbound stored states are dropped only when every listed Session has known bindings. A stream sends a `baseline` of the states its caller may read, then `changed` frames, filtered through Remote access. + +The browser keeps one source of states for every row and hover section: the first subscriber opens the stream, the last one closes it, and a transport reset reopens it. + +## Alternatives considered + +**Leaving change requests to the tab.** The forge panel first rejected a Session-row hover section because, for one Session, it repeated the tab. The list compares every Session at once, which no tab does, and the hover section adds only the facts a row's glyphs summarize. + +**Keeping the branch beside the Session title.** Truncating the chip would hide the branch name, and the header shows only the open Session; the list shows every Session, and the tab names the branch in full. + +**Polled states in a projection.** Projections fold the Session log, and polled states are not logged. A projection would either log every poll, adding events that no model request needs, or depend on process-local state that a replay cannot reconstruct. + +**Following every listed Session's panel.** A panel inspects its checkout with Git in the Session's execution world and probes sign-in; the list needs only bindings and change request states. + +**Loading listed Sessions to read their bindings.** Listing would replay every Session log; the cached summary answers without loading. + +**Keeping the states only in memory.** Every row would show unknown states after a restart until each change request is polled again. The stored states are derived data, so a record that fails its schema is set aside instead of failing the run. + +**A branch line for a Session that works in its project directory.** Reading that branch needs Git in the Session's execution world, which the list must not reach without loading the Session. + +## Consequences + +The list shows every Session's worktree and change request states without loading a Session, a restart shows the last known states at once, and the header no longer overlaps. While a row is shown, every open change request that a listed Session binds is polled, so many Sessions with open change requests mean more requests to the code host; closed requests keep polling at the idle cadence, and merged ones stop automatic polling. A Session without a cached summary shows its worktree and change requests in the list only after it is opened, and one last used before this change shows its worktree only then, because the `forgeWorktree` state changed and older cached rows are discarded. Checks that finish while no row is shown produce no notification. diff --git a/.agents/notes/implemented/feature/2026-10-20-forge-session-list.zh.md b/.agents/notes/implemented/feature/2026-10-20-forge-session-list.zh.md new file mode 100644 index 0000000000..73bfee90a7 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-20-forge-session-list.zh.md @@ -0,0 +1,39 @@ +# Agent Note: 会话列表中的 forge 状态 + +Status: implemented + +[English](2026-10-20-forge-session-list.md) | 中文 + +## Problem + +[forge 面板](2026-10-09-forge-panel.zh.md)原先在 Session 标题旁的标签中写出 Session 的分支。标题栏的操作行不会收缩,因此较长的分支名会与后台任务标签等其他标题栏条目重叠。想知道哪些 Session 在自己的 worktree 中工作、它们的哪些变更请求需要处理,也必须逐个打开 Session 的标签页。会话列表为行图标与悬浮卡片提供了位置,即 `sidebar.session.row.trailing` 与 `sidebar.session.row.hover`,其中的条目不得加载或绑定 Session。一行需要两类事实:Session 日志记录的事实,即面板创建并进入的 worktree 以及 Session 绑定的变更请求;以及只有轮询才知道的事实,即每个变更请求的状态与检查。 + +## Decision + +Session 标题旁的标签已移除,由标签页顶部一行写出分支。Session 在面板为它创建的 worktree 中工作时,会话行在时间之后显示分支图标;另有一个图标显示它最需要关注的变更请求,依次为需要处理、打开、草稿、尚未轮询、已合并、已关闭。打开中的变更请求图标保持会话行的浅色,并用圆点标出检查;已合并与已关闭的保留各自的状态颜色。会话行悬浮卡片中的部分写出 worktree 分支及其起点,再列出最多三个变更请求的状态与检查,并说明还有多少个。 + +日志事实经投影的客户端视图进入列表,由 Session 摘要携带。`forgeWorktree` 投影还从 Session 头部与每个 `working-directory/change` 记录 Session 的目录;该目录就是所创建的 worktree 或位于其下时,视图为该 worktree,否则为 null。`forgeChangeRequests` 视图列出绑定但不含状态,与[绑定决策](2026-10-09-session-change-request-bindings.zh.md)的设想一致。投影缓存为未加载的 Session 保留这两项。 + +轮询到的状态经 `forgePanel.changeRequestStates` 流进入列表。宿主用一次运行服务所有流,最后一条流结束时运行停止。它经 `sessionQuery` 列出 Session 而不加载它们,已加载 Session 的可见绑定取自 `ctx.forgeSession`,其他 Session 的取自缓存的摘要;它跟随已加载 Session 的绑定变化,并经共享的 `ctx.forge.watch` 订阅每个已绑定的变更请求;已关闭的按空闲间隔轮询,已合并的保留订阅以接收显式刷新结果。每个变化了的状态以其 URL 的 SHA-256 摘要为键存入 `forge_panel_change_requests` 存储域,因此重启后在下次轮询之前就能显示最近已知的状态;仅在所有已列出 Session 的绑定均已知时删除无人绑定的已存状态。每条流先发送调用方可读状态的 `baseline`,之后发送 `changed` 帧,均经 Remote 访问控制过滤。 + +浏览器为所有会话行与悬浮卡片部分只保留一个状态来源:第一个订阅方打开流,最后一个关闭它,传输重置后重新打开。 + +## Alternatives considered + +**把变更请求只留给标签页。** forge 面板最初否决了 Session 行悬浮分区,因为对单个 Session 而言它与标签页重复。列表能同时比较所有 Session,这是任何标签页都做不到的;悬浮部分也只补充会话行图标所概括的事实。 + +**保留 Session 标题旁的分支。** 截断标签会遮住分支名,而且标题栏只显示当前打开的 Session;列表显示每个 Session,标签页则完整写出分支。 + +**把轮询状态放进投影。** 投影折叠的是 Session 日志,而轮询到的状态不写入日志。放进投影要么为每次轮询记录事件,而这些事件没有任何模型请求需要;要么让投影依赖重放无法重建的进程内状态。 + +**为每个已列出的 Session 跟随其面板。** 面板会在 Session 的执行环境中用 Git 检查检出并检查登录状态;列表只需要绑定与变更请求状态。 + +**加载已列出的 Session 来读取绑定。** 列表将重放每个 Session 的日志;缓存的摘要无需加载就能回答。 + +**只把状态保存在内存中。** 重启后每一行都会显示未知状态,直到每个变更请求再次被轮询。已存状态是派生数据,因此不符合 schema 的记录会被放到一边,而不是让运行失败。 + +**为在项目目录中工作的 Session 显示分支一行。** 读取该分支需要在 Session 的执行环境中运行 Git,而列表不加载 Session 就无法到达那里。 + +## Consequences + +列表无需加载 Session 就能显示每个 Session 的 worktree 与变更请求状态,重启后立即显示最近已知的状态,标题栏也不再重叠。有会话行显示期间,已列出 Session 绑定的每个打开中的变更请求都会被轮询,因此有大量带打开变更请求的 Session 时,对代码托管平台的请求会增多;已关闭的继续按空闲间隔轮询,已合并的停止自动轮询。没有缓存摘要的 Session 要打开之后才会在列表中显示其 worktree 与变更请求;此变更之前最后使用的 Session 也要到那时才显示其 worktree,因为 `forgeWorktree` 的状态已变化,旧的缓存行会被丢弃。没有会话行显示期间结束的检查不会有通知。 diff --git a/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.i18n.yaml b/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.i18n.yaml new file mode 100644 index 0000000000..e3b8a71842 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-22-listed-model-badges.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/feature/2026-10-22-listed-model-badges.md +/agent-note-model-selector-badges-come-from-the-model-listing: + en: cc4b8fee78e75a98 + zh: 3cf21a37bea4410c +/agent-note-model-selector-badges-come-from-the-model-listing/problem: + en: ed011e743511ef25 + zh: 2389e339f1a6dcbf +/agent-note-model-selector-badges-come-from-the-model-listing/decision: + en: 84b99f80d8824117 + zh: 94cd96f59caf5b43 +/agent-note-model-selector-badges-come-from-the-model-listing/alternatives-considered: + en: 079466e0dd56584e + zh: 8e33049c422d77cb +/agent-note-model-selector-badges-come-from-the-model-listing/consequences: + en: 3ba39c2a109e49cd + zh: 3eb16676854230f9 diff --git a/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.md b/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.md new file mode 100644 index 0000000000..cb4260bdb1 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.md @@ -0,0 +1,47 @@ +# Agent Note: Model selector badges come from the model listing + +Status: implemented + +English | [中文](2026-10-22-listed-model-badges.zh.md) + +## Problem + +The Web model selector showed `1017` beside `DeepSeek-V4.1-Flash` through a dedicated client package that matched two DeepSeek providers, one model id, and one exact catalog name. Changing or retiring that release marker, or labeling another model, required a client release, while both DeepSeek routes already read model metadata from the service's `GET /models` listing. + +## Decision + +- Each `/models` entry may declare `badge: { text, tone }` under `client_metadata["deepseek-harness"]`, keyed like `client_requirements` by the User-Agent product token. `client_metadata` holds client-specific data that every build may ignore, so the public model fields stay unchanged and later DeepSeek Harness fields join the same object. `text` uses the package-metadata `LocalizedText` form: one string, or translations keyed by lowercase language id that include `en`. Each text is trimmed and holds 1 to 12 user-perceived characters on one line; controls, Unicode line separators, bidi controls, and text made only of whitespace or default-ignorable characters are rejected. +- `tone` is `neutral`, `info`, `success`, or `warning`: the `Tag` palettes for a plain fact, a highlight, a benefit, and attention. The shipped release marker used `info`; the surveyed selectors draw plan and capability tags neutral, new features with an accent, and usage hints in a warning color. `danger` and `solid` keep their failure and selection meanings. +- [`parseModelListing`](../../../../packages/llm/llm-deepseek/src/listing.ts) decodes the badge beside the entry's other declarations, and [`resolveListedCatalog`](../../../../packages/llm/llm-deepseek/src/listed-catalog.ts) keeps the badges of offered models; both routes attach them through `catalogModelInfo`. An absent or `null` value at any level of that path shows no badge, and an absent or unrecognized tone shows `neutral`. A level that is not an object or an unreadable badge drops the badge, which is reported with the listing's other problems and never hides its model. +- `LlmModelInfo.badge` and `ModelCatalogModel.badge` carry the badge from discovery through the Host model catalog to the browser. `resolveModelInfo` omits it, and no model request, Session event, or model-facing tool reads it. +- [`ui-model-selection`](../../../../packages/client/ui-model-selection/README.md) renders the badge before the catalog name in the trigger and after it in the root-menu value and each option, resolving its text with `ctx.locale.resolveText`. A `conversation.input.model.label` contribution replaces the name and badge together and receives the badge in its owner props. +- The build ships no badge of its own: a route before its first listing shows none. The API-key route does not follow listings for customized catalogs or non-official endpoints; the account route follows the origin of its configured `baseURL`, including custom endpoints. The fixed `ui-deepseek-model-label-1017` package and its Web bundle row are removed. +- After first use, the credential-scoped cache refreshes automatically at `modelsMaxAgeMs` (ten minutes by default) and publishes changed catalogs through `llm/models-updated`. Successful listings replace all badge metadata, so omission or `null` clears an old badge. Failed refreshes retain the last successful listing, including badges: a failed request does not establish that the server withdrew a badge. This avoids independent polling in every browser. A zero interval preserves access-only refresh. + +## Alternatives considered + +**A top-level `badge` field.** The public `/models` fields describe model capabilities and limits for every client, while the badge is DeepSeek Harness release and campaign copy. A top-level field reads as a model attribute that other clients could start rendering, after which the service could no longer change or retire it freely, and two clients could not show different badges. + +**`badge` inside `client_requirements["deepseek-harness"]`.** It adds no top-level field, and released builds ignore an extra key there. But an unreadable requirement hides its model on every build, released ones included, so a serializer that writes the emptied object as `null` after a badge is retired would hide the model. Requirements gate availability; display data must stay ignorable. + +**A narrower map such as `client_presentation` or `client_annotations`.** These names limit the object to display data or descriptive notes, so the next client-specific field of another kind, such as a default, would need another top-level field; `client_metadata` takes any field a build may ignore. + +**A semantic badge kind mapped to client dictionaries.** OpenCode, ZCode, and DeepSeek-Reasonix translate a closed set of client-owned tag kinds, and none renders text a server sends. A fixed vocabulary cannot express a release code such as `1017`, and each new wording would need the client release the listing exists to avoid. Listing text is provider data, which the [locale-owned copy decision](../architecture/2026-08-23-locale-owned-client-ui-copy.md) renders verbatim; the `LocalizedText` form still lets the service supply translations. + +**Accept colors, CSS, or markup from the listing.** No surveyed product accepts styling from a catalog, and arbitrary colors bypass the theme's light and dark palettes and their contrast. A closed tone set maps onto the existing `Tag` palettes, and an unrecognized tone degrades to `neutral`, so a value a newer service adds never removes the text. + +**Keep `1017` as a built-in default.** A bundled badge would keep the marker offline and on custom endpoints. Under the listing rule that an absent field keeps the build's value, the service could then remove it only with a special empty value, and old builds would keep the stale marker wherever no listing loads. Shipping no badge makes the listing the only source, so omitting the field removes the badge. + +**An expiry or start time on the badge.** The listing cache lives in memory and refreshes on access after `modelsMaxAgeMs`, so a badge the service drops disappears within one refresh. Client-side time windows would add dependence on the client clock without removing reachable staleness. + +**Drop the whole entry or listing for a bad badge.** OpenCode drops a model whose catalog status it cannot decode, and Codex fails its catalog decode on one unknown enum value. A badge is presentation only, so a bad one is dropped by itself. + +**A DeepSeek-specific badge Remote beside the catalog.** Keeping the badge out of `LlmModelInfo` would need a second Remote, a second client fetch, and agreement between two caches about which models exist. One optional field on the existing catalog reaches every placement in one load. + +## Consequences + +- The service changes, localizes, or removes a badge without a client release; a running client shows the change after its next listing refresh and catalog reload. +- A condition that older builds must enforce belongs in `client_requirements`; every field under `client_metadata` must be safe for a build that does not read it. +- `deepseek-flash` shows no badge until the service declares one and a listing loads. Customized catalogs and non-official endpoints on the API-key route do not receive listed badges. +- The `/model` popup, the trigger's accessible name, and the Models settings page keep plain names. +- A profile patch that still targets `ui-deepseek-model-label-1017` matches no row and boots with a warning ([upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.md)). diff --git a/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.zh.md b/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.zh.md new file mode 100644 index 0000000000..6b2e42032d --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-22-listed-model-badges.zh.md @@ -0,0 +1,47 @@ +# Agent Note: 模型选择器角标来自模型列表 + +Status: implemented + +[English](2026-10-22-listed-model-badges.md) | 中文 + +## 问题 + +Web 模型选择器通过一个专门的客户端包在 `DeepSeek-V4.1-Flash` 旁显示 `1017`,该包写死匹配两个 DeepSeek 提供方、一个模型 id 和一个确切的目录名称。修改或撤下这个版本标记、给其他模型加标签,都要发一次客户端版本;而两条 DeepSeek 路由早已从服务端的 `GET /models` 列表读取模型元数据。 + +## 决策 + +- 每个 `/models` 条目可以在 `client_metadata["deepseek-harness"]` 下声明 `badge: { text, tone }`,与 `client_requirements` 一样以 User-Agent 的产品名为键。`client_metadata` 存放所有版本都可以忽略的客户端专属数据,因此公开的模型字段保持不变,以后新增的 DeepSeek Harness 字段也放进同一个对象。`text` 采用包元数据的 `LocalizedText` 形式:单个字符串,或以小写语言 id 为键且包含 `en` 的译文。每段文字去掉首尾空白后须为单行,长度为 1 到 12 个用户可见字符;拒绝控制字符、Unicode 行分隔符、双向控制符及仅由空白或默认可忽略字符组成的文字。 +- `tone` 取 `neutral`、`info`、`success` 或 `warning`,分别是 `Tag` 表示普通事实、强调、利好和需要注意的配色。已发布的版本标记使用 `info`;调研的选择器把套餐和能力标签画成中性色,新功能用强调色,用量提示用警示色。`danger` 和 `solid` 保留各自表示失败和当前选中的含义。 +- [`parseModelListing`](../../../../packages/llm/llm-deepseek/src/listing.ts) 与条目的其他声明一起解码角标,[`resolveListedCatalog`](../../../../packages/llm/llm-deepseek/src/listed-catalog.ts) 保留已提供模型的角标;两条路由都通过 `catalogModelInfo` 附上角标。这条路径上任何一层缺省或为 `null` 时不显示角标;`tone` 缺省或无法识别时按 `neutral` 显示。某一层不是对象或角标无法读取时,角标被丢弃,并与列表的其他问题一起报告,但不会隐藏所属模型。 +- `LlmModelInfo.badge` 与 `ModelCatalogModel.badge` 把角标从模型发现经 Host 模型目录送到浏览器。`resolveModelInfo` 不返回它,模型请求、Session 事件和面向模型的工具都不读取它。 +- [`ui-model-selection`](../../../../packages/client/ui-model-selection/README.zh.md) 在触发按钮中把角标放在目录名称之前,在根菜单当前值和各选项中仍放在名称之后,并用 `ctx.locale.resolveText` 解析其文字。`conversation.input.model.label` 的扩展会同时替换名称与角标,并从 owner props 收到角标。 +- 本版本不内置任何角标:路由加载到第一份列表之前不显示角标。API key 路由在自定义目录或非官方端点上不跟随列表;账号路由跟随配置的 `baseURL` 的源,包括自定义端点。写死的 `ui-deepseek-model-label-1017` 包及其 Web bundle 条目已移除。 +- 按凭据区分的缓存首次使用后按 `modelsMaxAgeMs`(默认十分钟)自动刷新,通过 `llm/models-updated` 发布目录变化。成功加载会替换所有角标元数据,因此省略或 `null` 都会清除旧角标。刷新失败时保留上次成功加载的列表及其角标:请求失败并不能证明服务端撤下了角标。这样也避免了每个浏览器独立轮询。间隔为零时仍只在访问时刷新。 + +## 考虑过的替代方案 + +**顶层 `badge` 字段。** 公开的 `/models` 字段为所有客户端描述模型的能力与限制,而角标是 DeepSeek Harness 的发版和运营文案。放在顶层会被当作模型属性,其他客户端可能开始显示它,此后服务端就无法随意修改或撤下;两个客户端也无法显示不同的角标。 + +**把 `badge` 放进 `client_requirements["deepseek-harness"]`。** 这样不增加顶层字段,已发布版本也会忽略其中多出的键。但版本要求无法读取时,所有版本都会隐藏该模型,已发布的版本也不例外;撤下角标后,序列化器若把清空的对象写成 `null`,模型就会被隐藏。版本要求决定模型能否使用,展示数据必须可以忽略。 + +**范围更窄的映射,例如 `client_presentation` 或 `client_annotations`。** 这些名称把对象限定为展示数据或附加说明,下一个其他类型的客户端专属字段(例如默认值)又要新开一个顶层字段;`client_metadata` 可以容纳任何版本都能忽略的字段。 + +**用语义化的角标类别映射到客户端字典。** OpenCode、ZCode 和 DeepSeek-Reasonix 都翻译一组封闭的、由客户端持有的标签类别,没有一家直接渲染服务端发来的文字。固定词表表达不了 `1017` 这样的版本代号,每出现一种新说法都要发一次客户端版本,而列表正是为了免去这一步。列表文字属于提供方数据,按[客户端文案归 locale 管理的决定](../architecture/2026-08-23-locale-owned-client-ui-copy.zh.md)原样渲染;`LocalizedText` 形式仍允许服务端提供译文。 + +**接受列表下发的颜色、CSS 或标记。** 调研的产品都不接受目录下发的样式;任意颜色会绕过主题的明暗配色及其对比度。封闭的语调集合直接对应现有 `Tag` 配色;无法识别的语调降级为 `neutral`,因此较新的服务端新增的取值不会让文字消失。 + +**把 `1017` 保留为内置默认值。** 内置角标能让标记在离线和自定义端点下继续显示。但按照“字段缺失时沿用本版本的值”的列表规则,服务端只能用特殊的空值才能去掉它,而且旧版本在加载不到列表时会一直显示过时的标记。不内置角标,列表就是唯一来源,省略字段即可去掉角标。 + +**给角标加过期时间或生效时间。** 列表缓存只在内存中,超过 `modelsMaxAgeMs` 后在访问时刷新,服务端撤下的角标在一次刷新内就会消失。客户端时间窗口会引入对客户端时钟的依赖,却消除不了实际可能出现的陈旧数据。 + +**角标有误时丢弃整个条目或整份列表。** OpenCode 会丢掉目录状态无法解码的模型,Codex 遇到一个未知枚举值就让整份目录解码失败。角标只用于展示,有误时只丢弃角标本身。 + +**在目录之外为 DeepSeek 单独提供角标 Remote。** 让角标不进入 `LlmModelInfo`,就需要第二个 Remote、客户端的第二次请求,以及两份缓存对“有哪些模型”保持一致。在现有目录上增加一个可选字段,一次加载即可送到所有显示位置。 + +## 影响 + +- 服务端无需客户端发版即可修改、翻译或撤下角标;运行中的客户端在下一次列表刷新和目录重新加载后显示变化。 +- 旧版本必须执行的条件放在 `client_requirements`;`client_metadata` 下的每个字段都必须保证不读取它的版本也能安全运行。 +- 服务端声明并加载到列表之前,`deepseek-flash` 不显示角标。API key 路由的自定义目录与非官方端点不接收列表角标。 +- `/model` 弹窗、触发按钮的无障碍名称和模型设置页仍只显示名称。 +- 仍指向 `ui-deepseek-model-label-1017` 的 profile patch 不再匹配任何条目,启动时会给出警告([升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.zh.md))。 diff --git a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.i18n.yaml b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.i18n.yaml index 4a98c2f555..c8b3188032 100644 --- a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.i18n.yaml @@ -18,11 +18,11 @@ en: 1310a89c351e5bfe zh: e6386ae10b893c2d /agent-note-performance-baselines-for-large-session-corpora/decision/measured-endpoints: - en: 11937dfa4a349a19 - zh: b91a136890fe92e8 + en: add81088ce51033f + zh: c7065dc3052648fa /agent-note-performance-baselines-for-large-session-corpora/calibration-evidence: - en: f934c95748dcbfe0 - zh: df6a68286c21510f + en: adb815c213373d8e + zh: 44d4c6752a4df51f /agent-note-performance-baselines-for-large-session-corpora/alternatives-considered: en: 4eaa4666262a1aee zh: 7e2eb44b94bad05d diff --git a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.md b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.md index 2efe66ead2..7890b73070 100644 --- a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.md +++ b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.md @@ -47,6 +47,8 @@ The shared corpus and projection-list controller fixture awaits controller activ The file must finish within five minutes on standard hosted CI, including seeding, and its last case asserts that limit. Search over 1,000 Sessions and one fork of the longest Session take about 170 s of it, so each has one sample. The list corpus is the largest that fits the remainder. Budgets enforce medians of list boot, first, and repeat, and of the fork median over strata and rank 990; search and the longest fork enforce their single samples. Search and fork use separate 1,000-Session roots, so fork children never reach the search index. Rank 988 is the highest top-level rank at the measured p99 (7,229 events); forking a subagent child adds a lineage lookup, and rank 990 already falls in the 99.3% anchor. Peak RSS of the list, search, and fork processes has a budget with the shared 1.25 headroom and no time scale. Each child's deadline equals the file limit, and each test's deadline outlasts its sequential children, so a failed case reaps every child before the scratch root is removed. +Fork workers use a 512 MiB V8 old-space limit so every runner measures the same heap constraint. The corpus, timed endpoints, and whole-process peak RSS budget stay unchanged; native buffers still count toward RSS. The longest fork retains about 193 MiB after collection, so the limit leaves room for live objects while forcing collection before a larger host-selected heap delays it. This measures a bounded-heap workload, not the default-heap peak of every deployment. + ## Calibration evidence Budgets use standard two-CPU hosted CI expectations (Linux x64, Node 24.21, PR #5403), rounded above the higher recorded median across the AMD EPYC 7763 and EPYC 9V74 runners and multiplied by the shared 1.25 headroom. Samples varied by up to 15% within one run; the runner models differed by up to 25% per endpoint. The hosted-to-reference ratio is 2.4 to 3.4, above the shared reference scale of 2, so reference-machine scaling would reject ordinary hosted runs. @@ -67,6 +69,8 @@ With seeding in one process, the complete file took 294.5 s on the EPYC 7763 run On Apple M5 Pro with Node 26.5, isolated runs measured list 1,000 at 311–358 ms first, list 5,000 at 1,364–1,603 ms first, search at 57 s first, and fork of the longest Session at 6.0–6.4 s. A 10,000-Session list took 3.2–5.4 s for the first call with 1.9 GB peak RSS. +Node 24.22 hosted runs of the unchanged longest-fork workload recorded 696 MiB peak RSS on EPYC 9V74 and 1,077.7 MiB on EPYC 7763, against the same 938 MiB budget. On Apple M5 Pro with Node 24.19, two default-heap runs retained 192.8–193.4 MiB and peaked at 770.7–812.6 MiB in 2.416–2.447 s; two 512 MiB old-space runs retained the same range and peaked at 764.2–764.7 MiB in 2.487–2.528 s. The fixed limit controls the heap policy without raising the RSS or timing budgets. + ## Alternatives considered - **Sanitized copies of real Sessions.** Rejected: benchmarks never use recorded user material. diff --git a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.zh.md b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.zh.md index f964d640b8..132619b19d 100644 --- a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.zh.md +++ b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.zh.md @@ -47,6 +47,8 @@ worker 是编译后的纯 Node 进程,每次都针对已播种根目录全新 此文件(包括播种)必须在标准托管 CI 上五分钟内完成,其最后一个用例断言该上限。1,000 个 Session 的搜索和最长 Session 的一次 fork 约占其中 170 s,因此两者各取一个样本。列表语料是剩余时间内能容纳的最大规模。预算约束列表启动、首次和重复的中位数,以及 fork 分层中位数和排名 990 的中位数;搜索和最长 fork 约束各自的单个样本。搜索和 fork 使用各自独立的 1,000 个 Session 根目录,因此 fork 子会话不会进入搜索索引。排名 988 是实测 p99(7,229 个事件)上最高的顶层排名;fork subagent 子会话会增加一次谱系查找,而排名 990 已落入 99.3% 锚点。列表、搜索和 fork 进程的峰值 RSS 设有预算,使用共享的 1.25 余量,不做时间缩放。每个子进程的截止时间等于文件上限,每个测试的截止时间长于其顺序子进程之和,因此失败的用例会在删除临时根目录之前回收所有子进程。 +Fork worker 使用 512 MiB 的 V8 old-space 上限,让各运行器在相同的堆约束下测量。语料、计时终点和整个进程的峰值 RSS 预算保持不变,原生缓冲区仍计入 RSS。最长 fork 回收后保留约 193 MiB,因此该上限为存活对象保留空间,同时避免较大的宿主默认堆延迟回收。该用例测量受限堆下的工作负载,不代表每种部署默认堆下的峰值。 + ## 校准证据 预算使用标准双 CPU 托管 CI 的期望值(Linux x64、Node 24.21、PR #5403),取高于 AMD EPYC 7763 与 EPYC 9V74 运行器中较高记录中位数的整数,再乘以共享的 1.25 余量。同一次运行内的样本差异最多 15%;两种运行器型号在单个端点上最多相差 25%。托管与参考机器的比值为 2.4 到 3.4,高于共享的参考缩放系数 2,因此按参考机器缩放会拒绝正常的托管运行。 @@ -67,6 +69,8 @@ worker 是编译后的纯 Node 进程,每次都针对已播种根目录全新 在 Apple M5 Pro 和 Node 26.5 上的隔离运行中,1,000 个 Session 的首次列表为 311–358 ms,5,000 个 Session 的首次列表为 1,364–1,603 ms,首次搜索为 57 s,fork 最长 Session 为 6.0–6.4 s。10,000 个 Session 的首次列表耗时 3.2–5.4 s,峰值 RSS 为 1.9 GB。 +未改动的最长 fork 工作负载在 Node 24.22 托管运行中,EPYC 9V74 的峰值 RSS 为 696 MiB,EPYC 7763 为 1,077.7 MiB,使用的预算均为 938 MiB。在 Apple M5 Pro、Node 24.19 上,两次默认堆运行回收后保留 192.8–193.4 MiB,峰值为 770.7–812.6 MiB,耗时 2.416–2.447 s;两次 512 MiB old-space 运行保留量相同,峰值为 764.2–764.7 MiB,耗时 2.487–2.528 s。固定上限用于控制堆策略,没有提高 RSS 或耗时预算。 + ## 考虑过的替代方案 - **脱敏复制真实 Session。** 已否决:基准从不使用录制的用户材料。 diff --git a/.agents/skills/dsh-client-ui-ux/SKILL.md b/.agents/skills/dsh-client-ui-ux/SKILL.md index 6d293a5f86..0b8b72de99 100644 --- a/.agents/skills/dsh-client-ui-ux/SKILL.md +++ b/.agents/skills/dsh-client-ui-ux/SKILL.md @@ -67,6 +67,10 @@ Use [dsh-responsive-ui](../dsh-responsive-ui/SKILL.md) for responsive layouts, m - Review every changed region for gaps: nothing sits flush against its neighbour without an intentional gap, and adjacent icons and elements keep the region's established spacing. - Prefer symmetry for parallel elements; an unexplained one-off gap or offset usually signals a missed shared value. +- **For complex pages and containers, establish shared horizontal and vertical alignment axes before adjusting local spacing.** Consider hover, selected, and expanded/collapsed states, as well as the overall relationship with adjacent panels and other page elements. Choose from the following references according to layout similarity: + - **Sidebar:** align corresponding text starts and leading icons across vertically stacked rows. Align hover and selected row backgrounds with the buttons above the list to maintain visual balance. + - **Plugins page:** keep content left-aligned along a shared edge. List-row hover backgrounds may extend beyond that edge while the content stays in place. + - **Conversation and right-sidebar headers:** align the first header rows across both panels and match their heights. Apply the same rule to the second rows, and align corresponding underlines and dividers on shared horizontal baselines. ## Design review escalation diff --git a/.agents/skills/dsh-session-projection/SKILL.md b/.agents/skills/dsh-session-projection/SKILL.md index 90d483f4ac..f2820d7454 100644 --- a/.agents/skills/dsh-session-projection/SKILL.md +++ b/.agents/skills/dsh-session-projection/SKILL.md @@ -13,6 +13,8 @@ Keep recoverable session facts in Projection and expose them through their ownin 2. Do not expose projection definitions or handles as production read APIs. Keep the ready handle private; consumers use the owning service's domain methods. A handle is a process-local reader, recreated during activation. Require `sessionProjections` when the service depends on it; optional registration is only for optional contributions. Do not hide a missing required dependency with another recovery path. + A runtime that needs several projected states receives one typed object of callbacks created with `handle.readerFor(session)`. Add properties instead of a constructor parameter or feature-specific reader for each state. Each callback has type `() => S`; single-purpose collaborators receive it directly. These host-state readers are separate from wire snapshots and live Cordis Context; they own no registration, disposal, cache or I/O. Registration, Session preparation and handle lifetimes stay with their owners. Rule 9 defines synchronous API compatibility. + 3. State recovery through `init` and `apply` is synchronous and deterministic. Do not perform I/O or trigger Cordis hooks, models, or tools. Runtime hooks own those effects. Replay must not repeat them. Return the same state reference for unrelated events. Keep views synchronous and preserve their references when visible values do not change. 4. Persist JSON state that its schema accepts. Follow the README's state-version and checkpoint-watermark rules. Restoring a checkpoint and replaying its suffix must match full replay. Change the version when state fields or recovery semantics change. diff --git a/.agents/skills/record-browser-gif/SKILL.md b/.agents/skills/record-browser-gif/SKILL.md index a16f8294e9..1fa60fcb4f 100644 --- a/.agents/skills/record-browser-gif/SKILL.md +++ b/.agents/skills/record-browser-gif/SKILL.md @@ -1,6 +1,6 @@ --- name: record-browser-gif -description: Record browser or Web UI interaction demos as optimized GIFs using the available browser-control workflow, optional Playwright Videos for higher capture frame rates, and deterministic encoding, then attach the GIF to a pull request with `gh --attach`, falling back to a dedicated assets branch where attach cannot apply. Use when asked to make, record, or generate a GIF that demonstrates a browser workflow, and for every pull request that changes product-user-visible GUI behavior, which MUST include a GIF recorded from the pull request's real server and model flow. +description: Record browser or Web UI interaction demos as optimized GIFs using the available browser-control workflow, optional Playwright Videos for higher capture frame rates, and deterministic encoding, then attach the GIF to a pull request with `gh --attach`; publication requires gh v2.99.0 or later, and a GIF over 5 MiB is split into parts. Use when asked to make, record, or generate a GIF that demonstrates a browser workflow, and for every pull request that changes product-user-visible GUI behavior, which MUST include a GIF recorded from the pull request's real server and model flow. --- # Record Browser GIF @@ -17,8 +17,8 @@ The recording itself is part of the evidence: use a real server booted from that ## Keep recording separate from publication -- Recording produces local video or screenshots and one `.gif` artifact only; it never mutates remote state. -- Publication — attaching the GIF to a pull request body with `gh --attach`, or pushing it to an assets branch and embedding its URL where attach cannot apply — is the separate final step, performed only when the task includes attaching the GIF to a pull request. It never touches the pull request's own branch. +- Recording produces local video or screenshots; encoding produces one `.gif` artifact, or its [numbered parts](#attach-with-gh) when it exceeds the 5 MiB limit. Neither mutates remote state. +- Publication — attaching the GIF to a pull request body with `gh --attach` — is the separate final step, performed only when the task includes attaching the GIF to a pull request. It never commits to any branch. - Preserve the requested recording conditions. A real-server or real-API demo must not use fixture queries, mock transports, synthetic event injection, or test-only hooks. If credentials or the server are unavailable, report that limitation instead of substituting a fixture. - Never read or expose credential values. Use the application's normal configuration path and a benign demonstration prompt. @@ -117,13 +117,15 @@ Encoder maintenance: run `python3 -m unittest discover -s "$GIF_SKILL_DIR/script Perform this step only when the task includes attaching the GIF to a pull request. -Never commit a GIF to the pull request's own branch or any branch that merges into a long-lived branch: binary media committed there bloats the repository history for every future clone. Prefer `gh --attach`, which uploads the GIF to GitHub and rewrites the body reference in one command, so no branch carries the media. +Never commit a GIF to the pull request's own branch or any other branch: every default clone and full-history fetch of the repository downloads media committed to any branch. Publish only with `gh --attach`, which uploads the GIF to GitHub and rewrites the body reference in one command, so no branch carries the media. ### Attach with gh -`gh --attach` requires `gh` v2.99.0 or later (`gh --version`), a repository on github.com — GitHub Enterprise Server is not supported — write access to the repository, and a GIF at or below 10 MB. Confirm the verified artifact fits that limit; when it does not, shrink it with `--max-width`, then `--colors` or `--fps`, before attaching. +Before attaching, check `gh --version`. `gh --attach` requires `gh` v2.99.0 or later, a repository on github.com — GitHub Enterprise Server is not supported — and write access to the repository. When `gh` is older than v2.99.0, stop and tell the developer to upgrade `gh` to v2.99.0 or later; keep the verified GIF for the retry, and do not install or upgrade `gh` without authorization. When any other requirement is unmet or the attach fails — authentication, write access, a repository outside github.com, or an upload error — stop and report it to the developer. Never publish the GIF any other way. -Write the GIF into the body file as an ordinary local-path reference, using the same path passed to `--attach`; `gh` rewrites the reference in place to the uploaded URL, keeping its position and alt text: +Publish GIFs at or below 5 MiB, the encoder's default `--max-bytes`. When the encoder rejects the output for size, shrink it with `--max-width`, then `--colors` or `--fps`. When the GIF still exceeds 5 MiB at readable text quality, split it instead: encode consecutive, non-overlapping intervals of the same evidence run into numbered parts that each pass the encoder's size check — `--start` and `--end` over the one video, or consecutive screenshot subsets selected with `--pattern` — and verify every part as an artifact. Beside each embed, state its position (such as `2/3`) and its source interval or frames. + +Write each GIF into the body file as an ordinary local-path reference, using the same path passed to `--attach`; `gh` rewrites each reference in place to its uploaded URL, keeping its position and alt text, so the order of references in the body sets the display order of split parts: ```markdown ![]() @@ -138,35 +140,8 @@ gh pr create --body-file --attach # new pull re gh pr edit --body-file --attach # existing pull request ``` -`--attach` is repeatable but refuses the same file twice. A GIF the body does not reference is appended at the end, where alt text set on the flag (`--attach '#'`) applies; a rewritten reference keeps the body's alt text. After attaching, re-read the demonstrated live head and require it to remain at that recorded commit. Re-read the live body and confirm the reference now points at the uploaded URL, render the body through GitHub's Markdown API and confirm the expected ``, and fetch the uploaded URL once to confirm `200` and `image/gif`. +`--attach` is repeatable but refuses the same file twice. Publish split parts in one `gh pr create` or `gh pr edit` with one `--attach` per part, so a single pair of live-head checks covers every part. A GIF the body does not reference is appended at the end, where alt text set on the flag (`--attach '#'`) applies; a rewritten reference keeps the body's alt text. After attaching, re-read the demonstrated live head and require it to remain at that recorded commit. Re-read the live body and confirm the reference now points at the uploaded URL, render the body through GitHub's Markdown API and confirm the expected ``, and fetch the uploaded URL once to confirm `200` and `image/gif`. -### Fall back to an assets branch +### Existing assets branches -Use the assets-branch workflow only when `gh --attach` cannot apply: the GIF still exceeds 10 MB, `gh` is older than v2.99.0, or the repository is not on github.com. GIFs then live on a dedicated orphan assets branch — a branch with no parent commit and nothing but media — and one assets branch serves a whole pull request series (named `-assets`; list existing ones with `git ls-remote --heads origin '*assets*'`). - -Before either workflow below pushes, verify that the assets branch contains media only and that the staged GIF's checksum matches the verified local artifact. - -For an existing assets branch, work in a shallow single-branch scratch clone so the publication cannot touch your working tree: - -```sh -git clone --branch --single-branch --depth 1 /tmp/assets-checkout -cp /absolute/path/to/demo.gif /tmp/assets-checkout/.gif -cd /tmp/assets-checkout -git add .gif -git commit -m "assets: gif (#)" -git push origin -``` - -For a new series, make a fresh shallow scratch clone (`git clone --depth 1 /tmp/assets-checkout`), create the orphan branch with `git switch --orphan `, then add the GIF, commit, and push the same way. - -After pushing, use authenticated GitHub API or raw requests to confirm the remote path, byte size, checksum, `200` response, and `image/gif` content type. An anonymous `404` does not disprove a private-repository asset; authenticate the verification instead. This proves the repository-member review path, not public availability. - -Immediately before editing the pull-request body, re-read the demonstrated pull request's live head and compare it with the commit recorded next to the GIF. Stop and re-record when it moved. After the edit, re-read the demonstrated live head and require it to remain at that recorded commit. Separately, render the body through GitHub's Markdown API and confirm that the expected `` is present. - -Embed the GIF in the pull request body with the raw blob URL; the `?raw=true` suffix is required, because the plain blob URL renders GitHub's file page instead of the image: - -```markdown -![](https://github.com///blob//.gif?raw=true) -``` - -Never delete or rewrite an assets branch, and never force-push it: merged pull request bodies reference its URLs forever. Append new commits only. +Earlier pull request bodies embed GIFs from orphan `*-assets` branches in this repository. Never add to, delete, rewrite, or force-push them: merged pull request bodies reference their URLs. diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 9b891d3a6e..bde3142e27 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -4,6 +4,6 @@ Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. Native Win The manual [Windows packaging workflow](workflows/windows-package.yml) uses the dedicated self-hosted Windows x64 label `dsh-win-package-trial` under native `pwsh`; preserve this routing separately from the automatic CI lanes. -The merge checks run from three workflows named `Policy`, which run on `pull_request_target` so their definitions come from master, and call the `deepseek-harness-check` action directly: `pr-check-on-head-branch.yml` on pull-request updates gives `Policy / Master Change` and `Policy / Pre-Merge Check`, `pr-check-after-approve.yml` on pull-request updates and submitted or dismissed reviews gives `Policy / Approve Check` and `Policy / Reviewer Evaluate`, and `pr-check-on-base-branch.yml` on master pushes re-runs the head-branch checks as `Policy / Recheck`. Branch rules require those check names, so keep the job names and the workflow name. Each check reads its mode from a repository variable: `MERGE_POLICY_MASTER_DIFF`, `MERGE_POLICY_PRE_MERGE`, `MERGE_POLICY_HEAD_DIFF`, and `MERGE_POLICY_REVIEW`, each `off` (skip), `report` (run without failing the job), or `block` (fail the job); a missing value or an unknown one counts as `report`. `Policy / Pre-Merge Check` merges the commit `Policy / Master Change` reports, so it runs only after that job and stays off while `MERGE_POLICY_MASTER_DIFF` is `off`; a merge it cannot do and a build that fails both follow its own mode, so `report` warns and skips the build while `block` fails the job. +The merge checks run from three workflows named `Policy`, which run on `pull_request_target` so their definitions come from master, and call the `deepseek-harness-check` action directly: `pr-check-on-head-branch.yml` on pull-request updates gives `Policy / Master Change` and `Policy / Pre-Merge Check`, `pr-check-after-approve.yml` on pull-request updates and submitted or dismissed reviews gives `Policy / Approve Check` and `Policy / Reviewer Evaluate`, and `pr-check-on-base-branch.yml` on master pushes re-runs the head-branch checks as `Policy / Recheck`. Branch rules require those check names, so keep the job names and the workflow name. Each check reads its mode from a repository variable: `MERGE_POLICY_MASTER_DIFF`, `MERGE_POLICY_PRE_MERGE`, `MERGE_POLICY_HEAD_DIFF`, and `MERGE_POLICY_REVIEW`, each `off` (skip), `report` (run without failing the job), or `block` (fail the job); a missing value or an unknown one counts as `report`. `Policy / Pre-Merge Check` merges the commit `Policy / Master Change` reports, so it runs only after that job and stays off while `MERGE_POLICY_MASTER_DIFF` is `off`; a merge it cannot do and a build that fails both follow its own mode, so `report` warns and skips the build while `block` fails the job. `Policy / Pre-Merge Check` runs `scripts/pre-merge-check.ts` under plain Node and knows nothing else about the merged tree: that script checks the released manifests against the root version, installs, and builds the Host and Client contracts, so change it in one pull request together with the build it orchestrates. The manual Desktop publication workflow selects a successful package run and attempt, then routes each retained build through `DESKTOP_PUBLISH_RUNNERS` to its original machine's unique label. Keep Windows run steps on native PowerShell and Mac run steps on the default POSIX shell; shared entrypoints are Node scripts. Test and production jobs use separate `desktop-test` and `desktop-production` approval Environments after a credential-free preparation job. diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index 5620e6af63..d69a860098 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -14,6 +14,7 @@ on: - larger-runner-benchmark - consolidated-runner-benchmark - ssh-helper + - wine # New master pushes and manual runs replace obsolete checks in this workflow/ref. # Standby drills share cancellation; use completed runs as readiness evidence. @@ -49,9 +50,9 @@ jobs: secrets: DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }} - # Wine runs once here, independently of the native serial standby aggregates. + # Wine runs on master pushes or an explicit suite=wine dispatch, independently of native standbys. windows: - if: github.event_name == 'push' && github.ref == 'refs/heads/master' + if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || (github.event_name == 'workflow_dispatch' && inputs.suite == 'wine') runs-on: ubuntu-latest name: windows node 24 / wine timeout-minutes: 15 diff --git a/.github/workflows/cua-helper-package.yml b/.github/workflows/cua-helper-package.yml new file mode 100644 index 0000000000..20ecb7526f --- /dev/null +++ b/.github/workflows/cua-helper-package.yml @@ -0,0 +1,83 @@ +name: Package CUA helper · macOS + +on: + workflow_dispatch: + inputs: + source_ref: + description: '同仓库可信源码分支或 dsh-v* tag' + type: string + default: master + required: true + preset: + description: '机器上的签名配置' + type: choice + options: [test, production] + default: test + required: true + workflow_call: + inputs: + source_ref: + type: string + required: true + preset: + type: string + default: production + outputs: + artifact: + description: '包含两个架构的已签名、公证 helper 归档' + value: ${{ jobs.package.outputs.artifact }} + +permissions: + contents: read + +# Desktop uses the same keychain search list and optional system proxy transaction. +concurrency: + group: macos-desktop-package + cancel-in-progress: false + +jobs: + package: + runs-on: [self-hosted, macOS, ARM64, dsh-mac-package-trial] + timeout-minutes: 60 + outputs: + artifact: ${{ steps.artifact.outputs.name }} + env: + DSH_PACKAGING_PRESET: ${{ inputs.preset }} + DSH_TELEMETRY_DISABLED: '1' + PACKAGE_SOURCE_REF: ${{ inputs.source_ref }} + steps: + - uses: actions/checkout@v7.0.1 + with: + ref: ${{ inputs.source_ref }} + persist-credentials: false + clean: true + - name: Validate source selection + run: | + node -e 'const cp=require("node:child_process"); const fs=require("node:fs"); const ref=process.env.PACKAGE_SOURCE_REF; + if(ref.startsWith("refs/pull/") || /^[a-f0-9]{40}$/i.test(ref)) throw Error("Select a trusted repository branch or release tag"); + const tag=ref.replace(/^refs\/tags\//, ""); + if(tag.startsWith("dsh-v") && tag!=="dsh-v"+JSON.parse(fs.readFileSync("package.json","utf8")).version) throw Error("Release tag must match product version"); + if(!["test","production"].includes(process.env.DSH_PACKAGING_PRESET)) throw Error("Invalid preset");' + - uses: pnpm/action-setup@v5.0.0 + with: + dest: ${{ runner.temp }}/setup-pnpm-cua-${{ github.run_id }}-${{ github.run_attempt }} + - uses: actions/setup-node@v7.0.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install locked dependencies + run: pnpm install --frozen-lockfile + - name: Build and verify notarized applications + run: | + mkdir -p dist + pnpm exec tsx scripts/cua-helper/package.ts --out dist/cua-helper + - name: Name retained artifact + id: artifact + run: echo "name=cua-helper-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@v7.0.1 + with: + name: ${{ steps.artifact.outputs.name }} + path: dist/cua-helper/* + if-no-files-found: error + compression-level: 0 + retention-days: 14 diff --git a/.github/workflows/pr-check-on-head-branch.yml b/.github/workflows/pr-check-on-head-branch.yml index 5ab02a6a94..adb884d62c 100644 --- a/.github/workflows/pr-check-on-head-branch.yml +++ b/.github/workflows/pr-check-on-head-branch.yml @@ -78,8 +78,19 @@ jobs: if [[ ! "$BASE_SHA" =~ ^[0-9a-f]{40}$ || ! "$SINCE" =~ ^[0-9]+$ ]]; then stop "the master diff check reported no base commit, so nothing was merged or compiled" fi - git fetch --no-tags --no-write-fetch-head --shallow-since="$SINCE" origin "$HEAD_SHA" "$BASE_SHA" || + # A shallow fetch starts a background maintenance run that rewrites .git/shallow, and the second shallow fetch + # below then fails with "shallow file has changed since we read it". + fetch() { + git -c maintenance.auto=false -c gc.auto=0 fetch --no-tags --no-write-fetch-head "$@" origin "$HEAD_SHA" "$BASE_SHA" + } + fetch --shallow-since="$SINCE" || stop "the base commit $BASE_SHA could not be fetched, so nothing was merged or compiled" + # A time window records a merge commit itself as shallow when one of its parents predates the window, which + # drops the other, in-window parent too; a depth bound keeps both parents of every commit it covers. + if ! git merge-base "$HEAD_SHA" "$BASE_SHA" >/dev/null; then + fetch --depth=20 --filter=blob:none || + stop "the base commit $BASE_SHA could not be fetched, so nothing was merged or compiled" + fi git -c user.name=pr-check -c user.email=pr-check@users.noreply.github.com merge --no-ff --no-edit -m "Merge $BASE_SHA into $HEAD_SHA" "$BASE_SHA" || stop "the pull request cannot merge $BASE_SHA; merge master into it and push, then this check runs again" echo "merged=true" >> "$GITHUB_OUTPUT" @@ -93,15 +104,26 @@ jobs: node-version: '24' package-manager-cache: false - - name: Run the pre-merge command + - name: Run the tree's pre-merge check if: steps.merge.outputs.merged == 'true' env: CHECK_MODE: ${{ vars.MERGE_POLICY_PRE_MERGE }} run: | - if ! bash -euxo pipefail -c 'pnpm install --frozen-lockfile && pnpm exec tsx scripts/compile-referenced-projects.ts host-contracts && pnpm exec tsx scripts/compile-referenced-projects.ts client'; then + # The check names one file and knows nothing else about how the merged tree builds itself. + if [ ! -f scripts/pre-merge-check.ts ]; then if [ "$CHECK_MODE" = block ]; then - echo "::error title=Pre-Merge Check::the merged tree does not install or compile" + echo "::error title=Pre-Merge Check::this tree has no scripts/pre-merge-check.ts, so nothing was installed, compiled, or checked" exit 1 fi - echo "::warning title=Pre-Merge Check::the merged tree does not install or compile; this check does not block" + echo "::warning title=Pre-Merge Check::this tree has no scripts/pre-merge-check.ts, so nothing was installed, compiled, or checked" + exit 0 fi + if node scripts/pre-merge-check.ts; then + exit 0 + fi + # A check that crashes reports nothing, so the step reports its exit the way the mode asks. + if [ "$CHECK_MODE" = block ]; then + echo "::error title=Pre-Merge Check::scripts/pre-merge-check.ts exited without reporting, so the merged tree is unchecked" + exit 1 + fi + echo "::warning title=Pre-Merge Check::scripts/pre-merge-check.ts exited without reporting, so the merged tree is unchecked" diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index ec03a117de..eb7e3a5eef 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -1,12 +1,28 @@ # Publish the dsh release sequence to npm. This workflow is manual-only # (workflow_dispatch) and intentionally does not listen to pull_request or push: -# publication must always be an explicit, reviewed act from a dsh-v* tag, and it -# must never appear as a PR check. It repacks the current tree before publishing +# publication is an explicit, reviewed act; dsh-v* tag validation is enabled by +# default and may be skipped through the dispatch input. It never appears as a PR check. It repacks the current tree before publishing # so the bytes uploaded are exactly what this dispatch produced. name: Release publish (dsh) on: workflow_dispatch: + inputs: + cua_run_id: + description: '已有 CUA 打包 run ID:留空重新打包;填写时复用同一源码和版本的已验证产物' + type: string + required: false + default: '' + cua_run_attempt: + description: 'CUA 打包 attempt:仅与 run ID 一起使用;留空选择最新一次,不回退到更早的成功构建' + type: string + required: false + default: '' + skip-tag-check: + description: Skip release tag validation for the selected ref; other publication checks still apply. + type: boolean + required: false + default: false permissions: contents: read @@ -16,9 +32,23 @@ env: DSH_TELEMETRY_DISABLED: '1' jobs: + cua-helper: + if: (startsWith(github.ref, 'refs/tags/dsh-v') || inputs.skip-tag-check) && inputs.cua_run_id == '' && inputs.cua_run_attempt == '' + uses: ./.github/workflows/cua-helper-package.yml + with: + source_ref: ${{ github.ref }} + preset: production + pack: name: Pack npm tarballs + needs: cua-helper + if: >- + ${{ !cancelled() && (needs.cua-helper.result == 'success' + || (needs.cua-helper.result == 'skipped' && (inputs.cua_run_id != '' || inputs.cua_run_attempt != ''))) }} runs-on: ubuntu-24.04 + permissions: + contents: read + actions: read steps: # Complete history: the release scripts read tags. - uses: actions/checkout@v7.0.1 @@ -55,10 +85,50 @@ jobs: - name: Verify release version env: RELEASE_PUBLISH: 'true' - run: pnpm run release:verify --family dsh + run: pnpm run release:verify --family dsh ${{ inputs.skip-tag-check && '--skip-tag-check' || '' }} + + - name: Select retained CUA helper + if: inputs.cua_run_id != '' || inputs.cua_run_attempt != '' + id: retained-cua + env: + GH_TOKEN: ${{ github.token }} + run: pnpm exec tsx scripts/cua-helper/select-artifact.ts + + - name: Download retained CUA helper + if: inputs.cua_run_id != '' || inputs.cua_run_attempt != '' + uses: actions/download-artifact@v8.0.1 + with: + artifact-ids: ${{ steps.retained-cua.outputs.artifact-id }} + run-id: ${{ inputs.cua_run_id }} + github-token: ${{ github.token }} + path: dist/cua-helper - name: Build - run: pnpm run build:official + run: pnpm run build:official --serial + + - name: Download freshly built CUA helper + if: inputs.cua_run_id == '' && inputs.cua_run_attempt == '' + uses: actions/download-artifact@v8.0.1 + with: + name: ${{ needs.cua-helper.outputs.artifact }} + path: dist/cua-helper + + - name: Stage verified native helper applications + run: | + mkdir dist/cua-helper/extracted + tar -xzf dist/cua-helper/cua-helper.tgz -C dist/cua-helper/extracted + pnpm exec tsx scripts/cua-helper/stage.ts --from dist/cua-helper/extracted + + - name: Record CUA source and publication retry instructions + env: + CUA_RUN_ID: ${{ inputs.cua_run_id || github.run_id }} + CUA_RUN_ATTEMPT: ${{ steps.retained-cua.outputs.attempt || github.run_attempt }} + run: | + { + printf 'CUA helper: run `%s`, attempt `%s`.\n\n' "$CUA_RUN_ID" "$CUA_RUN_ATTEMPT" + printf '%s\n\n' 'npm 部分发布失败时,只重跑 Publish to npm job,复用原 dsh-npm-tarballs;不要重跑全部 job。' + printf '若需新建发布运行,填写 `cua_run_id=%s`、`cua_run_attempt=%s`,并使用相同源码 commit 和版本。\n' "$CUA_RUN_ID" "$CUA_RUN_ATTEMPT" + } >> "$GITHUB_STEP_SUMMARY" - name: Pack release tarballs run: pnpm run release:pack --family dsh --out dist/npm diff --git a/.github/workflows/windows-package.yml b/.github/workflows/windows-package.yml index cbb4fd2642..29229605de 100644 --- a/.github/workflows/windows-package.yml +++ b/.github/workflows/windows-package.yml @@ -53,11 +53,6 @@ on: type: string default: '' required: false - build_root: - description: '独立构建根目录:填写绝对路径,每次任务创建独立源码副本;留空使用 runner 工作区' - type: string - default: '' - required: false permissions: contents: read @@ -82,7 +77,6 @@ jobs: PACKAGE_TEST_RELEASE_ID: ${{ inputs.test_release_id }} PACKAGE_SIGNING: ${{ inputs.signing }} PACKAGE_CACHE_NAMESPACE: ${{ inputs.cache_namespace }} - PACKAGE_BUILD_ROOT: ${{ inputs.build_root }} PACKAGE_WINDOWS_COMPRESSION_LEVEL: ${{ inputs.compression_level }} steps: - name: Reset packaging checkout @@ -106,7 +100,7 @@ jobs: run: | $helpers = Join-Path $env:RUNNER_TEMP "packaging-helpers-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT" New-Item -ItemType Directory -Path $helpers -ErrorAction Stop | Out-Null - foreach ($name in @('windows-packaging-priority.ps1', 'windows-build-workspace.mjs')) { + foreach ($name in @('windows-packaging-priority.ps1')) { Copy-Item -LiteralPath (Join-Path 'apps/desktop/scripts' $name) -Destination $helpers -ErrorAction Stop } "PACKAGE_CI_HELPERS=$helpers" >> $env:GITHUB_ENV @@ -140,17 +134,6 @@ jobs: - uses: actions/setup-node@v7.0.0 with: node-version: '24' - - name: Select packaging workspace - id: build_workspace - env: - PACKAGE_SOURCE_SHA: ${{ steps.checkout.outputs.commit }} - run: | - if (!$env:PACKAGE_BUILD_ROOT) { - "directory=$env:GITHUB_WORKSPACE" >> $env:GITHUB_OUTPUT - } else { - node "$env:PACKAGE_CI_HELPERS/windows-build-workspace.mjs" - exit $LASTEXITCODE - } - name: Configure persistent pnpm store # PNPM_HOME belongs to this run; keep downloaded packages outside its temporary directory. env: @@ -167,7 +150,6 @@ jobs: "DSH_DESKTOP_PACKAGE_CACHE_DIR=$env:DSH_DESKTOP_PACKAGE_CACHE_DIR" >> $env:GITHUB_ENV "PNPM_CONFIG_STORE_DIR=$storeRoot" >> $env:GITHUB_ENV - name: Install locked dependencies - working-directory: ${{ steps.build_workspace.outputs.directory }} run: | & "$env:PACKAGE_CI_HELPERS/windows-packaging-priority.ps1" -BestEffort node -e "const os=require('node:os'); console.log(JSON.stringify({node:process.version,availableParallelism:os.availableParallelism(),logicalCpus:os.cpus().length,totalMemoryBytes:os.totalmem(),freeMemoryBytes:os.freemem()}))" @@ -178,12 +160,10 @@ jobs: exit $LASTEXITCODE - name: Validate preset and resolve build version id: settings - working-directory: ${{ steps.build_workspace.outputs.directory }} env: PACKAGE_BUILD_VERSION: ${{ inputs.build_version }} run: node apps/desktop/scripts/prepare-windows-ci.mjs - name: Check packaging configuration and toolchain - working-directory: ${{ steps.build_workspace.outputs.directory }} env: PACKAGE_VERSION: ${{ steps.settings.outputs.version }} run: | @@ -192,23 +172,16 @@ jobs: pnpm @packageArgs exit $LASTEXITCODE - name: Build installer and verify packaged runtime - working-directory: ${{ steps.build_workspace.outputs.directory }} env: PACKAGE_VERSION: ${{ steps.settings.outputs.version }} - PACKAGE_BUILD_TEMP: ${{ steps.build_workspace.outputs.temporaryDirectory }} run: | & "$env:PACKAGE_CI_HELPERS/windows-packaging-priority.ps1" -BestEffort - if ($env:PACKAGE_BUILD_TEMP) { - $env:TEMP = $env:PACKAGE_BUILD_TEMP - $env:TMP = $env:PACKAGE_BUILD_TEMP - } $packageArgs = @('run', 'package:desktop:win:x64', '--build-version', $env:PACKAGE_VERSION) if ($env:PACKAGE_SIGNING -eq 'unsigned') { $packageArgs += '--unsigned' } pnpm @packageArgs exit $LASTEXITCODE - name: Retain complete package outside the checkout id: retained - working-directory: ${{ steps.build_workspace.outputs.directory }} env: PACKAGE_SOURCE_SHA: ${{ steps.checkout.outputs.commit }} PACKAGE_VERSION: ${{ steps.settings.outputs.version }} @@ -219,7 +192,7 @@ jobs: with: name: windows-package-logs-${{ github.run_id }}-${{ github.run_attempt }} include-hidden-files: true - path: ${{ steps.build_workspace.outputs.directory || github.workspace }}/apps/desktop/.desktop-build/packaging-runs/ + path: ${{ github.workspace }}/apps/desktop/.desktop-build/packaging-runs/ if-no-files-found: warn retention-days: 14 - name: Retain verified unsigned installer @@ -242,8 +215,3 @@ jobs: path: ${{ steps.retained.outputs.directory }}/* if-no-files-found: error retention-days: 14 - - name: Remove isolated packaging workspace - if: success() && inputs.build_root != '' - env: - PACKAGE_SOURCE_SHA: ${{ steps.checkout.outputs.commit }} - run: node "$env:PACKAGE_CI_HELPERS/windows-build-workspace.mjs" --remove diff --git a/.gitignore b/.gitignore index 3cb86ce87d..4d52487a06 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ worktrees/ .agents/worktrees/ .typert-*/ native/system/test/bin/ + +# Generated company-signed CUA release record. +/packages/experimental/cua-macos/cua-helper.json diff --git a/apps/cli/package.json b/apps/cli/package.json index 81bbe20879..463f4f1b9e 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, @@ -104,9 +104,9 @@ "@deepseek-ai/dsh-experimental-ralph-bundle": "workspace:*", "@deepseek-ai/dsh-experimental-session-titles-bundle": "workspace:*", "@deepseek-ai/dsh-experimental-terminal-bundle": "workspace:*", + "@deepseek-ai/dsh-experimental-liveshare": "workspace:*", "@deepseek-ai/dsh-experimental-session-search": "workspace:*", "@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*", - "@deepseek-ai/dsh-cua-bundle": "workspace:*", "@deepseek-ai/dsh-experimental-plugin-market-bundle": "workspace:*", "@deepseek-ai/dsh-personalized-recommendations": "workspace:*", "@deepseek-ai/dsh-experimental-forge-bundle": "workspace:*" diff --git a/apps/desktop-host/package.json b/apps/desktop-host/package.json index ef3bfd7af1..93469b15f2 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "private": true, "license": "MIT", "type": "module", diff --git a/apps/desktop/config/publishing/macos/production.json.example b/apps/desktop/config/publishing/macos/production.json.example new file mode 100644 index 0000000000..cb7f67a0a4 --- /dev/null +++ b/apps/desktop/config/publishing/macos/production.json.example @@ -0,0 +1,13 @@ +{ + "bucket": "replace-with-bucket-1250000000", + "origin": "https://download.deepseek.com", + "credentialsFile": "/absolute/path/to/packaging/secrets/cos-production.json", + "macOS": { + "signingIdentity": "REPLACE_WITH_DEVELOPER_ID_APPLICATION_IDENTITY", + "teamId": "XXXXXXXXXX" + }, + "githubApp": { + "clientId": "REPLACE_WITH_GITHUB_APP_CLIENT_ID", + "privateKeyFile": "/absolute/path/to/packaging/publishing/keys/desktop-release-tagger.pem" + } +} diff --git a/apps/desktop/config/publishing/macos/test.json.example b/apps/desktop/config/publishing/macos/test.json.example new file mode 100644 index 0000000000..6981a88c6b --- /dev/null +++ b/apps/desktop/config/publishing/macos/test.json.example @@ -0,0 +1,9 @@ +{ + "bucket": "replace-with-bucket-1250000000", + "origin": "https://download-test.example.com", + "credentialsFile": "/absolute/path/to/packaging/secrets/cos-test.json", + "macOS": { + "signingIdentity": "REPLACE_WITH_DEVELOPER_ID_APPLICATION_IDENTITY", + "teamId": "XXXXXXXXXX" + } +} diff --git a/apps/desktop/config/publishing/windows/production.json.example b/apps/desktop/config/publishing/windows/production.json.example new file mode 100644 index 0000000000..9a2a743227 --- /dev/null +++ b/apps/desktop/config/publishing/windows/production.json.example @@ -0,0 +1,12 @@ +{ + "bucket": "replace-with-bucket-1250000000", + "origin": "https://download.deepseek.com", + "credentialsFile": "C:/absolute/path/to/packaging/secrets/cos-production.json", + "windows": { + "certificateThumbprint": "0000000000000000000000000000000000000000" + }, + "githubApp": { + "clientId": "REPLACE_WITH_GITHUB_APP_CLIENT_ID", + "privateKeyFile": "C:/absolute/path/to/packaging/publishing/keys/desktop-release-tagger.pem" + } +} diff --git a/apps/desktop/config/publishing/windows/test.json.example b/apps/desktop/config/publishing/windows/test.json.example new file mode 100644 index 0000000000..cf3e1a40a1 --- /dev/null +++ b/apps/desktop/config/publishing/windows/test.json.example @@ -0,0 +1,8 @@ +{ + "bucket": "replace-with-bucket-1250000000", + "origin": "https://download-test.example.com", + "credentialsFile": "C:/absolute/path/to/packaging/secrets/cos-test.json", + "windows": { + "certificateThumbprint": "0000000000000000000000000000000000000000" + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a39070a78c..081a90ead3 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "private": true, "license": "MIT", "author": { diff --git a/apps/desktop/packaging/README.i18n.yaml b/apps/desktop/packaging/README.i18n.yaml index b98c3d075f..c39bb71bc3 100644 --- a/apps/desktop/packaging/README.i18n.yaml +++ b/apps/desktop/packaging/README.i18n.yaml @@ -24,11 +24,11 @@ en: 5b4f0f80a117f202 zh: ff05be196c181087 /desktop-packaging/windows-actions-packaging: - en: 9dd2227f2878519e - zh: cd95dd6efd48298f + en: 253e17998951931c + zh: b582b377391d2ec2 /desktop-packaging/macos-self-hosted-packaging: - en: ee628e830d849ae9 - zh: 44313f5fb842f042 + en: f4cc5209bdaeefba + zh: 1db959d1ed345fcc /desktop-packaging/unsigned-windows-test-installer: en: f429bc0c280e2dfa zh: 4adbcc9e906bdf80 diff --git a/apps/desktop/packaging/README.md b/apps/desktop/packaging/README.md index 83a0e5a77b..28aa9ff9c7 100644 --- a/apps/desktop/packaging/README.md +++ b/apps/desktop/packaging/README.md @@ -115,11 +115,9 @@ Select the workflow in Actions, choose the source branch, preset, signing mode a Open the completed run in Actions and download its Artifacts. `windows-x64-signed---` and `windows-x64-unsigned---` contain the corresponding EXE; `windows-package-logs--` contains packaging diagnostics. The upload steps are `Retain verified signed installer`, `Retain verified unsigned installer`, and `Retain packaging diagnostics`; their step durations and timestamped logs show upload time. These artifacts are not a Release or download-site publication. -For path-sensitive Windows build measurements, `build_root` optionally selects an absolute directory outside existing Git checkouts. Each run clones the validated source commit into a new `windows-package--/source` directory; ignored files and previous build outputs are not copied. Only the packaging process uses a sibling run-owned `temp` directory through `TEMP` and `TMP`; Actions steps and post-job hooks retain their original temporary directories. Dependency and signing caches retain their existing locations. Successful runs remove only their owned checkout after artifact uploads, retrying transient filesystem failures up to three times with 100 ms increasing delays; persistent cleanup errors fail the Job and may leave a partially removed directory. Failed builds retain their checkout for inspection. Leave the input empty to build in the original Actions checkout. Include checkout creation and cleanup in the complete Job timing. - For cache measurements, `cache_namespace` selects a retained subdirectory for both dependency stores and upstream downloads; leave it empty for normal shared caches. Use a new name for a cold sample and repeat that name for warm samples. Names contain 1–64 lowercase letters, digits or hyphens, start with a letter or digit, and cannot be Windows device names. This does not reset the separate signature cache or OS file cache; report those states and Job start-to-completion time alongside phase timings and artifact size. Before dependency installation, the workflow records the Node version, available CPU parallelism, logical CPU count, and total/free memory. -The dependency-install and packaging steps normalize their own shell to normal CPU, I/O and memory priority before launching children. Priority and isolated-workspace helpers are copied from the exact workflow commit into a run-specific temporary directory, independently of the selected product source. Missing helpers stop CI setup; unavailable priority adjustment emits a warning and marks scheduling unsuitable for performance comparison, but does not stop packaging. This does not modify the runner task or other processes. Version, preset and retained-artifact adapters remain tied to product source and its formats. +The dependency-install and packaging steps normalize their own shell to normal CPU, I/O and memory priority before launching children. The priority helper is copied from the exact workflow commit into a run-specific temporary directory, independently of the selected product source. A missing helper stops CI setup; unavailable priority adjustment emits a warning and marks scheduling unsuitable for performance comparison, but does not stop packaging. This does not modify the runner task or other processes. Version, preset and retained-artifact adapters remain tied to product source and its formats. Installer uploads store the already-compressed release files without ZIP recompression; this preserves installer bytes and signatures. Diagnostic uploads retain their ordinary compression. @@ -129,13 +127,12 @@ Installer uploads store the already-compressed release files without ZIP recompr The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG. -All three platform forms put source, environment, update channel, version mode, manual version, test release ID and compression level first, with matching defaults. macOS notarization and Windows signing, cache measurement and isolated-workspace options follow these common fields. +All three platform forms put source, environment, update channel, version mode, manual version, test release ID and compression level first, with matching defaults. macOS notarization and Windows signing and cache measurement options follow these common fields. Run `node apps/desktop/scripts/setup-macos-packaging.mjs "$HOME/dsh-ci"` to prepare private `runner/`, `packaging/{presets,secrets,certificates}` and `cache/macos-signatures` directories without overwriting existing files; append `--check` to inspect them. Fill the [preset](../config/macos-packaging/test.env.example) and [signing](../config/macos-packaging/macos-signing.env.example) templates outside the checkout. Set secret and certificate files to mode `0600`. `DSH_PACKAGING_CONFIG_DIR` selects the absolute `packaging` directory and `DSH_PACKAGING_PRESET` selects its preset; missing files fail without falling back to `.env.macos`. Signed and preparation-only Mac builds read `secrets/macos-signing.env`; relative credential paths resolve beside that file. Public presets reject signing credentials, and signing files cannot override public settings. Local unsigned Mac builds retain their configuration-free behavior. Run `bash apps/desktop/scripts/start-macos-packaging-runner.sh "$HOME/dsh-ci/runner" "$HOME/dsh-ci/packaging" --check` to check the host tools and configuration files. The script selects the active Xcode and its macOS SDK together and compiles a small linker probe. After registration, omit `--check` to run in the foreground with a cleared inherited environment and prevent idle sleep while the runner is alive. Stop it with Ctrl-C before installing the service below. The runner owns its `_work` checkout, separate from daily development. The account can read its signing files, so run only trusted code. The existing notarization proxy restoration rules apply; after forced termination, inspect retained recovery records before another build. New setup templates set `DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR` to the absolute `cache/macos-signatures` directory outside checkout. For existing installations, rerun setup and add that field to each preset; existing files are preserved. This retains signatures across clean checkouts; cache verification and limits are described under [macOS signing](#macos-release-signing). - ZIP compression uses the same configured level with or without notarization; the App is assembled once, then DMG and ZIP are built separately so the ZIP setting does not change DMG compression. The `skip_notarization` workflow option defaults to `false`; the equivalent CLI flag is `--skip-notarization`. It retains Developer ID signing, DMG signature verification and packaged-runtime verification, but omits Apple notarization and stapling and does not require Apple notarization credentials. DMG/ZIP filenames carry `-unnotarized`, are stored under `.desktop-build/targets//unnotarized-artifacts/`, and are uploaded with an `unnotarized-debug` label. These artifacts are for internal debugging only: Gatekeeper may block downloaded copies, and they are not qualified for distribution or update publishing. No release completion record is written. The flag rejects unsigned, Windows and preparation-only builds. Install the user service from the checkout as the same account that registered the runner, without `sudo`: diff --git a/apps/desktop/packaging/README.zh.md b/apps/desktop/packaging/README.zh.md index 3868508c9e..2e87933826 100644 --- a/apps/desktop/packaging/README.zh.md +++ b/apps/desktop/packaging/README.zh.md @@ -115,11 +115,9 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s 在 Actions 打开完成的运行并下载 Artifacts。`windows-x64-signed---` 与 `windows-x64-unsigned---` 包含对应 EXE;`windows-package-logs--` 包含打包诊断。上传步骤名为 `Retain verified signed installer`、`Retain verified unsigned installer` 和 `Retain packaging diagnostics`;步骤耗时与带时间戳的日志显示上传用时。这些产物不等于发布 Release 或下载站内容。 -测量 Windows 构建路径对性能的影响时,可用 `build_root` 指定现有 Git checkout 之外的绝对目录。每轮任务将已验证的源码提交克隆到新的 `windows-package--/source` 目录;不会复制忽略文件或此前的构建输出。仅打包进程通过 `TEMP` 和 `TMP` 使用同轮所属的同级 `temp` 目录;Actions 步骤及 Job 后置钩子保留原临时目录。依赖缓存和签名缓存沿用原有位置。成功任务在产物上传后仅删除本轮所属的 checkout,临时文件系统错误最多重试三次,等待时间按 100 ms 递增;持续清理失败会使 Job 失败,并可能留下部分删除的目录。构建失败时保留 checkout 供检查。输入留空时使用原 Actions checkout。完整 Job 耗时须包含 checkout 创建和清理。 - 测量缓存时,`cache_namespace` 为两个依赖 store 和上游下载选择保留的子目录;留空使用正常共享缓存。新名称用于冷缓存采样,再次使用同名用于热缓存采样。名称由 1–64 个小写字母、数字或连字符组成,以字母或数字开头,且不能为 Windows 设备名。这不会重置独立的签名缓存或操作系统文件缓存;应将其状态、Job 开始至完成的总耗时与阶段耗时和产物体积一并报告。workflow 在依赖安装前记录 Node 版本、可用 CPU 并行数、逻辑 CPU 数及总内存和空闲内存。 -依赖安装和打包步骤在启动子进程前,将自身 shell 的 CPU、I/O 和内存优先级调整为普通值。优先级及独立构建目录辅助脚本从 workflow 的精确提交复制到本轮临时目录,与选中的产品源码分离。缺失辅助脚本会终止 CI 准备;无法调整优先级时明确告警并标记调度条件不适合性能对比,但继续打包。这不会修改 runner 任务或其他进程。版本、preset 和产物留存适配器仍跟随产品源码及其格式。 +依赖安装和打包步骤在启动子进程前,将自身 shell 的 CPU、I/O 和内存优先级调整为普通值。优先级辅助脚本从 workflow 的精确提交复制到本轮临时目录,与选中的产品源码分离。缺失辅助脚本会终止 CI 准备;无法调整优先级时明确告警并标记调度条件不适合性能对比,但继续打包。这不会修改 runner 任务或其他进程。版本、preset 和产物留存适配器仍跟随产品源码及其格式。 安装器上传直接存储已经压缩的发布文件,不再进行 ZIP 二次压缩;安装器字节和签名保持不变。诊断日志上传保留通常的压缩方式。 @@ -129,13 +127,12 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s [Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。 -三个平台的表单均依次展示源码、环境、升级通道、版本模式、手动版本号、测试 release ID 和压缩等级,默认值一致。macOS 公证选项和 Windows 签名、缓存测量、独立构建目录选项位于公共字段之后。 +三个平台的表单均依次展示源码、环境、升级通道、版本模式、手动版本号、测试 release ID 和压缩等级,默认值一致。macOS 公证选项和 Windows 签名、缓存测量选项位于公共字段之后。 运行 `node apps/desktop/scripts/setup-macos-packaging.mjs "$HOME/dsh-ci"` 创建私有 `runner/`、`packaging/{presets,secrets,certificates}` 和 `cache/macos-signatures` 目录,不覆盖已有文件;追加 `--check` 仅检查。在 checkout 外填写 [preset](../config/macos-packaging/test.env.example) 和[签名](../config/macos-packaging/macos-signing.env.example)模板,将秘密和证书文件权限设为 `0600`。`DSH_PACKAGING_CONFIG_DIR` 指定 `packaging` 的绝对目录,`DSH_PACKAGING_PRESET` 选择其中的 preset;缺少文件立即失败,不回退到 `.env.macos`。Mac 签名和仅准备构建读取 `secrets/macos-signing.env`,相对凭据路径以该文件所在目录为基准。公开 preset 拒绝签名凭据,签名文件不能覆盖公开配置。本地未签名 Mac 构建保留无需配置的行为。 运行 `bash apps/desktop/scripts/start-macos-packaging-runner.sh "$HOME/dsh-ci/runner" "$HOME/dsh-ci/packaging" --check` 检查宿主工具和配置文件。脚本同时选择当前 Xcode 及其 macOS SDK,并编译小型链接探针。注册后省略 `--check`,清理继承环境并在前台启动 runner,存活期间阻止空闲睡眠。安装下方服务前,先用 Ctrl-C 停止。runner 拥有 `_work` 下的源码目录,与日常开发分开。该账户可读取签名文件,因此只执行可信代码。沿用现有公证代理恢复规则;强制终止后,先检查保留的恢复记录再构建。新建模板将 `DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR` 设为 checkout 外 `cache/macos-signatures` 的绝对路径。已有部署重新运行 setup 后,在各 preset 中补上该字段;已有文件保持不变。这样可跨干净检出保留签名;缓存验证和容量限制见 [macOS 签名](#macos-release-signing)。 - 无论是否公证,ZIP 都使用同一压缩配置;App 只组装一次,再分别生成 DMG 和 ZIP,确保 ZIP 设置不改变 DMG 压缩。工作流选项 `skip_notarization` 默认为 `false`;对应 CLI 参数是 `--skip-notarization`。该模式保留 Developer ID 签名、DMG 签名验证和打包运行时验证,跳过 Apple 公证和 stapling,不要求 Apple 公证凭据。DMG/ZIP 文件名带 `-unnotarized`,存放于 `.desktop-build/targets//unnotarized-artifacts/`,上传产物标为 `unnotarized-debug`。这些产物仅供内部调试:Gatekeeper 可能阻止下载副本运行,不能视为可分发包或用于更新发布。该模式不写入发布完成记录,不能与未签名、Windows 或仅准备构建组合。 在 checkout 中,以注册 runner 的同一账户安装用户服务,不使用 `sudo`: diff --git a/apps/desktop/publishing/ci/README.i18n.yaml b/apps/desktop/publishing/ci/README.i18n.yaml index d6940d4d14..14ef478ed7 100644 --- a/apps/desktop/publishing/ci/README.i18n.yaml +++ b/apps/desktop/publishing/ci/README.i18n.yaml @@ -21,14 +21,14 @@ en: 0478d8e35f9a3d55 zh: 0aa3fd828bc02691 /desktop-ci-publication/machine-setup: - en: 6e7b928af17f4377 - zh: 7c559078306df3fd + en: 0f43f2e193e95043 + zh: 1c1efb062c06f7b8 /desktop-ci-publication/package-and-publish: en: f7c12d343891e547 zh: da596ee1366c2046 /desktop-ci-publication/recovery-and-records: - en: 2e1e6b16d0d6bd41 - zh: 6ff4ad5a8f1f57b4 + en: b62a7484929f9941 + zh: c9dd06c77098e6ef /desktop-ci-publication/restore-a-channel: en: d82e2ccf70cce397 zh: d93dcb77cc4b463e diff --git a/apps/desktop/publishing/ci/README.md b/apps/desktop/publishing/ci/README.md index 02f1e1b6af..5009bf87e7 100644 --- a/apps/desktop/publishing/ci/README.md +++ b/apps/desktop/publishing/ci/README.md @@ -54,7 +54,9 @@ For a queued job, compare the job page's requested labels with **Settings → Ac Keep `DSH_PACKAGING_CONFIG_DIR` pointed at the existing machine packaging directory. Completed attempts are copied to its sibling `releases///`; `DSH_DESKTOP_RELEASES_DIR` overrides that root with an absolute path. The inventory includes installers, update archives, blockmaps, completion metadata and a credential-free manifest. Debug packages are retained but cannot publish. Copies are checked before the attempt directory becomes visible, and an existing attempt cannot be replaced. GitHub retains the same file inventory for 14 days. Local files remain until an operator removes them; do not delete an attempt while it is being verified or published. -Create `publishing/test.json` and, when authorized, `publishing/production.json` below the configuration directory. Each declares `bucket`, HTTPS `origin` without a path, and an absolute `credentialsFile`. The credential file contains only `secretId` and `secretKey`. macOS also requires `macOS.signingIdentity` and `macOS.teamId`; Windows requires `windows.certificateThumbprint`. Production requires the exact origin `https://download.deepseek.com` and `githubApp.clientId` plus an absolute `githubApp.privateKeyFile`. These files stay outside Git. The uploader supports permanent COS credentials, not STS session credentials. Administrators must keep test and production buckets unversioned (neither enabled nor suspended). Publication assumes this deployment prerequisite and does not call `GetBucketVersioning` or require its permission. Immutable binary PUTs retain `x-cos-forbid-overwrite`; their protection depends on administrators maintaining the bucket configuration. +Start from the [macOS test](../../config/publishing/macos/test.json.example), [macOS production](../../config/publishing/macos/production.json.example), [Windows test](../../config/publishing/windows/test.json.example), or [Windows production](../../config/publishing/windows/production.json.example) template. Create the `publishing/` directory and copy the selected file to `publishing/.json` below `DSH_PACKAGING_CONFIG_DIR`. Replace the example bucket, test origin, absolute paths, macOS signing identity and team ID or Windows certificate thumbprint, and production App client ID with the machine settings; keep the fixed production origin. Templates contain no credentials and do not grant COS, GitHub, or signing permissions. Store referenced secrets outside Git with access restricted to the publishing account. macOS uses the same configuration for arm64 and x64; the retained package determines the target. + +Each configuration declares `bucket`, HTTPS `origin` without a path, and an absolute `credentialsFile`. The credential file contains only `secretId` and `secretKey`. macOS also requires `macOS.signingIdentity` and `macOS.teamId`; Windows requires `windows.certificateThumbprint`. Production requires the exact origin `https://download.deepseek.com` and `githubApp.clientId` plus an absolute `githubApp.privateKeyFile`. The uploader supports permanent COS credentials, not STS session credentials. Administrators must keep test and production buckets unversioned (neither enabled nor suspended). Publication assumes this deployment prerequisite and does not call `GetBucketVersioning` or require its permission. Immutable binary PUTs retain `x-cos-forbid-overwrite`; their protection depends on administrators maintaining the bucket configuration. Set the repository variable `DESKTOP_PUBLISH_RUNNERS` to a JSON mapping of registered runner names to unique labels and targets, for example `{"mini":{"label":"dsh-mini-publish","target":["mac-arm64","mac-x64"]}}`. Assign that label to exactly that runner. A target may be a single name or a nonempty list; the Mac entry above authorizes both packaging workflows on the same machine. Windows uses target `win-x64`. The retained target must match the selected packaging workflow. The routing job checks the original successful job, and the publisher checks its own runner name against the recorded name. Machines need no shared filesystem or LAN connection to each other. @@ -82,7 +84,7 @@ After resolving a failure, re-run the failed publishing job to keep its prepared The sibling `upload-records/` directory retains an operation identity, newline-delimited events and terminal result. Feed-write intents retain both previous and requested YAML. Failures record their stage, safe error classification and diagnostic; COS failures also identify `GetObject` or `PutObject`, plus a validated HTTP status and request ID when available. Access-denied reports name the permission to inspect. Raw provider responses and credentials are excluded. Audit initialization becomes visible only after its files are complete; abandoned `.initializing-*` directories are not scanned for completed publication. Writes and readback are recorded before proceeding; missing terminal results mean interruption, not success. Actions also keeps its operation record as a diagnostic artifact for 30 days. These local records are not tamper-proof. COS readback verifies storage bytes, not public CDN behavior; public download acceptance is separate. -If the feed completed but tagging failed, the result is `published-tag-pending` and the job fails visibly. On retry, durable feed-completion evidence permits only tag recovery, even if a later release has advanced the channel. It never re-publishes old YAML. An existing conflicting tag requires administrator investigation and is never moved automatically. +If the feed completed but tagging failed, the result is `published-tag-pending` and the job fails visibly. On retry, durable feed-completion evidence permits only tag recovery, even if a later release has advanced the channel. It never re-publishes old YAML. An existing conflicting tag requires administrator investigation and is never moved automatically. A rejected tag creation retains its HTTP status and validated GitHub request ID; a follow-up lookup does not replace that failure with a missing-tag error. A repository-rule rejection directs the operator to the matching rulesets without logging the raw response. Normal publication and manual restoration acquire the same local channel lock. A crash can leave a lock directory under the sibling `publish-locks/`; verify no writer is running before manually removing it. A runner label alone does not coordinate writers on other machines. Keep one publishing machine per channel and preserve its records when migrating. diff --git a/apps/desktop/publishing/ci/README.zh.md b/apps/desktop/publishing/ci/README.zh.md index 3a635b5116..151032eed9 100644 --- a/apps/desktop/publishing/ci/README.zh.md +++ b/apps/desktop/publishing/ci/README.zh.md @@ -54,7 +54,9 @@ job 排队时,先对照 job 页面请求的标签与 **Settings → Actions 保持 `DSH_PACKAGING_CONFIG_DIR` 指向现有机器打包目录。完成的构建复制到其同级 `releases///`;`DSH_DESKTOP_RELEASES_DIR` 可用绝对路径覆盖该根目录。文件清单包含安装包、更新归档、blockmap、完成记录和不含凭据的 manifest。调试包会留存,但不能发布。复制内容通过校验后才开放 attempt 目录,已有 attempt 不能被替换。GitHub 保留相同文件清单 14 天。本地文件保留到操作员删除;校验或发布期间不要删除对应 attempt。 -在配置目录下创建 `publishing/test.json`,获得授权后再创建 `publishing/production.json`。每份配置声明 `bucket`、不带路径的 HTTPS `origin` 和绝对路径 `credentialsFile`。凭据文件只包含 `secretId` 和 `secretKey`。macOS 还需 `macOS.signingIdentity` 和 `macOS.teamId`;Windows 需要 `windows.certificateThumbprint`。production 必须使用 origin `https://download.deepseek.com`,并提供 `githubApp.clientId` 和绝对路径 `githubApp.privateKeyFile`。这些文件不进入 Git。上传器支持长期 COS 凭据,不支持 STS 会话凭据。管理员必须维持 test 和 production bucket 未启用版本控制(不能是已启用或已暂停状态)。发布流程以此为部署前提,不调用 `GetBucketVersioning`,也不要求该权限。二进制 PUT 仍携带 `x-cos-forbid-overwrite`;防覆盖保证依赖管理员维持桶配置。 +从 [macOS test](../../config/publishing/macos/test.json.example)、[macOS production](../../config/publishing/macos/production.json.example)、[Windows test](../../config/publishing/windows/test.json.example) 或 [Windows production](../../config/publishing/windows/production.json.example) 模板开始,创建 `publishing/` 目录,将所选文件复制到 `DSH_PACKAGING_CONFIG_DIR` 下的 `publishing/.json`。用机器实际配置替换示例 bucket、test origin、绝对路径、macOS 签名身份和 Team ID 或 Windows 证书指纹,以及 production App client ID;保留固定的 production origin。模板不含凭据,也不授予 COS、GitHub 或签名权限。引用的秘密文件保存在 Git 之外,只允许发布账户访问。macOS 的 arm64 和 x64 共用配置,目标由留存包决定。 + +每份配置声明 `bucket`、不带路径的 HTTPS `origin` 和绝对路径 `credentialsFile`。凭据文件只包含 `secretId` 和 `secretKey`。macOS 还需 `macOS.signingIdentity` 和 `macOS.teamId`;Windows 需要 `windows.certificateThumbprint`。production 必须使用 origin `https://download.deepseek.com`,并提供 `githubApp.clientId` 和绝对路径 `githubApp.privateKeyFile`。上传器支持长期 COS 凭据,不支持 STS 会话凭据。管理员必须维持 test 和 production bucket 未启用版本控制(不能是已启用或已暂停状态)。发布流程以此为部署前提,不调用 `GetBucketVersioning`,也不要求该权限。二进制 PUT 仍携带 `x-cos-forbid-overwrite`;防覆盖保证依赖管理员维持桶配置。 将仓库变量 `DESKTOP_PUBLISH_RUNNERS` 配置为已注册 runner 名称到唯一标签和目标的 JSON 映射,例如 `{"mini":{"label":"dsh-mini-publish","target":["mac-arm64","mac-x64"]}}`。该标签只能分配给这台 runner。目标可为单个名称或非空列表;上述 Mac 条目允许同一台机器处理两种架构的打包工作流。Windows 使用目标 `win-x64`。留存包的目标必须与所选打包工作流一致。路由 job 检查原构建成功的 job,发布器再检查自身 runner 名称与记录一致。机器之间不需要共享文件系统或局域网连接。 @@ -82,7 +84,7 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任 同级 `upload-records/` 目录保留操作标识、逐行事件和最终结果。频道写入意图同时记录先前和目标 YAML。失败记录阶段、安全错误分类与诊断;COS 失败还标明 `GetObject` 或 `PutObject`,并在可用时记录经过校验的 HTTP 状态码和 request ID。访问拒绝提示会指出需检查的权限。不保存原始服务响应或凭据。审计初始化在文件完整后才可见;遗留的 `.initializing-*` 目录不参与已完成发布的扫描。写入和回读在继续操作前落盘;没有最终结果代表中断,不代表成功。Actions 还将操作记录保存为诊断 artifact,保留 30 天。这些本地记录不防篡改。COS 回读验证存储字节,不代表公开 CDN 行为;公开下载需单独验收。 -频道更新完成但 tag 失败时,结果为 `published-tag-pending`,job 明确失败。重试时,已落盘的频道完成证据只允许恢复 tag,即使后来发布已推进频道,也不会重新发布旧 YAML。已有冲突 tag 需要管理员调查,不会自动移动。 +频道更新完成但 tag 失败时,结果为 `published-tag-pending`,job 明确失败。重试时,已落盘的频道完成证据只允许恢复 tag,即使后来发布已推进频道,也不会重新发布旧 YAML。已有冲突 tag 需要管理员调查,不会自动移动。tag 创建被拒绝时保留其 HTTP 状态及经过校验的 GitHub request ID,后续查询不会用 tag 不存在的错误替换创建失败。仓库规则拒绝会提示操作人员检查匹配的 ruleset,不记录原始响应。 正常发布和手动恢复获取同一把本地频道锁。崩溃可能在同级 `publish-locks/` 下留下锁目录;手动删除前确认没有写入任务运行。runner 标签本身不能协调其他机器的写入。每个频道保持单一发布机器,迁移时保留记录。 diff --git a/apps/desktop/scripts/desktop-publish-tag.ts b/apps/desktop/scripts/desktop-publish-tag.ts index f47cc698a6..de60c03e84 100644 --- a/apps/desktop/scripts/desktop-publish-tag.ts +++ b/apps/desktop/scripts/desktop-publish-tag.ts @@ -40,8 +40,32 @@ export async function publishDesktopTag( body: JSON.stringify({ ref: `refs/tags/${tag}`, sha: commit }), redirect: 'error', signal: AbortSignal.timeout(30_000), }) if (response.status === 201) { await matches(response); return 'created' } - // A parallel platform release may have created the same version tag after our first read. - if (response.status === 422) { await response.body?.cancel(); await matches(await get()); return 'present' } + const requestId = response.headers.get('x-github-request-id') + const identity = requestId !== null && /^[A-Fa-f0-9:]{1,128}$/u.test(requestId) ? `; GitHub request ${requestId}` : '' + const failure = `desktop tag: creation failed (HTTP ${response.status}${identity})` + // A 422 can reject repository rules as well as report a concurrent tag creation. + if (response.status === 422) { + let detail: unknown + try { detail = await response.json() } catch (error) { + // Non-JSON error responses still retain the original HTTP status and request ID. + } + const rulesRejected = typeof detail === 'object' && detail !== null && 'message' in detail + && typeof detail.message === 'string' && detail.message.startsWith('Repository rule violations found') + const guidance = rulesRejected ? 'repository rules rejected tag creation; inspect the matching tag rulesets' + : 'inspect repository rule insights and App permissions before retrying' + let reconciled: Response + try { reconciled = await get() } catch (cause) { + throw new Error(`${failure}; follow-up tag lookup failed; ${guidance}`, { cause }) + } + if (!reconciled.ok) { + await reconciled.body?.cancel() + throw new Error(`${failure}; follow-up tag lookup returned HTTP ${reconciled.status}; ${guidance}`) + } + try { await matches(reconciled) } catch (cause) { + throw new Error(`${failure}; follow-up tag verification failed; inspect the existing tag before retrying`, { cause }) + } + return 'present' + } await response.body?.cancel() - throw new Error(`desktop tag: creation failed (HTTP ${response.status}); reconcile the tag before retrying`) + throw new Error(`${failure}; reconcile the tag before retrying`) } diff --git a/apps/desktop/scripts/windows-build-workspace.d.mts b/apps/desktop/scripts/windows-build-workspace.d.mts deleted file mode 100644 index 92f3a3c704..0000000000 --- a/apps/desktop/scripts/windows-build-workspace.d.mts +++ /dev/null @@ -1,19 +0,0 @@ -/** Identity of a Windows packaging run and its independent build root. */ -export interface WindowsBuildWorkspaceOptions { - sourceDirectory: string - buildRoot: string - runId: string - runAttempt: string - sourceSha: string -} -/** - * Clone the clean selected commit into a new run-owned directory. - * @param options - Original checkout and Actions run identity. - * @returns Checkout and temporary directory owned by this run. - */ -export function prepareWindowsBuildWorkspace(options: WindowsBuildWorkspaceOptions): { directory: string; temporaryDirectory: string } -/** - * Remove the directory only when its ownership record matches the run. - * @param options - Original checkout and Actions run identity. - */ -export function removeWindowsBuildWorkspace(options: WindowsBuildWorkspaceOptions): void diff --git a/apps/desktop/scripts/windows-build-workspace.mjs b/apps/desktop/scripts/windows-build-workspace.mjs deleted file mode 100644 index 977dda6618..0000000000 --- a/apps/desktop/scripts/windows-build-workspace.mjs +++ /dev/null @@ -1,96 +0,0 @@ -/** Create and remove a run-owned checkout for Windows packaging. */ -import { execFileSync } from 'node:child_process' -import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { dirname, isAbsolute, join, parse, resolve } from 'node:path' -import { pathToFileURL } from 'node:url' - -function assertUnlinkedAncestors(directory) { - for (let current = directory; ; current = dirname(current)) { - if (existsSync(current) && lstatSync(current).isSymbolicLink()) { - throw new Error('Packaging workspace ancestors must not be links') - } - if (dirname(current) === current) break - } -} - -function locations(options) { - const { buildRoot, runId, runAttempt, sourceSha } = options - if (!buildRoot || !isAbsolute(buildRoot) || /[\r\n\0]/.test(buildRoot)) { - throw new Error('Packaging build root must be a nonempty absolute directory') - } - const root = resolve(buildRoot) - if (root === parse(root).root) throw new Error('Packaging build root must not be a volume root') - if (!/^[1-9]\d*$/.test(runId) || !/^[1-9]\d*$/.test(runAttempt) || !/^[a-f0-9]{40}$/.test(sourceSha)) { - throw new Error('Packaging workspace requires a run identity and exact commit') - } - assertUnlinkedAncestors(root) - for (let current = root; ; current = dirname(current)) { - if (existsSync(join(current, '.git'))) throw new Error('Packaging build root must be outside existing Git checkouts') - if (dirname(current) === current) break - } - const owner = join(root, `windows-package-${runId}-${runAttempt}`) - assertUnlinkedAncestors(owner) - return { root, owner, directory: join(owner, 'source'), temporaryDirectory: join(owner, 'temp') } -} - -function git(directory, args) { - return execFileSync('git', args, { cwd: directory, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() -} - -function ownership(options, paths) { - return JSON.stringify({ version: 1, root: paths.root, source: resolve(options.sourceDirectory), runId: options.runId, runAttempt: options.runAttempt, sourceSha: options.sourceSha }) -} - -/** - * Clone the validated commit into a new run-owned checkout; failures retain diagnostics. - * @param {{sourceDirectory: string, buildRoot: string, runId: string, runAttempt: string, sourceSha: string}} options - Source and run identity. - * @returns {{directory: string, temporaryDirectory: string}} Checkout and temporary directory owned by this run. - */ -export function prepareWindowsBuildWorkspace(options) { - const paths = locations(options) - if (git(options.sourceDirectory, ['rev-parse', 'HEAD']) !== options.sourceSha || git(options.sourceDirectory, ['status', '--porcelain'])) { - throw new Error('Packaging source must be clean and match the selected commit') - } - mkdirSync(paths.root, { recursive: true }) - mkdirSync(paths.owner) - writeFileSync(join(paths.owner, 'owner.json'), ownership(options, paths), { flag: 'wx' }) - mkdirSync(paths.temporaryDirectory) - git(paths.owner, ['clone', '--no-hardlinks', '--no-checkout', resolve(options.sourceDirectory), paths.directory]) - git(paths.directory, ['checkout', '--detach', options.sourceSha]) - if (git(paths.directory, ['rev-parse', 'HEAD']) !== options.sourceSha || git(paths.directory, ['status', '--porcelain'])) { - throw new Error('Isolated packaging checkout does not match the selected commit') - } - return { directory: paths.directory, temporaryDirectory: paths.temporaryDirectory } -} - -/** - * Remove only the matching run-owned directory after successful artifact upload. - * Transient filesystem failures retry up to three times; persistent failures propagate. - * @param {{sourceDirectory: string, buildRoot: string, runId: string, runAttempt: string, sourceSha: string}} options - Original source and run identity. - * @returns {void} - */ -export function removeWindowsBuildWorkspace(options) { - const paths = locations(options) - const marker = join(paths.owner, 'owner.json') - const stat = lstatSync(marker) - if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 8192 || readFileSync(marker, 'utf8') !== ownership(options, paths)) { - throw new Error('Packaging workspace ownership does not match this run') - } - rmSync(paths.owner, { recursive: true, maxRetries: 3, retryDelay: 100 }) -} - -if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { - const options = { - sourceDirectory: process.env.GITHUB_WORKSPACE ?? '', - buildRoot: process.env.PACKAGE_BUILD_ROOT ?? '', - runId: process.env.GITHUB_RUN_ID ?? '', - runAttempt: process.env.GITHUB_RUN_ATTEMPT ?? '', - sourceSha: process.env.PACKAGE_SOURCE_SHA ?? '', - } - if (process.argv[2] === '--remove') removeWindowsBuildWorkspace(options) - else { - if (process.argv.length !== 2 || !process.env.GITHUB_OUTPUT) throw new Error('Expected Actions output file and no arguments') - const result = prepareWindowsBuildWorkspace(options) - appendFileSync(process.env.GITHUB_OUTPUT, `directory=${result.directory}\ntemporaryDirectory=${result.temporaryDirectory}\n`) - } -} diff --git a/apps/desktop/tests/cua-optional-package.spec.ts b/apps/desktop/tests/cua-optional-package.spec.ts index 3f3f7ad572..85ef15f0ee 100644 --- a/apps/desktop/tests/cua-optional-package.spec.ts +++ b/apps/desktop/tests/cua-optional-package.spec.ts @@ -1,4 +1,4 @@ -/** Cold pnpm requests exercise shipped CUA dependency/platform metadata with inert package contents. */ +/** Cold pnpm requests exercise on-demand CUA dependency/platform metadata with inert package contents. */ import { createHash } from 'node:crypto' import { createServer } from 'node:http' import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' @@ -37,18 +37,20 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for const cli = await readManifest('apps/cli') const bundle = await readManifest('packages/experimental/cua-bundle') const native = await readManifest('packages/experimental/cua-macos') - expect(cli.dependencies).toHaveProperty(bundleName) + expect(cli.dependencies).not.toHaveProperty(bundleName) expect(bundle.dependencies).not.toHaveProperty(providerName) expect(bundle.optionalDependencies).toEqual({ [providerName]: 'workspace:*' }) expect(native.os).toEqual(['darwin']) const version = '1.0.0' const bundleDependencies = Object.fromEntries(Object.keys(bundle.dependencies as Record).map(name => [name, version])) const manifests = [ - { name: cli.name, version, dependencies: { [bundleName]: version } }, + { name: cli.name, version }, { name: bundleName, version, dependencies: bundleDependencies, optionalDependencies: { [providerName]: version } }, ...Object.keys(bundleDependencies).map(name => ({ name, version })), { name: providerName, version, os: native.os, files: native.files }, ] + const generation = 'a'.repeat(64) + const nativePayload = `native/bin/darwin-arm64/${generation}/DSH Computer Use.app/Contents/MacOS/dsh-cua-native` const packages = new Map; bytes: Buffer }>() for (const manifest of manifests) { const name = String(manifest.name) @@ -56,18 +58,20 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for await mkdir(directory) await writeFile(join(directory, 'package.json'), JSON.stringify(manifest)) if (name === providerName) { - await mkdir(join(directory, 'native/bin/darwin-arm64'), { recursive: true }) + await mkdir(join(directory, nativePayload, '..'), { recursive: true }) for (const file of ['.gitignore', '.npmignore']) { await writeFile(join(directory, 'native', file), await readFile(join(repository, 'packages/experimental/cua-macos/native', file))) } - await writeFile(join(directory, 'native/bin/darwin-arm64/dsh-cua-native'), 'inert native payload') + await writeFile(join(directory, 'native/bin/darwin-arm64/current.json'), JSON.stringify({ bundle: generation })) + await writeFile(join(directory, nativePayload), 'inert native payload') } await command(['pack', '--out', 'package.tgz'], directory) const bytes = await readFile(join(directory, 'package.tgz')) packages.set(name, { manifest, bytes }) if (name === providerName) { const result = await execa('tar', ['-tzf', join(directory, 'package.tgz')]) - expect(result.stdout).toContain('package/native/bin/darwin-arm64/dsh-cua-native') + expect(result.stdout).toContain('package/native/bin/darwin-arm64/current.json') + expect(result.stdout).toContain(`package/${nativePayload}`) } } const requests: string[] = [] @@ -98,13 +102,13 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve) }) const address = server.address() if (!address || typeof address === 'string') throw new Error('fixture registry has no TCP address') - for (const target of ['darwin', 'win32', 'linux', 'linux-unrestricted']) { + for (const target of ['darwin-base', 'darwin', 'win32', 'linux', 'linux-unrestricted']) { unrestricted = target === 'linux-unrestricted' const project = join(root, target) await mkdir(project) - await writeFile(join(project, 'package.json'), JSON.stringify({ private: true, dependencies: { [String(cli.name)]: version } })) + await writeFile(join(project, 'package.json'), JSON.stringify({ private: true, dependencies: { [target === 'darwin-base' ? String(cli.name) : bundleName]: version } })) await writeFile(join(project, 'pnpm-workspace.yaml'), [ - 'autoInstallPeers: false', 'supportedArchitectures:', ` os: [${unrestricted ? 'linux' : target}]`, + 'autoInstallPeers: false', 'supportedArchitectures:', ` os: [${unrestricted ? 'linux' : target === 'darwin-base' ? 'darwin' : target}]`, ' cpu: [arm64]', 'minimumReleaseAge: 0', '', ].join('\n')) requests.length = 0 @@ -113,8 +117,8 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for `--config.registry=http://127.0.0.1:${address.port}/`, `--config.store-dir=${join(project, 'store')}`, `--config.cache-dir=${join(project, 'cache')}`, `--config.state-dir=${join(project, 'state')}`, ], project) - expect(requests).toContain(`/${bundleName}.tgz`) - expect(requests).toContain('/@deepseek-ai/dsh-cua.tgz') + expect(requests.includes(`/${bundleName}.tgz`)).toBe(target !== 'darwin-base') + expect(requests.includes('/@deepseek-ai/dsh-cua.tgz')).toBe(target !== 'darwin-base') expect(requests.includes(`/${providerName}.tgz`), `${target}: ${JSON.stringify(requests)}\n${output}`) .toBe(target === 'darwin' || unrestricted) } diff --git a/apps/desktop/tests/desktop-publish-tag.spec.ts b/apps/desktop/tests/desktop-publish-tag.spec.ts index 7a13daba54..06894f7504 100644 --- a/apps/desktop/tests/desktop-publish-tag.spec.ts +++ b/apps/desktop/tests/desktop-publish-tag.spec.ts @@ -3,13 +3,13 @@ import { publishDesktopTag } from '../scripts/desktop-publish-tag.ts' const commit = 'a'.repeat(40) const tag = (sha = commit) => ({ object: { type: 'commit', sha } }) -function transport(replies: Array<{ status: number; body?: object }>) { +function transport(replies: Array<{ status: number; body?: object; rawBody?: string; headers?: Record }>) { const calls: Array<{ url: string; init: RequestInit | undefined }> = [] const request: typeof fetch = async (url, init) => { calls.push({ url: typeof url === 'string' ? url : url instanceof URL ? url.href : url.url, init }) const reply = replies.shift() if (!reply) throw new Error('unexpected HTTP request') - return new Response(JSON.stringify(reply.body ?? {}), { status: reply.status }) + return new Response(reply.rawBody ?? JSON.stringify(reply.body ?? {}), { status: reply.status, headers: reply.headers ?? {} }) } return { calls, request } } @@ -57,3 +57,36 @@ it('explains an existing annotated tag without replacing it', async () => { await expect(publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request)).rejects.toThrow(/not a lightweight commit tag/u) expect(http.calls).toHaveLength(1) }) + +it('keeps the creation status and safe rule diagnostic when no competing tag exists', async () => { + const http = transport([{ status: 404 }, { status: 422, + headers: { 'x-github-request-id': 'ABCD:1234' }, + body: { message: 'Repository rule violations found for refs/tags/desktop-v1.2.3', errors: ['secret-value'] } }, { status: 404 }]) + const failure = publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request) + await expect(failure).rejects.toThrow('creation failed (HTTP 422; GitHub request ABCD:1234); follow-up tag lookup returned HTTP 404; repository rules rejected') + await expect(failure).rejects.not.toThrow('secret-value') + expect(http.calls.map(call => call.init?.method ?? 'GET')).toEqual(['GET', 'POST', 'GET']) +}) + +it('does not echo arbitrary GitHub response text or unrecognized request IDs', async () => { + const http = transport([{ status: 404 }, { status: 422, + headers: { 'x-github-request-id': 'fixture-token' }, body: { message: 'fixture-token' } }, { status: 403 }]) + const failure = publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request) + await expect(failure).rejects.toThrow('creation failed (HTTP 422); follow-up tag lookup returned HTTP 403') + await expect(failure).rejects.not.toThrow('fixture-token') +}) + +it('still rejects a competing tag on a different commit', async () => { + const http = transport([{ status: 404 }, { status: 422 }, { status: 200, body: tag('b'.repeat(40)) }]) + await expect(publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request)).rejects.toThrow('creation failed (HTTP 422)') +}) + +it('retains creation context for a non-JSON rejection', async () => { + const http = transport([{ status: 404 }, { status: 422, rawBody: 'not JSON', headers: { 'x-github-request-id': 'ABCD:1234' } }, { status: 404 }]) + await expect(publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request)).rejects.toThrow('creation failed (HTTP 422; GitHub request ABCD:1234)') +}) + +it('retains creation context when the follow-up request fails', async () => { + const http = transport([{ status: 404 }, { status: 422, headers: { 'x-github-request-id': 'ABCD:1234' } }]) + await expect(publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request)).rejects.toThrow('creation failed (HTTP 422; GitHub request ABCD:1234); follow-up tag lookup failed') +}) diff --git a/apps/desktop/tests/desktop-publish-templates.spec.ts b/apps/desktop/tests/desktop-publish-templates.spec.ts new file mode 100644 index 0000000000..0ead9a11ab --- /dev/null +++ b/apps/desktop/tests/desktop-publish-templates.spec.ts @@ -0,0 +1,35 @@ +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, posix, win32 } from 'node:path' +import { fileURLToPath } from 'node:url' +import { expect, it } from 'vitest' +import { readDesktopPublishConfig, type DesktopPublishConfig } from '../scripts/desktop-publish-config.ts' + +for (const platform of ['macos', 'windows'] as const) { + for (const preset of ['test', 'production'] as const) { + it(`accepts the ${platform} ${preset} template after machine paths are supplied`, async () => { + const path = fileURLToPath(new URL(`../config/publishing/${platform}/${preset}.json.example`, import.meta.url)) + const template = JSON.parse(await readFile(path, 'utf8')) as DesktopPublishConfig + const paths = platform === 'windows' ? win32 : posix + expect(paths.isAbsolute(template.credentialsFile)).toBe(true) + expect(template[platform === 'macos' ? 'macOS' : 'windows']).toBeDefined() + expect(Object.hasOwn(template, 'secretId')).toBe(false) + expect(Object.hasOwn(template, 'secretKey')).toBe(false) + if (preset === 'production') expect(paths.isAbsolute(template.githubApp?.privateKeyFile ?? '')).toBe(true) + const root = await mkdtemp(join(tmpdir(), 'publish-template-')) + try { + await mkdir(join(root, 'publishing')) + // The parser uses the current host's path syntax; examples retain their target platform's paths. + const config = { ...template, credentialsFile: join(root, 'absent-credentials.json'), + ...(template.githubApp ? { githubApp: { ...template.githubApp, privateKeyFile: join(root, 'absent-key.pem') } } : {}) } + const output = join(root, 'publishing', `${preset}.json`) + await writeFile(output, JSON.stringify(config)) + await expect(readDesktopPublishConfig(root, preset)).resolves.toEqual(config) + await writeFile(output, JSON.stringify({ ...config, bucket: undefined })) + const failure = readDesktopPublishConfig(root, preset) + await expect(failure).rejects.toHaveProperty('name', 'ZodError') + await expect(failure).rejects.toHaveProperty('issues.0.path', ['bucket']) + } finally { await rm(root, { recursive: true, force: true }) } + }) + } +} diff --git a/apps/plugin-market-generator/package.json b/apps/plugin-market-generator/package.json index b9b31d1912..a426b81646 100644 --- a/apps/plugin-market-generator/package.json +++ b/apps/plugin-market-generator/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-market-generator", "description": "Reference generator of plugin market index files from a list of npm packages and Git repositories", - "version": "0.2.1-rc.3", + "version": "0.2.2-alpha.2", "private": true, "type": "module", "scripts": { diff --git a/apps/web/package.json b/apps/web/package.json index bba6161259..51ecc68787 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/apps/web/tests/caption-overlays.e2e.ts b/apps/web/tests/caption-overlays.e2e.ts index 2a86277843..d336b64950 100644 --- a/apps/web/tests/caption-overlays.e2e.ts +++ b/apps/web/tests/caption-overlays.e2e.ts @@ -21,8 +21,9 @@ it('keeps settings and expanded plugin dialogs clear of the Windows caption acro 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 page.setViewportSize({ width: 521, height: 600 }) + await page.locator('[data-sidebar-drawer]').waitFor({ state: 'detached' }) + const reopenSidebar = page.locator('[data-shell-leading]').getByRole('button', { name: '打开侧边栏', exact: true }) await openSettings(page, 'zh') const settings = page.getByRole('dialog', { name: '设置', exact: true }) const settingsMask = settings.locator('..').locator(':scope > [aria-hidden="true"]') @@ -51,7 +52,7 @@ it('keeps settings and expanded plugin dialogs clear of the Windows caption acro 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() + if (await page.locator('[data-sidebar-drawer][data-sidebar-collapsed]').count() > 0) 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 }) @@ -88,7 +89,9 @@ it('keeps settings and expanded plugin dialogs clear of the Windows caption acro if (value === 'darwin') document.documentElement.setAttribute('data-platform', value) else document.documentElement.removeAttribute('data-platform') }, platform) - if (await reopenSidebar.isVisible()) await reopenSidebar.click() + // Publish the synthetic platform change through the frame's size observer. + await page.setViewportSize({ width: platform === 'darwin' ? 520 : 521, height: 600 }) + await page.locator('[data-sidebar-drawer]').waitFor() await openSettings(page, 'zh') await expect.poll(async () => (await settings.boundingBox())?.y).toBe(platform === 'darwin' ? 68 : 0) expect((await settingsMask.boundingBox())?.y).toBe(0) diff --git a/apps/web/tests/chat-fold-opened-group.e2e.ts b/apps/web/tests/chat-fold-opened-group.e2e.ts index a419c25a0a..1b92611d27 100644 --- a/apps/web/tests/chat-fold-opened-group.e2e.ts +++ b/apps/web/tests/chat-fold-opened-group.e2e.ts @@ -7,8 +7,8 @@ * * - A long answer leaves the folded group above the viewport. The scroll position changes in one direction * only: the fold never scrolls the view away and then snaps it back. - * - A short answer leaves fold room below the tail. Growth fills that room without scrolling, so the running - * status keeps its entrance slide when the new Step's first row appears instead of moving in one frame. + * - A short answer leaves fold room below the tail. The new Turn's growth fills that room without scrolling + * through Turn end, the room never grows back, and the running status only moves down. */ import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' @@ -26,7 +26,7 @@ import { newEnglishPage, openSettings } from './support.ts' declare global { interface Window { - openFoldTrace?: { readonly frames: TraceFrame[]; readonly slides: number[]; stop(): void } + openFoldTrace?: { readonly frames: TraceFrame[]; stop(): void } } } @@ -43,8 +43,6 @@ interface TraceFrame { readonly floor: number /** Height of the fold room below the transcript. */ readonly spacer: number - /** The column marks the tail as pinned, which runs the status slide at zero duration. */ - readonly pinned: boolean /** The running status's top relative to the scrollport, when it is mounted. */ readonly status: number | null readonly folding: boolean @@ -162,17 +160,13 @@ async function openLatestGroup(page: Page): Promise { await page.mouse.move(0, 0) } -/** - * Sample the scrollport on every animation frame, and record the duration of every status entrance slide - * that a flow change starts. - */ +/** Sample the scrollport on every animation frame. */ async function startTrace(page: Page): Promise { await page.evaluate((selector) => { const host = document.querySelector(selector)! const column = host.querySelector('[data-chat-flow]')! const spacer = host.querySelector('[data-chat-turn-spacer]')! const frames: TraceFrame[] = [] - const slides: number[] = [] let frame = 0 const tick = (): void => { const status = host.querySelector('[data-chat-running]') @@ -180,32 +174,18 @@ async function startTrace(page: Page): Promise { top: host.scrollTop, floor: Math.max(0, host.scrollHeight - host.clientHeight), spacer: spacer.getBoundingClientRect().height, - pinned: column.hasAttribute('data-chat-tail-pinned'), status: status === null ? null : status.getBoundingClientRect().top - host.getBoundingClientRect().top, folding: column.hasAttribute('data-input-fold') || host.querySelector('[data-chat-motion="collapse"]') !== null, }) frame = requestAnimationFrame(tick) } - // The slide starts at the style update after the flow change; reading animations there sees its duration. - const observer = new MutationObserver(() => { - const status = host.querySelector('[data-chat-running]') - for (const animation of status?.getAnimations() ?? []) { - if (animation instanceof CSSAnimation && animation.animationName.includes('running-follows-output')) { - slides.push(Number(animation.effect?.getComputedTiming().duration ?? 0)) - } - } - }) - observer.observe(column, { childList: true, subtree: true }) - window.openFoldTrace = { frames, slides, stop: () => { cancelAnimationFrame(frame); observer.disconnect() } } + window.openFoldTrace = { frames, stop: () => { cancelAnimationFrame(frame) } } frame = requestAnimationFrame(tick) }, ROOT) } -async function readTrace(page: Page): Promise<{ frames: TraceFrame[]; slides: number[] }> { - return page.evaluate(() => { - const trace = window.openFoldTrace! - return { frames: [...trace.frames], slides: [...trace.slides] } - }) +async function readTrace(page: Page): Promise<{ frames: TraceFrame[] }> { + return page.evaluate(() => ({ frames: [...window.openFoldTrace!.frames] })) } /** Per-frame scroll movements larger than subpixel rounding. */ @@ -339,26 +319,28 @@ describe.skipIf(MODE === 'record')('web e2e: next-message fold of an opened proc } }) - it('keeps the running status slide while fold room below a short answer takes the new Step', async () => { + it('keeps the view still while fold room below a short answer takes the new Turn', async () => { const world = await sendFromOpenedGroup(browser, 'short', 2) try { - const folded = (await readTrace(world.page)).frames.at(-1)! + const foldedFrames = (await readTrace(world.page)).frames + const folded = foldedFrames.at(-1)! expect.soft(folded.spacer, 'fold room below the short answer').toBeGreaterThan(0) - expect.soft(folded.pinned, 'tail pin while fold room remains').toBe(false) world.releaseTurn() // The running Step's group row is the first row after the user message; output stays held until then. await world.page.locator(`${ROOT} [data-chat-group-key][data-chat-turn="${String(TURNS + 1)}"]`).waitFor({ timeout: 30_000 }) await world.page.locator(`${ROOT} [data-chat-running]`).waitFor() - await world.page.waitForTimeout(300) - const { frames, slides } = await readTrace(world.page) - const statusSteps = movements(frames.map(frame => ({ ...frame, top: frame.status ?? Number.NaN }))) - .filter(step => Number.isFinite(step)) - // The fold runs status moves at zero duration on its own, so only settled frames show the pin's effect. - const pinnedOverRoom = frames.filter(frame => !frame.folding && frame.pinned && frame.spacer > 0) - expect.soft(pinnedOverRoom, 'settled frames pinned over fold room').toEqual([]) - expect.soft(slides.some(duration => duration > 0), `status slide durations ${JSON.stringify(slides)}; status movements ${JSON.stringify(statusSteps)}`).toBe(true) world.releaseModel() await world.ended + await stableScroll(world.page) + // Frames from the settled fold through Turn end. + const after = (await readTrace(world.page)).frames.slice(foldedFrames.length - 1) + const roomed = after.filter(frame => frame.spacer > 0) + const regrowth = roomed.some((frame, index) => index > 0 && frame.spacer > roomed[index - 1]!.spacer + 1) + const statusSteps = movements(after.map(frame => ({ ...frame, top: frame.status ?? Number.NaN }))) + .filter(step => Number.isFinite(step)) + expect.soft(movements(roomed), 'scroll movements while fold room remains').toEqual([]) + expect.soft(regrowth, `fold room grows back; spacer ${JSON.stringify(roomed.map(frame => Math.round(frame.spacer)))}`).toBe(false) + expect.soft(statusSteps.every(step => step > 0), `running status movements ${JSON.stringify(statusSteps)}`).toBe(true) expect(world.tripwire.pageErrors).toEqual([]) } finally { await world.close() diff --git a/apps/web/tests/chat-scroll-contract.e2e.ts b/apps/web/tests/chat-scroll-contract.e2e.ts index 6092eddc02..fd5f778c1e 100644 --- a/apps/web/tests/chat-scroll-contract.e2e.ts +++ b/apps/web/tests/chat-scroll-contract.e2e.ts @@ -309,6 +309,9 @@ async function loadedFlowRows(page: Page): Promise { } async function openSeed(page: Page, fixture: ChatScrollFixture, tailMarker?: string): Promise { + if (await page.locator('[data-sidebar-drawer][data-sidebar-collapsed]').count() > 0) { + await page.locator('[data-shell-leading]').getByRole('button', { name: 'Open sidebar', exact: true }).click() + } // Search collapsed into a header action; expand it before filling. const searchButton = page.getByRole('button', { name: 'Search sessions' }) if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click() @@ -1700,9 +1703,6 @@ describe('web e2e: long Chat scroll contract', () => { await world.page.getByRole('tab', { name: 'Trajectory', exact: true }).click() await world.page.getByLabel('Trajectory timeline').waitFor({ timeout: 30_000 }) await world.page.setViewportSize({ width: 700, height: 900 }) - // The narrow breakpoint auto-collapses the sidebar. Re-open it because - // this scenario switches sessions while pinning the narrow Chat scroll owner. - await world.page.getByRole('button', { name: 'Open sidebar', exact: true }).click() await world.page.getByRole('tab', { name: 'Chat', exact: true }).click() await nextPaint(world.page) await expectSameFlowTop(world.page, sessionAnchor, RESPONSIVE_REFLOW_TOLERANCE) diff --git a/apps/web/tests/client-plugin-live.e2e.ts b/apps/web/tests/client-plugin-live.e2e.ts index 21ad5a1182..91f717363e 100644 --- a/apps/web/tests/client-plugin-live.e2e.ts +++ b/apps/web/tests/client-plugin-live.e2e.ts @@ -46,12 +46,13 @@ it('places dynamic Session menu rows by order among the shipped ones and removes const trailing = row.locator('[data-session-trailing]') const observations: { stage: string; icons: string[]; time: boolean; pinned: boolean; actions: boolean }[] = [] - const observe = async (stage: string, iconsVisible: boolean, actionsVisible: boolean, target = row): Promise => { + const observe = async (stage: string, iconsVisible: boolean, actionsVisible: boolean, target = row, + timeVisible = iconsVisible): Promise => { const icons = target.locator('[data-session-trailing]') const actions = target.getByRole('button', { name: `Session actions for ${SESSION_TITLE}` }) const time = target.locator('[class*="time"]') await expect.poll(() => icons.first().isVisible()).toBe(iconsVisible) - await expect.poll(() => time.isVisible()).toBe(iconsVisible) + await expect.poll(() => time.isVisible()).toBe(timeVisible) await expect.poll(() => actions.isVisible()).toBe(actionsVisible) observations.push({ stage, @@ -113,7 +114,7 @@ it('places dynamic Session menu rows by order among the shipped ones and removes await page.mouse.move(0, 0) await observe('blur', true, false) await page.setViewportSize({ width: 390, height: 844 }) - await page.getByRole('button', { name: 'Open sidebar', exact: true }).first().click() + await page.locator('[data-shell-leading]').getByRole('button', { name: 'Open sidebar', exact: true }).click() await page.mouse.move(0, 0) await observe('narrow', true, false) const bounds = await row.evaluate((element) => { @@ -133,23 +134,23 @@ it('places dynamic Session menu rows by order among the shipped ones and removes try { const touchConsole = watchConsole(touchPage) await touchPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) - await touchPage.getByRole('button', { name: 'Open sidebar', exact: true }).first().click() + await touchPage.locator('[data-shell-leading]').getByRole('button', { name: 'Open sidebar', exact: true }).click() const touchRow = touchPage.getByRole('treeitem').filter({ has: touchPage.getByText(SESSION_TITLE, { exact: true }) }) const touchTrigger = touchRow.getByRole('button', { name: `Session actions for ${SESSION_TITLE}` }) await touchTrigger.waitFor() expect(await touchPage.evaluate(() => matchMedia('(hover: hover)').matches)).toBe(false) - await observe('touch', true, true, touchRow) + await observe('touch', true, true, touchRow, false) expect(await touchRow.getByRole('button', { name: 'Archive session', exact: true }).isVisible()).toBe(false) expect(await touchRow.getByRole('button', { name: 'Pin session', exact: true }).isVisible()).toBe(false) await touchTrigger.tap() await touchPage.getByRole('menu').waitFor() - await observe('touch-menu-open', true, true, touchRow) + await observe('touch-menu-open', true, true, touchRow, false) await touchPage.keyboard.press('Escape') - await observe('touch-menu-closed', true, true, touchRow) + await observe('touch-menu-closed', true, true, touchRow, false) await touchTrigger.tap() await touchPage.getByRole('menuitem', { name: 'Pin session', exact: true }).tap() - await touchRow.getByRole('img', { name: 'Pinned', exact: true }).waitFor() - await observe('touch-pinned', true, true, touchRow) + await touchRow.getByRole('button', { name: 'Unpin session', exact: true }).waitFor() + await observe('touch-pinned', true, true, touchRow, false) const touchBounds = await touchRow.evaluate((element) => { const title = element.querySelector('[class*="title"]')!.getBoundingClientRect() const resting = element.querySelector('[class*="rowResting"]')!.getBoundingClientRect() @@ -173,7 +174,7 @@ it('places dynamic Session menu rows by order among the shipped ones and removes expect(await row.evaluate(element => element === document.activeElement)).toBe(true) await observe('clicked-archived-row', true, false) await scaffold.ctx.workspaceRegistry.unarchiveSession(sessionId) - await observe('touch-unarchived', true, true, touchRow) + await observe('touch-unarchived', true, true, touchRow, false) expect(touchConsole.pageErrors).toEqual([]) } finally { await touchPage.close() diff --git a/apps/web/tests/cua-optin.e2e.ts b/apps/web/tests/cua-optin.e2e.ts index a9ddcde869..c5900519c6 100644 --- a/apps/web/tests/cua-optin.e2e.ts +++ b/apps/web/tests/cua-optin.e2e.ts @@ -63,9 +63,9 @@ it.skipIf(process.platform !== 'darwin').each([ const dialog = page.getByRole('dialog', { name: '添加插件', exact: true }) await dialog.getByRole('textbox', { name: '包名或地址', exact: true }).fill('@deepseek-ai/dsh-cua-bundle') await dialog.getByRole('button', { name: '安装', exact: true }).click() - const ready = page.getByRole('dialog', { name: '插件已随 DSH 提供', exact: true }) + const ready = page.getByRole('dialog', { name: '已安装', exact: true }) await ready.waitFor() - expect(await ready.getByText('无需下载,启用即可使用', { exact: true }).count()).toBe(1) + expect(await ready.getByText('无需下载,启用即可使用', { exact: true }).count()).toBe(0) return ready.getByRole('button', { name: '立即启用', exact: true }) } const enableNow = await offerCua() @@ -199,8 +199,7 @@ it.skipIf(process.platform !== 'darwin').each([ const program = 'globalThis.counter = (globalThis.counter ?? 0) + 1' expect((await runtime.run(runtime.resolve({ program, bindings: [], stateOwner: owner }))).value).toBe(1) await toggle.click() - await toggle.waitFor({ state: 'hidden' }) - await card.waitFor({ state: 'hidden' }) + await toggle.and(page.locator('[aria-checked="false"]')).waitFor({ state: 'visible' }) expect(scaffold.ctx.get('computerUse')).toBeUndefined() expect(runtimeFiber() === persistentRuntimeFiber).toBe(true) expect(scaffold.ctx.agentPresets.defaultId).toBe(initialDefault) @@ -211,7 +210,7 @@ it.skipIf(process.platform !== 'darwin').each([ } expect((await runtime.run(runtime.resolve({ program, bindings: [], stateOwner: owner }))).value).toBe(2) expect(await readFile(settingsPath, 'utf8')).toBe(saved) - await (await offerCua()).click() + await toggle.click() await toggle.and(page.locator('[aria-checked="true"]')).waitFor({ state: 'visible' }) await setup.waitFor() expect(await setup.getByText('Computer Use 插件已开启,但暂时无法检测系统权限。请前往插件详情页查看原因并重试。', { exact: true }).count()).toBe(1) @@ -249,9 +248,8 @@ it.skipIf(process.platform !== 'darwin').each([ expect.objectContaining({ id: 'agent-preset-registry', config: { default: 'standard', selectedDefault: 'ptc' } }), ])) await toggle.click() - await toggle.waitFor({ state: 'hidden' }) - await card.waitFor({ state: 'hidden' }) - await (await offerCua()).click() + await toggle.and(page.locator('[aria-checked="false"]')).waitFor({ state: 'visible' }) + await toggle.click() await toggle.and(page.locator('[aria-checked="true"]')).waitFor({ state: 'visible' }) await setup.waitFor() expect(await setup.getByText('Computer Use 插件已开启,但暂时无法检测系统权限。请前往插件详情页查看原因并重试。', { exact: true }).count()).toBe(1) diff --git a/apps/web/tests/cua-permissions.e2e.ts b/apps/web/tests/cua-permissions.e2e.ts index 4835557ce8..72afb6648d 100644 --- a/apps/web/tests/cua-permissions.e2e.ts +++ b/apps/web/tests/cua-permissions.e2e.ts @@ -29,7 +29,7 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr const helper = await installNativeFixture(root) resources.closeFixture = await startFixtureHost(helper) const state = join(root, 'permissions.json'), audit = join(root, 'helper-events.jsonl') - const launchApplication = { name: 'Visual Studio Code.app', path: '/Applications/Visual Studio Code.app', + const launchApplication = { name: 'DSH Computer Use.app', path: '/Applications/DSH Computer Use.app', icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADElEQVQImWNgZGIGAAAOAAeCcsnOAAAAAElFTkSuQmCC' } await writeFile(state, JSON.stringify({ accessibility: false, screenRecording: false, launchApplication })) await writeFile(join(root, 'observe-helper'), '') @@ -89,10 +89,10 @@ it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fr expect(await panel.getByRole('region', { name: 'Manage applications computer use can access', exact: true }).count()).toBe(1) await expect.poll(() => panel.locator('li img').evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(1) expect(await panel.getByText('/fixture/app', { exact: true }).count()).toBe(0) - expect(await accessibility.getByText('Visual Studio Code.app', { exact: true }).count()).toBe(1) + expect(await accessibility.getByText('DSH Computer Use.app', { exact: true }).count()).toBe(1) expect(await accessibility.textContent()).toContain('In System Settings, allow') expect(await accessibility.textContent()).toContain('to read interface controls, click, and type.') - expect(await panel.getByRole('group', { name: 'Screen Recording', exact: true }).getByText('Visual Studio Code.app', { exact: true }).count()).toBe(1) + expect(await panel.getByRole('group', { name: 'Screen Recording', exact: true }).getByText('DSH Computer Use.app', { exact: true }).count()).toBe(1) await expect.poll(() => accessibility.locator('img').evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(1) await expect.poll(() => panel.getAttribute('aria-busy')).toBe('false') await compareOrRefreshGolden(expected, await captureStableAria(page, '[data-cua-settings]', scaffold.workspaceCwd), webSnapshotMode()) diff --git a/apps/web/tests/deepseek-model-label-1017.e2e.ts b/apps/web/tests/deepseek-model-label-1017.e2e.ts deleted file mode 100644 index f732f2f7a9..0000000000 --- a/apps/web/tests/deepseek-model-label-1017.e2e.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** The shipped browser plugin decorates both DeepSeek routes without changing catalog identity. */ -import { fileURLToPath } from 'node:url' -import { chromium, type Browser, type Page } from 'playwright' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { - captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, -} from './scaffold.ts' -import { connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts' - -const NAME = 'DeepSeek-V4.1-Flash' -const FIXTURE = fileURLToPath(new URL('./fixtures/deepseek-model-label-1017/session.v4.jsonl', import.meta.url)) -const EXPECTED = fileURLToPath(new URL('./expected/deepseek-model-label-1017/models.expected.md', import.meta.url)) -const MODE = webSnapshotMode() - -describe.skipIf(MODE === 'record')('web e2e: temporary DeepSeek release badge', () => { - let scaffold: WebScaffold - let browser: Browser - let page: Page - let tripwire: ReturnType - - beforeAll(async () => { - scaffold = await launchWebScaffold({ - extraOverlayPath: fileURLToPath(new URL('./fixtures/deepseek-model-label-1017/account.overlay.yml', import.meta.url)), - replayFixture: FIXTURE, - replayProvidersOnly: true, - replayProviders: [ - { id: 'deepseek-account', name: 'DeepSeek Account', models: [{ id: 'deepseek-flash', name: NAME }] }, - { id: 'deepseek-official', name: 'DeepSeek', models: [ - { id: 'deepseek-flash', name: NAME }, { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' }, - ] }, - { id: 'external', name: 'External', models: [{ id: 'deepseek-flash', name: NAME }] }, - ], - }) - browser = await chromium.launch() - page = await browser.newPage({ viewport: { width: 1440, height: 900 }, locale: ZH_BROWSER_LOCALE }) - tripwire = watchConsole(page) - await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) - await connectFreshWorkspaceZh(page, scaffold.workspaceCwd) - }) - - afterAll(async () => { - try { await browser?.close() } - finally { await scaffold?.close() } - }) - - it('shows the same badge in all placements while preserving the chosen provider and model id', async () => { - const trigger = page.getByRole('button', { name: /^选择模型/ }) - await trigger.getByText('1017', { exact: true }).waitFor() - expect(await trigger.textContent()).toContain(`${NAME} 1017`) - expect(await trigger.getAttribute('aria-label')).toBe(`选择模型,当前 ${NAME}`) - try { - await trigger.click() - const current = page.getByRole('menuitem', { name: /^模型/ }) - expect(await current.getByText('1017', { exact: true }).count()).toBe(1) - await current.click() - const menu = page.getByRole('menu', { name: '模型', exact: true }) - await compareOrRefreshGolden(EXPECTED, await captureStableAria(page, '[role="menu"][aria-label="模型"]', scaffold.workspaceCwd), MODE) - expect(await menu.getByText('1017', { exact: true }).count()).toBe(2) - expect(await menu.getByRole('group', { name: 'External', exact: true }).getByText('1017', { exact: true }).count()).toBe(0) - const account = menu.getByRole('group', { name: 'DeepSeek 账号', exact: true }) - await account.getByRole('menuitemradio', { name: `${NAME} 1017`, exact: true }).click() - await expect.poll(() => scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-account', model: 'deepseek-flash' }) - await expect.poll(() => trigger.textContent()).toContain(`${NAME} 1017`) - const catalog = await scaffold.ctx.llm.listModels('deepseek-account') - expect(catalog.map(model => ({ id: model.id, name: model.name }))).toEqual([{ id: 'deepseek-flash', name: NAME }]) - } finally { - await page.keyboard.press('Escape') - await page.keyboard.press('Escape') - } - }) - - it('keeps the release badge inside a 390px model menu and uses both theme palettes', async () => { - try { - await page.setViewportSize({ width: 390, height: 844 }) - const trigger = page.getByRole('button', { name: /^选择模型/ }) - await trigger.click() - await page.getByRole('menuitem', { name: /^模型/ }).click() - const menu = page.getByRole('menu', { name: '模型', exact: true }) - const badge = menu.getByRole('group', { name: 'DeepSeek 账号', exact: true }).getByText('1017', { exact: true }) - const box = await badge.boundingBox() - expect(box).not.toBeNull() - expect(box!.x).toBeGreaterThanOrEqual(12) - expect(box!.x + box!.width).toBeLessThanOrEqual(378) - const colors = await badge.evaluate((node) => { - const body = node.ownerDocument.body - const previous = body.getAttribute('data-ds-dark-theme') - try { - return [false, true].map((dark) => { - body.toggleAttribute('data-ds-dark-theme', dark) - const style = getComputedStyle(node) - return { color: style.color, background: style.backgroundColor } - }) - } finally { - if (previous === null) body.removeAttribute('data-ds-dark-theme') - else body.setAttribute('data-ds-dark-theme', previous) - } - }) - expect(colors[0]).not.toEqual(colors[1]) - for (const color of colors) expect(color.color).not.toBe(color.background) - } finally { - await page.keyboard.press('Escape') - await page.keyboard.press('Escape') - await page.setViewportSize({ width: 1440, height: 900 }) - } - expect(tripwire.pageErrors).toEqual([]) - }) -}) diff --git a/apps/web/tests/details-session-lifecycle.e2e.ts b/apps/web/tests/details-session-lifecycle.e2e.ts index 68dd75f3d8..84a34241f8 100644 --- a/apps/web/tests/details-session-lifecycle.e2e.ts +++ b/apps/web/tests/details-session-lifecycle.e2e.ts @@ -188,7 +188,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S await blankColumn.locator('[data-sidebar-right-toggle]').click() await blankColumn.locator('[data-dockkit-pane]').first().waitFor({ state: 'hidden' }) await page.setViewportSize({ width: 767, height: blankViewport.height }) - await expect.poll(() => columns(page)).toEqual([56, 711, 0]) + await expect.poll(() => columns(page)).toEqual([0, 767, 0]) await page.locator('[data-sidebar-right-expand]').click() // The fullscreen shell keeps its viewport box while its docked content closes. await expect.poll(() => blankColumn.locator('[data-sidebar-right-open]').count()).toBe(1) @@ -425,7 +425,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S await expect.poll(() => columns(page)).toEqual([420, viewport.width - 420 - normalWidth, normalWidth]) await page.setViewportSize({ width: 767, height: viewport.height }) await expect.poll(() => panel.boundingBox()).toEqual({ x: 0, y: 0, width: 767, height: viewport.height }) - await expect.poll(() => columns(page)).toEqual([56, 711, 0]) + await expect.poll(() => columns(page)).toEqual([0, 767, 0]) expect(await sidebarSnapshot(page)).toMatchObject({ mode: 'fullscreen', resizeHandleWidth: 0, coversViewport: true }) await checkpoint('A automatic fullscreen at 767px') await column.locator('[data-sidebar-right-mode="push"]').click() diff --git a/apps/web/tests/expected/client-plugin-live/session-trailing.expected.json b/apps/web/tests/expected/client-plugin-live/session-trailing.expected.json index 8b5c4cb664..83f9ecd3fb 100644 --- a/apps/web/tests/expected/client-plugin-live/session-trailing.expected.json +++ b/apps/web/tests/expected/client-plugin-live/session-trailing.expected.json @@ -86,7 +86,7 @@ "Fixture badge", "Fixture section" ], - "time": true, + "time": false, "pinned": false, "actions": true }, @@ -96,7 +96,7 @@ "Fixture badge", "Fixture section" ], - "time": true, + "time": false, "pinned": false, "actions": true }, @@ -106,7 +106,7 @@ "Fixture badge", "Fixture section" ], - "time": true, + "time": false, "pinned": false, "actions": true }, @@ -116,8 +116,8 @@ "Fixture badge", "Fixture section" ], - "time": true, - "pinned": true, + "time": false, + "pinned": false, "actions": true }, { @@ -136,7 +136,7 @@ "Fixture badge", "Fixture section" ], - "time": true, + "time": false, "pinned": false, "actions": true } diff --git a/apps/web/tests/expected/cua-permissions/missing.expected.md b/apps/web/tests/expected/cua-permissions/missing.expected.md index cb4ade57ac..3b32eb2418 100644 --- a/apps/web/tests/expected/cua-permissions/missing.expected.md +++ b/apps/web/tests/expected/cua-permissions/missing.expected.md @@ -10,11 +10,11 @@ - button "Use persistent mode" - group "Accessibility": - strong: Accessibility - - paragraph: In System Settings, allow Visual Studio Code.app to read interface controls, click, and type. + - paragraph: In System Settings, allow DSH Computer Use.app to read interface controls, click, and type. - button "Request permission" - group "Screen Recording": - strong: Screen Recording - - paragraph: In System Settings, allow Visual Studio Code.app to capture application windows. + - paragraph: In System Settings, allow DSH Computer Use.app to capture application windows. - button "Request permission" - paragraph: Manage permissions in System Settings for the application named by macOS. - region "Manage applications computer use can access": diff --git a/apps/web/tests/expected/forge-panel/branch-details.expected.md b/apps/web/tests/expected/forge-panel/branch-details.expected.md new file mode 100644 index 0000000000..8c348d423a --- /dev/null +++ b/apps/web/tests/expected/forge-panel/branch-details.expected.md @@ -0,0 +1 @@ +- 'tooltip "Current branch: feature/a-long-current-branch-name-for-mobile-review"' diff --git a/apps/web/tests/expected/forge-panel/changes-view.expected.md b/apps/web/tests/expected/forge-panel/changes-view.expected.md index 27d92b67f7..b6c825a833 100644 --- a/apps/web/tests/expected/forge-panel/changes-view.expected.md +++ b/apps/web/tests/expected/forge-panel/changes-view.expected.md @@ -1,5 +1,6 @@ - img "变更请求状态:需要处理" - text: "Add the pull request panel #7" +- button "提交评审" - button "在浏览器中打开" - button "全部 2 个文件" - text: +4 −2 @@ -7,14 +8,64 @@ - button "全部折叠" - button "左右对比" [pressed] - button "自动换行" [pressed] -- button "收起 panel.ts" [expanded] -- text: src/panel.ts +2 −2 -- button "panel.ts 的更多操作" -- text: 1 export const n1 = 1 1 export const n1 = 1 2 export const n2 = 2 2 export const n2 = 'two' 3 export const n3 = 3 3 export const n3 = 3 4 export const n4 = 4 4 export const n4 = 4 5 export const n5 = 5 5 export const n5 = 5 -- button "6 行未变更" -- text: 12 export const n12 = 12 12 export const n12 = 12 13 export const n13 = 13 13 export const n13 = 13 14 export const n14 = 14 14 export const n14 = 14 15 export const n15 = 15 15 export const n15 = 'fifteen' 16 export const n16 = 16 16 export const n16 = 16 17 export const n17 = 17 17 export const n17 = 17 18 export const n18 = 18 18 export const n18 = 18 -- button "其余未变更的行" -- button "收起 panel.md" [expanded] -- text: docs/panel.md 新文件 +2 −0 -- button "panel.md 的更多操作" -- text: "1 + # Panel 2 + Shows the change requests of a Session." +- region "全部 2 个文件": + - button "收起 panel.ts" [expanded] + - text: src/panel.ts +2 −2 + - button "panel.ts 的更多操作" + - button "选择第 L1 行": "1" + - text: export const n1 = 1 + - button "选择第 R1 行": "1" + - text: export const n1 = 1 + - button "选择第 L2 行": "2" + - text: export const n2 = 2 + - button "选择第 R2 行": "2" + - text: export const n2 = 'two' + - button "选择第 L3 行": "3" + - text: export const n3 = 3 + - button "选择第 R3 行": "3" + - text: export const n3 = 3 + - button "选择第 L4 行": "4" + - text: export const n4 = 4 + - button "选择第 R4 行": "4" + - text: export const n4 = 4 + - button "选择第 L5 行": "5" + - text: export const n5 = 5 + - button "选择第 R5 行": "5" + - text: export const n5 = 5 + - button "6 行未变更" + - button "选择第 L12 行": "12" + - text: export const n12 = 12 + - button "选择第 R12 行": "12" + - text: export const n12 = 12 + - button "选择第 L13 行": "13" + - text: export const n13 = 13 + - button "选择第 R13 行": "13" + - text: export const n13 = 13 + - button "选择第 L14 行": "14" + - text: export const n14 = 14 + - button "选择第 R14 行": "14" + - text: export const n14 = 14 + - button "选择第 L15 行": "15" + - text: export const n15 = 15 + - button "选择第 R15 行": "15" + - text: export const n15 = 'fifteen' + - button "选择第 L16 行": "16" + - text: export const n16 = 16 + - button "选择第 R16 行": "16" + - text: export const n16 = 16 + - button "选择第 L17 行": "17" + - text: export const n17 = 17 + - button "选择第 R17 行": "17" + - text: export const n17 = 17 + - button "选择第 L18 行": "18" + - text: export const n18 = 18 + - button "选择第 R18 行": "18" + - text: export const n18 = 18 + - button "其余未变更的行" + - button "收起 panel.md" [expanded] + - text: docs/panel.md 新文件 +2 −0 + - button "panel.md 的更多操作" + - button "选择第 R1 行": "1" + - text: "+ # Panel" + - button "选择第 R2 行": "2" + - text: + Shows the change requests of a Session. diff --git a/apps/web/tests/expected/forge-panel/file-tree-toggle.expected.md b/apps/web/tests/expected/forge-panel/file-tree-toggle.expected.md new file mode 100644 index 0000000000..b1c29e9f64 --- /dev/null +++ b/apps/web/tests/expected/forge-panel/file-tree-toggle.expected.md @@ -0,0 +1,77 @@ +- img "变更请求状态:需要处理" +- text: "Add the pull request panel #7" +- button "提交评审" +- button "在浏览器中打开" +- button "全部 2 个文件" +- text: +4 −2 +- button "刷新变更" +- button "全部折叠" +- button "左右对比" [pressed] +- button "自动换行" [pressed] +- button "文件树" [expanded] [pressed] +- region "全部 2 个文件": + - button "收起 panel.ts" [expanded] + - text: src/panel.ts +2 −2 + - button "panel.ts 的更多操作" + - button "选择第 L1 行": "1" + - text: export const n1 = 1 + - button "选择第 R1 行": "1" + - text: export const n1 = 1 + - button "选择第 L2 行": "2" + - text: export const n2 = 2 + - button "选择第 R2 行": "2" + - text: export const n2 = 'two' + - button "选择第 L3 行": "3" + - text: export const n3 = 3 + - button "选择第 R3 行": "3" + - text: export const n3 = 3 + - button "选择第 L4 行": "4" + - text: export const n4 = 4 + - button "选择第 R4 行": "4" + - text: export const n4 = 4 + - button "选择第 L5 行": "5" + - text: export const n5 = 5 + - button "选择第 R5 行": "5" + - text: export const n5 = 5 + - button "6 行未变更" + - button "选择第 L12 行": "12" + - text: export const n12 = 12 + - button "选择第 R12 行": "12" + - text: export const n12 = 12 + - button "选择第 L13 行": "13" + - text: export const n13 = 13 + - button "选择第 R13 行": "13" + - text: export const n13 = 13 + - button "选择第 L14 行": "14" + - text: export const n14 = 14 + - button "选择第 R14 行": "14" + - text: export const n14 = 14 + - button "选择第 L15 行": "15" + - text: export const n15 = 15 + - button "选择第 R15 行": "15" + - text: export const n15 = 'fifteen' + - button "选择第 L16 行": "16" + - text: export const n16 = 16 + - button "选择第 R16 行": "16" + - text: export const n16 = 16 + - button "选择第 L17 行": "17" + - text: export const n17 = 17 + - button "选择第 R17 行": "17" + - text: export const n17 = 17 + - button "选择第 L18 行": "18" + - text: export const n18 = 18 + - button "选择第 R18 行": "18" + - text: export const n18 = 18 + - button "其余未变更的行" + - button "收起 panel.md" [expanded] + - text: docs/panel.md 新文件 +2 −0 + - button "panel.md 的更多操作" + - button "选择第 R1 行": "1" + - text: "+ # Panel" + - button "选择第 R2 行": "2" + - text: + Shows the change requests of a Session. +- separator "调整文件树宽度" +- navigation "全部 2 个文件": + - textbox "筛选文件…": src + - button "src" [expanded] + - button "src/panel.ts": panel.ts +2 −2 diff --git a/apps/web/tests/expected/forge-panel/local-card.expected.md b/apps/web/tests/expected/forge-panel/local-card.expected.md new file mode 100644 index 0000000000..24058934d3 --- /dev/null +++ b/apps/web/tests/expected/forge-panel/local-card.expected.md @@ -0,0 +1,4 @@ +- button "Local changes" +- text: HEAD → origin/main +- 'button "View the changes of this Session: 60 files, 60 lines added, 0 lines deleted"': +60 −0 60 files +- button "Create change request" diff --git a/apps/web/tests/expected/forge-panel/review-composer.expected.md b/apps/web/tests/expected/forge-panel/review-composer.expected.md new file mode 100644 index 0000000000..d2c1d4682d --- /dev/null +++ b/apps/web/tests/expected/forge-panel/review-composer.expected.md @@ -0,0 +1,7 @@ +- region "评论 R2–4": + - text: 评论 R2–4 + - textbox "留下评论…": 请核对边界条件 + - button "取消" + - button "添加到输入框" + - button "开始评审" + - button "GitHub 评论选项" diff --git a/apps/web/tests/expected/forge-panel/review-message.expected.md b/apps/web/tests/expected/forge-panel/review-message.expected.md new file mode 100644 index 0000000000..c454458d69 --- /dev/null +++ b/apps/web/tests/expected/forge-panel/review-message.expected.md @@ -0,0 +1,25 @@ +> ## User Comment 2 +> File: src/extra.ts +> Side: R +> Lines: 1 +> Source: Local (branch) +> Diff hunk: +> ```diff +> @@ -0,0 +1,1 @@ +> +export const extra = 1 +> ``` +> Comment: +> 请检查这项修改 + +> ## User Comment 1 +> File: src/app.ts +> Side: R +> Lines: 1 +> Source: Local (branch) +> Diff hunk: +> ```diff +> @@ -0,0 +1,1 @@ +> +export const app = 1 +> ``` +> Comment: +> 请解释这行代码 diff --git a/apps/web/tests/expected/forge-panel/review-quotes.expected.md b/apps/web/tests/expected/forge-panel/review-quotes.expected.md new file mode 100644 index 0000000000..602d647831 --- /dev/null +++ b/apps/web/tests/expected/forge-panel/review-quotes.expected.md @@ -0,0 +1,6 @@ +- textbox "发消息或创建任务, / 调用指令, @ 文件或对话": + - paragraph: + - button "移除引用" + - text: "## User Comment 2 File: src/extra.ts Side: R Lines: 1 Source: Local (branch) Diff hunk: ```diff @@ -0,0 +1,1 @@ +export const extra = 1 ``` Comment: 请检查这项修改" + - button "移除引用" + - text: "## User Comment 1 File: src/app.ts Side: R Lines: 1 Source: Local (branch) Diff hunk: ```diff @@ -0,0 +1,1 @@ +export const app = 1 ``` Comment: 请解释这行代码" diff --git a/apps/web/tests/expected/forge-panel/review-record.expected.md b/apps/web/tests/expected/forge-panel/review-record.expected.md new file mode 100644 index 0000000000..3e678d6071 --- /dev/null +++ b/apps/web/tests/expected/forge-panel/review-record.expected.md @@ -0,0 +1,4 @@ +- region "评论 R2–4": + - text: 你的评论 R2–4 已发布到 GitHub 请核对边界条件 + - button "添加到输入框" + - button "在 GitHub 查看" diff --git a/apps/web/tests/expected/forge-panel/session-row-hover.expected.md b/apps/web/tests/expected/forge-panel/session-row-hover.expected.md new file mode 100644 index 0000000000..1e00472353 --- /dev/null +++ b/apps/web/tests/expected/forge-panel/session-row-hover.expected.md @@ -0,0 +1 @@ +- text: "dsh-{{worktree}} 工作树 · 基于 release #7 Add the pull request panel 需要处理 · 检查失败" diff --git a/apps/web/tests/expected/forge-panel/submit-review.expected.md b/apps/web/tests/expected/forge-panel/submit-review.expected.md new file mode 100644 index 0000000000..c06de70320 --- /dev/null +++ b/apps/web/tests/expected/forge-panel/submit-review.expected.md @@ -0,0 +1,13 @@ +- dialog "提交评审": + - heading "提交评审" [level=2] + - button "关闭" + - text: 待提交评论(1) + - textbox "评审总结…": 需要补充边界检查 + - radio "评论" + - text: 评论 + - radio "批准" + - text: 批准 + - radio "请求更改" [checked] + - text: 请求更改 + - button "取消" + - button "提交评审" diff --git a/apps/web/tests/expected/forge-panel/worktree-option.expected.md b/apps/web/tests/expected/forge-panel/worktree-option.expected.md new file mode 100644 index 0000000000..c9a85b8723 --- /dev/null +++ b/apps/web/tests/expected/forge-panel/worktree-option.expected.md @@ -0,0 +1,3 @@ +- button "当前分支:main": main +- checkbox "新建工作树" +- text: 新建工作树 diff --git a/apps/web/tests/expected/layout-bottom/geometry.expected.md b/apps/web/tests/expected/layout-bottom/geometry.expected.md index 39188ce561..8e53379673 100644 --- a/apps/web/tests/expected/layout-bottom/geometry.expected.md +++ b/apps/web/tests/expected/layout-bottom/geometry.expected.md @@ -4,6 +4,6 @@ grown: {"bottom":{"top":440,"height":360,"bottom":800},"main":{"top":0,"height": darwin: {"bottom":{"top":440,"height":360,"bottom":800},"main":{"top":0,"height":440,"bottom":440},"sidebar":{"top":0,"height":440,"bottom":440},"rightbar":{"top":0,"height":440,"bottom":440},"overlay":{"top":0,"height":800,"bottom":800},"handles":[{"top":0,"height":440,"bottom":440}],"pageHeight":800} windows: {"bottom":{"top":440,"height":360,"bottom":800},"main":{"top":40,"height":400,"bottom":440},"sidebar":{"top":40,"height":400,"bottom":440},"rightbar":{"top":40,"height":400,"bottom":440},"overlay":{"top":0,"height":800,"bottom":800},"handles":[{"top":40,"height":400,"bottom":440}],"pageHeight":800} browser: {"bottom":{"top":440,"height":360,"bottom":800},"main":{"top":0,"height":440,"bottom":440},"sidebar":{"top":0,"height":440,"bottom":440},"rightbar":{"top":0,"height":440,"bottom":440},"overlay":{"top":0,"height":800,"bottom":800},"handles":[{"top":0,"height":440,"bottom":440}],"pageHeight":800} -narrow: {"bottom":{"top":240,"height":360,"bottom":600},"main":{"top":0,"height":240,"bottom":240},"sidebar":{"top":0,"height":240,"bottom":240},"rightbar":{"top":0,"height":240,"bottom":240},"overlay":{"top":0,"height":600,"bottom":600},"handles":[],"pageHeight":600} +narrow: {"bottom":{"top":240,"height":360,"bottom":600},"main":{"top":0,"height":240,"bottom":240},"sidebar":{"top":0,"height":600,"bottom":600},"rightbar":{"top":0,"height":240,"bottom":240},"overlay":{"top":0,"height":600,"bottom":600},"handles":[],"pageHeight":600} unregistered: {"bottom":{"top":600,"height":0,"bottom":600},"main":{"top":0,"height":600,"bottom":600},"sidebar":{"top":0,"height":600,"bottom":600},"rightbar":{"top":0,"height":600,"bottom":600},"overlay":{"top":0,"height":600,"bottom":600},"handles":[],"pageHeight":600} collapsed: {"bottom":{"top":600,"height":0,"bottom":600},"main":{"top":0,"height":600,"bottom":600},"sidebar":{"top":0,"height":600,"bottom":600},"rightbar":{"top":0,"height":600,"bottom":600},"overlay":{"top":0,"height":600,"bottom":600},"handles":[],"pageHeight":600} diff --git a/apps/web/tests/expected/markdown-colon-file-links.md b/apps/web/tests/expected/markdown-colon-file-links.md new file mode 100644 index 0000000000..fe7031c9f0 --- /dev/null +++ b/apps/web/tests/expected/markdown-colon-file-links.md @@ -0,0 +1,21 @@ +- paragraph: + - button "Relative colon:24–30" + - text: · + - button "Absolute colon:30" + - text: · + - button "Fragment:12" + - text: · + - button "Column:24:5" + - text: · + - button "Zero line" + - text: · + - button "Reversed range" + - text: · + - link "Bare URL": + - /url: index.ts:24 + - text: · Blocked script · + - link "Development server": + - /url: localhost:3000 + - text: · + - link "Mail": + - /url: mailto:10086 diff --git a/apps/web/tests/expected/deepseek-model-label-1017/models.expected.md b/apps/web/tests/expected/model-badge/models.expected.md similarity index 55% rename from apps/web/tests/expected/deepseek-model-label-1017/models.expected.md rename to apps/web/tests/expected/model-badge/models.expected.md index 9f86e38326..3ac3338e57 100644 --- a/apps/web/tests/expected/deepseek-model-label-1017/models.expected.md +++ b/apps/web/tests/expected/model-badge/models.expected.md @@ -4,8 +4,10 @@ - menuitemradio "DeepSeek-V4.1-Flash 1017" - group "DeepSeek": - text: DeepSeek - - menuitemradio "DeepSeek-V4.1-Flash 1017" [checked] + - menuitemradio "DeepSeek-V4.1-Flash 新" [checked] - menuitemradio "DeepSeek-V4-Pro" + - menuitemradio "DeepSeek model with a very long release and capability description that exceeds the selector width Preview" + - menuitemradio "Invalid badge model" - group "External": - text: External - menuitemradio "DeepSeek-V4.1-Flash" diff --git a/apps/web/tests/expected/navigation-panes/live-result-evicted.expected.md b/apps/web/tests/expected/navigation-panes/live-result-evicted.expected.md new file mode 100644 index 0000000000..c81c4d0b19 --- /dev/null +++ b/apps/web/tests/expected/navigation-panes/live-result-evicted.expected.md @@ -0,0 +1,7 @@ +- tabpanel "Result": + - region "Raw result": + - heading "Raw result" [level=3] + - paragraph: Raw results are only available for live tool calls + - region "Rendered result": + - heading "Rendered result" [level=3] + - text: LIVE_RESULT_RENDERED diff --git a/apps/web/tests/expected/navigation-panes/live-result-running.expected.md b/apps/web/tests/expected/navigation-panes/live-result-running.expected.md new file mode 100644 index 0000000000..91844f0a86 --- /dev/null +++ b/apps/web/tests/expected/navigation-panes/live-result-running.expected.md @@ -0,0 +1,7 @@ +- tabpanel "Result": + - region "Raw result": + - heading "Raw result" [level=3] + - paragraph: Running… + - region "Rendered result": + - heading "Rendered result" [level=3] + - paragraph: Running… diff --git a/apps/web/tests/expected/navigation-panes/live-result.expected.md b/apps/web/tests/expected/navigation-panes/live-result.expected.md new file mode 100644 index 0000000000..8e37486cbb --- /dev/null +++ b/apps/web/tests/expected/navigation-panes/live-result.expected.md @@ -0,0 +1,10 @@ +- tabpanel "Result": + - region "Raw result": + - heading "Raw result" [level=3] + - text: "{" + - tree "Result JSON": + - treeitem "answer:42" + - text: "}" + - region "Rendered result": + - heading "Rendered result" [level=3] + - text: LIVE_RESULT_RENDERED diff --git a/apps/web/tests/expected/plugin-manager/header-natural-motion.expected.md b/apps/web/tests/expected/plugin-manager/header-natural-motion.expected.md index e1561e6f7c..648b1e5f3b 100644 --- a/apps/web/tests/expected/plugin-manager/header-natural-motion.expected.md +++ b/apps/web/tests/expected/plugin-manager/header-natural-motion.expected.md @@ -6,4 +6,5 @@ - Tabs / 1280px: resting title 28, actions 28, tabs 96; pinned actions 17, tabs 16, bar 66; stable range, reversible coordinates, same controls. - Tabs / 600px: resting title 28, actions 97, tabs 96; pinned actions 17, tabs 16, bar 66; stable range, reversible coordinates, same controls. - Tabs / 470px: resting title 28, actions 97, tabs 96; pinned actions 17, tabs 16, bar 66; stable range, reversible coordinates, same controls. -- Tabs / 390px: resting title 28, actions 146, tabs 96; pinned actions 66, tabs 16, bar 114; stable range, reversible coordinates, same controls. +- Tabs / 390px: resting title 28, actions 97, tabs 96; pinned actions 17, tabs 16, bar 66; stable range, reversible coordinates, same controls. +- Tabs / 320px: resting title 28, actions 146, tabs 96; pinned actions 66, tabs 16, bar 114; stable range, reversible coordinates, same controls. diff --git a/apps/web/tests/expected/schedule-header.expected.md b/apps/web/tests/expected/schedule-header.expected.md index 96bff76bee..eb305c44f7 100644 --- a/apps/web/tests/expected/schedule-header.expected.md +++ b/apps/web/tests/expected/schedule-header.expected.md @@ -7,7 +7,7 @@ en 440px: title and New single-line, New beside the title; contained; New retain en 390px: title and New single-line, New beside the title; contained; New retains focus en 430px: title and New single-line, New beside the title; contained; New retains focus en 1024px: title and New single-line, New beside the title; contained; New retains focus -en 509px with sidebar expanded: title single-line; New moves below the title +en 220px page column: title single-line; New moves below the title zh 1680px: title and New single-line, New beside the title; contained; New retains focus zh 800px: title and New single-line, New beside the title; contained; New retains focus zh 520px: title and New single-line, New beside the title; contained; New retains focus diff --git a/apps/web/tests/expected/sidebar-alignment/account-signed-in.expected.md b/apps/web/tests/expected/sidebar-alignment/account-signed-in.expected.md new file mode 100644 index 0000000000..81589bea33 --- /dev/null +++ b/apps/web/tests/expected/sidebar-alignment/account-signed-in.expected.md @@ -0,0 +1,20 @@ +{ + "wide": { + "height": 44, + "width": 260, + "footerCenterOffset": 0, + "leftOffset": -2, + "rightOffset": 2, + "paintedLeftOffset": -2, + "paintedRightOffset": 2 + }, + "rail": { + "height": 36, + "width": 36, + "footerCenterOffset": 0, + "leftOffset": -0.5, + "rightOffset": -0.5, + "paintedLeftOffset": -0.5, + "paintedRightOffset": -0.5 + } +} diff --git a/apps/web/tests/expected/sidebar-alignment/account-signed-out.expected.md b/apps/web/tests/expected/sidebar-alignment/account-signed-out.expected.md new file mode 100644 index 0000000000..3bacdca875 --- /dev/null +++ b/apps/web/tests/expected/sidebar-alignment/account-signed-out.expected.md @@ -0,0 +1,20 @@ +{ + "wide": { + "height": 32, + "width": 264, + "footerCenterOffset": 0, + "leftOffset": -4, + "rightOffset": 4, + "paintedLeftOffset": -1, + "paintedRightOffset": 1 + }, + "rail": { + "height": 36, + "width": 36, + "footerCenterOffset": 0, + "leftOffset": -0.5, + "rightOffset": -0.5, + "paintedLeftOffset": -0.5, + "paintedRightOffset": -0.5 + } +} diff --git a/apps/web/tests/expected/sidebar-alignment/geometry.expected.md b/apps/web/tests/expected/sidebar-alignment/geometry.expected.md new file mode 100644 index 0000000000..5282e0ea90 --- /dev/null +++ b/apps/web/tests/expected/sidebar-alignment/geometry.expected.md @@ -0,0 +1,7 @@ +# Sidebar alignment + +- search title is clipped: true +- unarchive hit target width: 24px +- clipped search title label gap: 6px +- clipped search title clearance from unarchive hit target: 2px +- clipped search title edge hit-tests to title: true diff --git a/apps/web/tests/expected/sidebar-drawer.expected.md b/apps/web/tests/expected/sidebar-drawer.expected.md new file mode 100644 index 0000000000..fafb1402b2 --- /dev/null +++ b/apps/web/tests/expected/sidebar-drawer.expected.md @@ -0,0 +1,24 @@ +390px: closed sidebar has no rail; main width 390px +open: main geometry retained; background inert; no resize handle +close: Session navigation, Plugins navigation, mask and Escape +retained: sidebar DOM and opener focus +768px: 56px rail; main width 712px; drawer focus transfers to the rail toggle +breakpoint motion: collapsed rail hides immediately and returns compact without crossfades; expanded sidebar and explicit toggles retain their slides +767px: hidden drawer; main width 767px; no page overflow +global panels: browser top inset 48px; macOS keeps its existing clearance +tooltips: sidebar and workspace controls escape the drawer and fit the viewport +search input: 16px on touch; 13px with a fine pointer +simulated keyboard: homepage and drawer retain 844px layout height without following the 360px visual viewport; Settings stays within the viewport +keyboard close: layout height stays 844px; sidebar search retained; Settings close remains reachable +shell coverage: drawer backing and mask cover 844px without synthetic safe-area insets +rotation: 844x390 then 390x844 restores shell height and sidebar breakpoint +folded landscape: 951px frame splits at 475.5px without a resize handle; unfolding restores 280px and search focus; portrait retains the drawer +fold transitions: sidebar controls follow the divider in both directions; reduced motion changes width without animation +drawer mask: opacity transitions from 0 to 1 on open and 1 to 0 on close +reduced motion: drawer and mask settle without transitions +Web header: only the sidebar opener remains, with 48px leading clearance +below 768px: plugin and automation pages use 20px insets; Web view tabs align at 20px, while Desktop and wider Web tabs retain 28px +touch selected Session: More remains visible; update time is hidden; pinned rows offer Unpin before More with Workspace action spacing +touch Workspace: actions appear only while expanded, including after folding the focused row +fine pointer: folded Workspace actions remain available on hover +fine pointer: selected Session actions stay hidden until hover, including at 390px diff --git a/apps/web/tests/expected/sidebar-menu-dismissal.expected.md b/apps/web/tests/expected/sidebar-menu-dismissal.expected.md new file mode 100644 index 0000000000..585e3877cd --- /dev/null +++ b/apps/web/tests/expected/sidebar-menu-dismissal.expected.md @@ -0,0 +1,2 @@ +View options and Session menus close when the sidebar becomes an inert drawer. +Focus returns to the visible sidebar opener; retained Session rows keep their DOM identity. diff --git a/apps/web/tests/fixtures/markdown-colon-file-links/session.v3.jsonl b/apps/web/tests/fixtures/markdown-colon-file-links/session.v3.jsonl new file mode 100644 index 0000000000..7ef9426138 --- /dev/null +++ b/apps/web/tests/fixtures/markdown-colon-file-links/session.v3.jsonl @@ -0,0 +1,10 @@ +{"version":3,"isSeeded":false,"createdAt":0,"cwd":"{{cwd}}","delegationDepth":0,"type":"session","id":"{{session:1}}"} +{"type":"turn/start","data":{"turn":1}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:1}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Link src/example.txt using relative :24-30, absolute :30, and relative #L12 destinations, plus line/column and invalid locations, localhost:3000, index.ts:24, mailto:10086, and a blocked script link."}],"source":{"kind":"user","rpcId":"{{rpc:1}}"},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Colon file references","messageSeqs":[3],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"[Relative colon:24–30](src/example.txt:24-30) · [Absolute colon:30]({{cwd}}/src/example.txt:30) · [Fragment:12](src/example.txt#L12) · [Column:24:5](src/example.txt:24:5) · [Zero line](src/example.txt:0) · [Reversed range](src/example.txt:30-24) · [Bare URL](index.ts:24) · [Blocked script](javascript:alert) · [Development server](localhost:3000) · [Mail](mailto:10086)"}],"source":{"provider":"deepseek-official","model":"deepseek-v4-flash","kind":"model"},"id":"{{message:3}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["[Relative colon:24–30](src/example.txt:24-30) · [Absolute colon:30]({{cwd}}/src/example.txt:30) · [Fragment:12](src/example.txt#L12) · [Column:24:5](src/example.txt:24:5) · [Zero line](src/example.txt:0) · [Reversed range](src/example.txt:30-24) · [Bare URL](index.ts:24) · [Blocked script](javascript:alert) · [Development server](localhost:3000) · [Mail](mailto:10086)"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"[Relative colon:24–30](src/example.txt:24-30) · [Absolute colon:30]({{cwd}}/src/example.txt:30) · [Fragment:12](src/example.txt#L12) · [Column:24:5](src/example.txt:24:5) · [Zero line](src/example.txt:0) · [Reversed range](src/example.txt:30-24) · [Bare URL](index.ts:24) · [Blocked script](javascript:alert) · [Development server](localhost:3000) · [Mail](mailto:10086)"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/apps/web/tests/fixtures/deepseek-model-label-1017/account.overlay.yml b/apps/web/tests/fixtures/model-badge/account.overlay.yml similarity index 100% rename from apps/web/tests/fixtures/deepseek-model-label-1017/account.overlay.yml rename to apps/web/tests/fixtures/model-badge/account.overlay.yml diff --git a/apps/web/tests/fixtures/deepseek-model-label-1017/session.v4.jsonl b/apps/web/tests/fixtures/model-badge/session.v4.jsonl similarity index 100% rename from apps/web/tests/fixtures/deepseek-model-label-1017/session.v4.jsonl rename to apps/web/tests/fixtures/model-badge/session.v4.jsonl diff --git a/apps/web/tests/forge-panel.e2e.ts b/apps/web/tests/forge-panel.e2e.ts index 2e537da246..10e99da527 100644 --- a/apps/web/tests/forge-panel.e2e.ts +++ b/apps/web/tests/forge-panel.e2e.ts @@ -8,6 +8,7 @@ import { promisify } from 'node:util' import type {} from '@deepseek-ai/dsh-experimental-forge-session' import { pluginRecordOf, type SessionEvent } from '@deepseek-ai/dsh-session' import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { initialShortcutConfig, type DesktopShortcutInput, type ShortcutRevision } from '@deepseek-ai/dsh-client-shortcuts/protocol' import { abbreviateHomePath } from '@deepseek-ai/dsh-util-workspace-path' import { chromium, type Browser, type Page } from 'playwright' import { expect, it, onTestFailed, onTestFinished } from 'vitest' @@ -17,6 +18,12 @@ import { } from './scaffold.ts' import { connectFreshWorkspaceZh, saveFailureShot, writeComposerDraft, ZH_BROWSER_LOCALE } from './support.ts' +declare global { + interface Window { + forgeKeyboard: { readonly revision: ShortcutRevision; deliver(input: DesktopShortcutInput): void } + } +} + const BUNDLE = fileURLToPath(new URL('../../../packages/experimental/forge-bundle', import.meta.url)) const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/lifecycle-chrome/session.v4.jsonl', import.meta.url)) const EXPECTED = fileURLToPath(new URL('./expected/forge-panel', import.meta.url)) @@ -70,13 +77,23 @@ if (args[0] === 'api' && args[5] === 'repos/octo/demo/pulls/9/merge') { process.stdout.write(JSON.stringify({ sha: 'c'.repeat(40), merged: true, message: 'Pull Request successfully merged' })) process.exit(0) } +if (args[0] === 'api' && args[3] === 'repos/octo/demo/pulls/7') { + process.stdout.write(JSON.stringify({ head: { sha: 'a'.repeat(40) }, state: 'open' })) + process.exit(0) +} +if (args[0] === 'api' && args[4] === 'POST' && ['repos/octo/demo/pulls/7/comments', 'repos/octo/demo/pulls/7/reviews'].includes(args[5])) { + const body = JSON.parse(readFileSync(0, 'utf8')) + appendFileSync(join(here, 'reviews.jsonl'), JSON.stringify({ endpoint: args[5], body }) + '\\n') + process.stdout.write(JSON.stringify({ html_url: 'https://github.com/octo/demo/pull/7#pullrequestreview-1' })) + process.exit(0) +} if (args[0] === 'pr' && args[1] === 'diff') { const refused = join(here, 'pr-' + args[2] + '.refused') if (existsSync(refused)) { process.stderr.write(readFileSync(refused, 'utf8')) process.exit(1) } - process.stdout.write(readFileSync(join(here, 'pr-' + args[2] + '.diff'), 'utf8')) + await new Promise(resolve => process.stdout.write(readFileSync(join(here, 'pr-' + args[2] + '.diff'), 'utf8'), resolve)) process.exit(0) } if (args[0] === 'api' && args[3] === '-H' && args[4] === 'Accept: application/vnd.github.raw+json') { @@ -152,6 +169,7 @@ const PR9_REFUSAL = [ /** Line `n` of `src/panel.ts` before (`old`) or after (`new`) pull request 7, which changes lines 2 and 15 of its 20. */ function panelLine(n: number, side: 'old' | 'new'): string { if (n === 2) return side === 'old' ? 'export const n2 = 2' : 'export const n2 = \'two\'' + if (n === 7) return 'export const n7 = "Long unchanged context stays wrapped while switching between Sessions and reviewing the same file"' if (n === 15) return side === 'old' ? 'export const n15 = 15' : 'export const n15 = \'fifteen\'' return `export const n${n} = ${n}` } @@ -199,9 +217,12 @@ interface PanelLaunch { * Launch the bundle against a GitHub checkout of `main` and `release` and open the New Session screen of that workspace. * @param failureShot - name of the screenshot saved when the test fails. * @param scenario - normal conversation, a restoration follow-up, or a numbered PR lookup. + * @param options - desktop transport, initial checkout branch, and touch input for the browser. * @returns the scaffold, page, and checkout. */ -async function launchPanel(failureShot: string, scenario: 'normal' | 'follow-up' | 'reference' = 'normal'): Promise { +async function launchPanel(failureShot: string, scenario: 'normal' | 'follow-up' | 'reference' = 'normal', + options: { desktop?: 'darwin' | 'win32'; branch?: string; hasTouch?: boolean } = {}): Promise { + const { desktop } = options const resources: { scaffold?: WebScaffold; browser?: Browser } = {} const root = await mkdtemp(join(tmpdir(), 'dsh-forge-panel-')) onTestFinished(async () => { @@ -257,6 +278,7 @@ async function launchPanel(failureShot: string, scenario: 'normal' | 'follow-up' await git(project, 'remote', 'add', 'origin', 'https://github.com/octo/demo.git') await git(project, 'branch', 'release') await git(project, 'commit', '--quiet', '--allow-empty', '-m', 'main only') + if (options.branch !== undefined) await git(project, 'checkout', '--quiet', '-b', options.branch) if (scenario === 'reference') { // The model's confined command must own the CLI double's writable call log. const modelBin = join(project, '.fixture-bin') @@ -267,7 +289,31 @@ async function launchPanel(failureShot: string, scenario: 'normal' | 'follow-up' const browser = await chromium.launch() resources.browser = browser - const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE, timezoneId: 'Asia/Shanghai' }) + const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE, timezoneId: 'Asia/Shanghai', + hasTouch: options.hasTouch ?? false }) + if (desktop !== undefined) { + // Only Electron's keyboard transport is substituted; the product registry and menus remain real. + await page.addInitScript(({ device, config }) => { + const mark = () => { document.documentElement.dataset.platform = device } + if (document.documentElement === null) window.addEventListener('DOMContentLoaded', mark) + else mark() + const listeners = new Set<(input: DesktopShortcutInput) => void>() + Object.assign(window, { + forgeKeyboard: { revision: config.revision, + deliver: (input: DesktopShortcutInput) => { for (const listener of listeners) listener(input) } }, + dshDesktop: { protocolVersion: 1, + shortcuts: { get: async () => config, subscribe: () => () => {}, recording: async () => {}, edit: async () => { throw new Error('Unused preference edit') } }, + keyboard: { + subscribe: (listener: (input: DesktopShortcutInput) => void) => { + listeners.add(listener) + return () => { listeners.delete(listener) } + }, + closeWindow: async () => {}, + }, + }, + }) + }, { device: desktop, config: { ...initialShortcutConfig(), status: 'ready' as const } }) + } const tripwire = watchConsole(page) onTestFailed(async () => { await saveFailureShot(page, failureShot) }) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) @@ -297,7 +343,7 @@ async function requestReleaseWorktree(failureShot: string, followUp = false): Pr const { page } = launched const option = page.locator('[data-forge-worktree]') await option.waitFor({ timeout: 20_000 }) - await option.getByRole('checkbox', { name: '工作树', exact: true }).check() + await option.getByRole('checkbox', { name: '新建工作树', exact: true }).check() await option.getByRole('button', { name: '基于 main', exact: true }).click() await page.getByRole('menuitem', { name: 'release', exact: true }).click() await option.getByRole('button', { name: '基于 release', exact: true }).waitFor() @@ -391,6 +437,23 @@ it.skipIf(MODE === 'record')('starts a Session in a worktree before its first me const header = page.locator('[data-forge-header]') await expect.poll(() => header.getAttribute('aria-label'), { timeout: 20_000 }).toBe('本会话的变更请求需要处理') expect(await header.getAttribute('aria-pressed')).toBe('false') + + // The Session row marks the worktree and its change request's state, and its hover card names both. + const row = page.locator(`[data-row-key="session:${sessionId}"]`) + await row.getByRole('img', { name: '在自己的工作树中', exact: true }).waitFor({ timeout: 20_000 }) + await row.getByRole('img', { name: '变更请求状态:需要处理 · 检查失败', exact: true }).waitFor({ timeout: 20_000 }) + // A fresh page cannot retain the previous browser source's status values. + await page.goto('about:blank') + await expect.poll(() => scaffold.ctx.storageDomain.get('forge_panel_change_requests')).toBeUndefined() + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await row.getByRole('img', { name: '变更请求状态:需要处理 · 检查失败', exact: true }).waitFor({ timeout: 20_000 }) + await row.hover() + const rowHover = page.locator('[data-forge-session-hover]') + await rowHover.getByText('#7 Add the pull request panel', { exact: true }).waitFor({ timeout: 20_000 }) + await compareOrRefreshGolden(join(EXPECTED, 'session-row-hover.expected.md'), + await captureStableAria(page, '[data-forge-session-hover]', scaffold.workspaceCwd, { replacements: [[branch, 'dsh-{{worktree}}']] }), MODE) + await page.mouse.move(0, 0) + await rowHover.waitFor({ state: 'detached' }) await header.click() await expect.poll(() => header.getAttribute('aria-pressed')).toBe('true') const panel = page.locator('[data-forge-panel]') @@ -547,7 +610,7 @@ it.skipIf(MODE === 'record')('marks a draft change request ready for review and }) it.skipIf(MODE === 'record')('shows the local changes of the Session by scope and the changes of its change requests, expanding unchanged lines', async () => { - const { scaffold, page, root, project, tripwire } = await launchPanel('web-e2e-forge-panel-changes') + const { scaffold, page, root, project, tripwire } = await launchPanel('web-e2e-forge-panel-changes', 'follow-up') // origin/main stops at the twelve-line README; the branch adds a commit, a staged file, an edit, and an untracked file. await writeFile(join(project, 'README.md'), Array.from({ length: 12 }, (_, index) => `line ${index + 1}\n`).join('')) await git(project, 'commit', '--quiet', '-am', 'readme') @@ -582,8 +645,47 @@ it.skipIf(MODE === 'record')('shows the local changes of the Session by scope an .toEqual(['README.md', 'src/app.ts', 'src/extra.ts', 'notes.md']) await local.locator('[data-forge-diff-file="notes.md"]').getByText('新文件 · 未提交', { exact: true }).waitFor() const readme = local.locator('[data-forge-diff-file="README.md"]') + await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) + await page.evaluate(() => navigator.clipboard.writeText('unchanged')) + await readme.locator('[data-forge-file-label]').click() + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe('unchanged') + await readme.locator('[data-forge-path-spacer]').click() + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe('unchanged') await readme.getByRole('button', { name: '6 行未变更', exact: true }).click() await readme.getByText('line 1', { exact: true }).first().waitFor({ timeout: 20_000 }) + await local.getByRole('button', { name: '全部 4 个文件', exact: true }).click() + const tree = page.getByRole('menu') + await tree.getByRole('textbox', { name: '筛选文件…' }).fill('extra') + await tree.getByRole('menuitem', { name: 'src/extra.ts', exact: true }).click() + for (const [path, comment] of [['src/app.ts', '请解释这行代码'], ['src/extra.ts', '请检查这项修改']] as const) { + const section = local.locator(`[data-forge-diff-file="${path}"]`) + const lineNumber = section.getByRole('button', { name: '选择第 R1 行', exact: true }) + await lineNumber.hover() + expect(await lineNumber.evaluate(element => getComputedStyle(element).backgroundColor)).toBe('rgba(0, 0, 0, 0)') + const content = await section.locator('[data-review-code]').first().boundingBox() + const fileBox = await section.boundingBox() + if (content === null || fileBox === null) throw new Error('Added file must be visible') + expect(content.x - fileBox.x).toBeLessThan(70) + await lineNumber.click() + expect(await section.getByRole('textbox', { name: '留下评论…' }).count()).toBe(0) + await section.getByRole('button', { name: '添加评论', exact: true }).click() + await section.getByRole('textbox', { name: '留下评论…' }).fill(comment) + await section.locator('[data-forge-comment]').getByRole('button', { name: '添加到输入框', exact: true }).click() + await section.locator('[data-forge-review-thread="agent"]').getByText(comment, { exact: true }).waitFor() + } + await compareOrRefreshGolden(join(EXPECTED, 'review-quotes.expected.md'), + await captureStableAria(page, '[data-composer-input]', scaffold.workspaceCwd), MODE) + expect((await readPersistedEvents(scaffold, sessionId)).some(event => event.type === 'user/message' + && event.data.content.some(block => block.type === 'text' && block.text.includes('## User Comment')))).toBe(false) + const reviewed = scaffold.whenTurnSettled() + await page.locator('[data-composer-input]').first().press('Enter') + await reviewed + await page.getByText(FOLLOW_UP_REPLY, { exact: true }).waitFor() + const comments = (await readPersistedEvents(scaffold, sessionId)).flatMap(event => event.type === 'user/message' + ? event.data.content.flatMap(block => block.type === 'text' && block.text.includes('## User Comment') ? [block.text] : []) : []) + expect(comments).toHaveLength(1) + await compareOrRefreshGolden(join(EXPECTED, 'review-message.expected.md'), comments.join('').trimEnd(), MODE) + await local.getByRole('button', { name: '分支', exact: true }).click() await page.getByRole('menuitem', { name: '已暂存', exact: true }).click() await local.getByText('暂存区与 HEAD 比较', { exact: true }).waitFor() @@ -599,12 +701,162 @@ it.skipIf(MODE === 'record')('shows the local changes of the Session by scope an await change.getByRole('button', { name: '查看 #7 的变更:新增 128 行,删除 14 行', exact: true }).click() const view = page.locator('[data-forge-changes="change"]') const panelFile = view.locator('[data-forge-diff-file="src/panel.ts"]') + const title = await view.getByText('Add the pull request panel', { exact: true }).boundingBox() + const submit = await view.getByRole('button', { name: '提交评审', exact: true }).boundingBox() + if (title === null || submit === null) throw new Error('PR title and submit action must be visible') + expect(Math.abs(title.y + title.height / 2 - submit.y - submit.height / 2)).toBeLessThan(1) + await panelFile.getByRole('button', { name: '6 行未变更', exact: true }).waitFor({ timeout: 20_000 }) await compareOrRefreshGolden(join(EXPECTED, 'changes-view.expected.md'), await captureStableAria(page, '[data-forge-changes="change"]', scaffold.workspaceCwd), MODE) + await page.getByRole('button', { name: '全屏', exact: true }).click() + await view.getByRole('navigation').getByRole('button', { name: 'src/panel.ts', exact: true }).waitFor() + const fileTree = view.getByRole('navigation') + const directoryName = await fileTree.getByText('src', { exact: true }).boundingBox() + const fileName = await fileTree.getByText('panel.ts', { exact: true }).boundingBox() + if (directoryName === null || fileName === null) throw new Error('Tree entries must be visible') + expect(fileName.x).toBeGreaterThan(directoryName.x) + await fileTree.getByRole('textbox', { name: '筛选文件…' }).fill('src') + await fileTree.getByRole('textbox', { name: '筛选文件…' }).fill('') + await fileTree.getByRole('button', { name: 'src', exact: true }).click() + await view.getByRole('button', { name: '文件树', exact: true }).click() + expect(await fileTree.isVisible()).toBe(false) + await view.getByRole('button', { name: '文件树', exact: true }).click() + expect(await fileTree.getByRole('button', { name: 'src', exact: true }).getAttribute('aria-expanded')).toBe('false') + await fileTree.getByRole('textbox', { name: '筛选文件…' }).fill('src') + await view.getByRole('button', { name: '文件树', exact: true }).click() + await view.getByRole('button', { name: '文件树', exact: true }).click() + expect(await fileTree.getByRole('textbox', { name: '筛选文件…' }).inputValue()).toBe('src') + await compareOrRefreshGolden(join(EXPECTED, 'file-tree-toggle.expected.md'), + await captureStableAria(page, '[data-forge-changes="change"]', scaffold.workspaceCwd), MODE) + await fileTree.getByRole('textbox', { name: '筛选文件…' }).fill('') + await fileTree.getByRole('button', { name: 'src', exact: true }).click() + const resizeTree = view.getByRole('separator', { name: '调整文件树宽度', exact: true }) + const originalWidth = Number(await resizeTree.getAttribute('aria-valuenow')) + const handle = await resizeTree.boundingBox() + if (handle === null) throw new Error('Tree resize handle must be visible') + await page.mouse.move(handle.x + handle.width / 2, handle.y + 100) + await page.mouse.down() + await page.mouse.move(handle.x - 80, handle.y + 100, { steps: 5 }) + await page.mouse.up() + expect(Number(await resizeTree.getAttribute('aria-valuenow'))).toBeGreaterThan(originalWidth) + await resizeTree.press('Home') + expect(await fileTree.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + const searchBounds = await fileTree.getByRole('textbox').boundingBox() + const treeBounds = await fileTree.boundingBox() + if (searchBounds === null || treeBounds === null) throw new Error('Tree search must be visible') + expect(searchBounds.x + searchBounds.width).toBeLessThanOrEqual(treeBounds.x + treeBounds.width) + await resizeTree.press('ArrowLeft') + const resizedWidth = await resizeTree.getAttribute('aria-valuenow') + await view.getByRole('button', { name: '文件树', exact: true }).click() + await view.getByRole('button', { name: '文件树', exact: true }).click() + expect(await resizeTree.getAttribute('aria-valuenow')).toBe(resizedWidth) + for (const side of ['old', 'new'] as const) { + const firstLine = await panelFile.locator(`[data-review-code][data-${side}-line="2"]`).boundingBox() + const lastLine = await panelFile.locator(`[data-review-code][data-${side}-line="4"]`).boundingBox() + if (firstLine === null || lastLine === null) throw new Error('Review text must be visible') + await page.mouse.move(firstLine.x + 7, firstLine.y + firstLine.height / 2) + await page.mouse.down() + await page.mouse.move(lastLine.x + 120, lastLine.y + lastLine.height / 2, { steps: 5 }) + await page.mouse.up() + await panelFile.getByRole('button', { name: '添加评论', exact: true }).waitFor() + expect(await panelFile.getByRole('textbox', { name: '留下评论…' }).count()).toBe(0) + const opposite = panelFile.locator(`[data-diff-side="${side === 'old' ? 'R' : 'L'}"]`).first() + expect(await opposite.evaluate(element => getComputedStyle(element).userSelect)).toBe('none') + await page.keyboard.press('ControlOrMeta+c') + const copied = await page.evaluate(() => navigator.clipboard.readText()) + expect(copied).toContain(side === 'old' ? 'n2 = 2' : "n2 = 'two'") + expect(copied).not.toContain(side === 'old' ? "n2 = 'two'" : 'n2 = 2') + expect(copied.match(/n3 = 3/g)).toHaveLength(1) + } + await panelFile.getByRole('button', { name: '添加评论', exact: true }).click() + await panelFile.getByRole('region', { name: '评论 R2–4', exact: true }).waitFor() + await panelFile.getByRole('textbox', { name: '留下评论…' }).fill('请核对边界条件') + await panelFile.getByRole('button', { name: 'GitHub 评论选项', exact: true }).click() + await page.getByRole('menuitem', { name: '发布单条评论', exact: true }).waitFor() + await page.keyboard.press('Escape') + await compareOrRefreshGolden(join(EXPECTED, 'review-composer.expected.md'), + await captureStableAria(page, '[data-forge-comment]', scaffold.workspaceCwd), MODE) + await page.getByRole('button', { name: '退出全屏', exact: true }).click() + const commentForm = panelFile.locator('[data-forge-comment]') + expect(await commentForm.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + await page.getByRole('button', { name: '全屏', exact: true }).click() + await page.setViewportSize({ width: 390, height: 780 }) + await commentForm.getByRole('textbox').focus() + expect(await commentForm.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + const narrowComment = await commentForm.boundingBox() + if (narrowComment === null) throw new Error('Comment form must remain visible') + expect(narrowComment.x + narrowComment.width).toBeLessThanOrEqual(390) + await page.setViewportSize({ width: 1680, height: 1000 }) + await panelFile.getByRole('button', { name: '开始评审', exact: true }).click() + await panelFile.locator('[data-forge-review-thread="pending"]').getByText('请核对边界条件', { exact: true }).waitFor() + await page.setViewportSize({ width: 1680, height: 500 }) + const changesScroll = view.locator('[data-forge-changes-scroll]') + await changesScroll.evaluate((element) => { element.scrollTop = element.scrollHeight }) + const reviewScroll = await changesScroll.evaluate(element => element.scrollTop) + expect(reviewScroll).toBeGreaterThan(0) + await view.getByRole('button', { name: '提交评审 (1)', exact: true }).click() + const reviewDialog = page.getByRole('dialog', { name: '提交评审', exact: true }) + await reviewDialog.waitFor() + expect(await reviewDialog.getByRole('textbox').count()).toBe(1) + expect(await reviewDialog.getByText('请核对边界条件', { exact: true }).count()).toBe(0) + expect(await changesScroll.evaluate(element => element.scrollTop)).toBe(reviewScroll) + await page.setViewportSize({ width: 390, height: 780 }) + expect(await reviewDialog.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + const narrowDialog = await reviewDialog.boundingBox() + if (narrowDialog === null) throw new Error('Review dialog must remain visible') + expect(narrowDialog.x + narrowDialog.width).toBeLessThanOrEqual(390) + await page.setViewportSize({ width: 1680, height: 500 }) + await page.keyboard.press('Escape') + expect(await reviewDialog.count()).toBe(0) + await view.getByRole('button', { name: '提交评审 (1)', exact: true }).click() + await reviewDialog.getByRole('textbox', { name: '评审总结…', exact: true }).fill('需要补充边界检查') + await reviewDialog.getByRole('radio', { name: '请求更改', exact: true }).check() + await compareOrRefreshGolden(join(EXPECTED, 'submit-review.expected.md'), + await captureStableAria(page, '[role="dialog"][aria-label="提交评审"]', scaffold.workspaceCwd), MODE) + await reviewDialog.getByRole('button', { name: '提交评审', exact: true }).click() + await page.getByRole('alert').filter({ hasText: '已提交评审' }).waitFor() + await panelFile.locator('[data-forge-review-thread="published"]').getByText('请核对边界条件', { exact: true }).waitFor() + await compareOrRefreshGolden(join(EXPECTED, 'review-record.expected.md'), await captureStableAria(page, '[data-forge-review-thread="published"]', scaffold.workspaceCwd), MODE) + const published: unknown = JSON.parse((await readFile(join(root, 'reviews.jsonl'), 'utf8')).trim()) + expect(published).toEqual({ endpoint: 'repos/octo/demo/pulls/7/reviews', body: { commit_id: HEAD7, event: 'REQUEST_CHANGES', body: '需要补充边界检查', + comments: [{ path: 'src/panel.ts', side: 'RIGHT', line: 4, start_line: 2, start_side: 'RIGHT', body: '请核对边界条件' }] } }) + await page.getByRole('button', { name: '退出全屏', exact: true }).click() await panelFile.getByRole('button', { name: '6 行未变更', exact: true }).click() await panelFile.getByText('export const n6 = 6', { exact: true }).first().waitFor({ timeout: 20_000 }) await panelFile.getByRole('button', { name: '2 行未变更', exact: true }).waitFor() + await panelFile.getByRole('button', { name: 'panel.ts 的更多操作', exact: true }).click() + await page.getByRole('menuitem', { name: '复制路径', exact: true }).click() + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe('src/panel.ts') + const docsFile = view.locator('[data-forge-diff-file="docs/panel.md"]') + await docsFile.getByRole('button', { name: '收起 panel.md', exact: true }).click() + await page.setViewportSize({ width: 1280, height: 500 }) + const scrollport = view.locator('[data-forge-changes-scroll]') + await scrollport.evaluate((element) => { element.scrollTop = 100; element.dispatchEvent(new Event('scroll')) }) + await panelFile.locator('[data-forge-file-label]').click() + const position = await scrollport.evaluate(element => element.scrollTop) + expect(position).toBeGreaterThan(0) + const originalFile = await panelFile.elementHandle() + await page.getByRole('button', { name: /^(?:New session|新.*会话)$/ }).last().click() + await expect.poll(() => view.isVisible()).toBe(false) + await page.locator(`[data-row-key="session:${sessionId}"]`).click() + await panelFile.getByText('export const n6 = 6', { exact: true }).first().waitFor({ timeout: 20_000 }) + expect(await panelFile.evaluate((element, original) => element === original, originalFile)).toBe(true) + await expect.poll(() => docsFile.getAttribute('data-expanded')).toBe(null) + await expect.poll(() => scrollport.evaluate(element => element.scrollTop)).toBe(position) + expect(await panelFile.locator('[data-forge-review-thread="published"]').count()).toBe(1) + await page.setViewportSize({ width: 1680, height: 1000 }) + await page.locator('[data-dockkit-tab]').filter({ hasText: '#7 的变更' }).locator('[data-dockkit-tab-close]').click() + await expect.poll(() => view.count()).toBe(0) + if (await header.getAttribute('aria-pressed') !== 'true') await header.click() + await change.getByRole('button', { name: '查看 #7 的变更:新增 128 行,删除 14 行', exact: true }).click() + await panelFile.getByRole('button', { name: '6 行未变更', exact: true }).waitFor() + expect(await panelFile.locator('[data-forge-review-thread]').count()).toBe(0) + await view.getByRole('button', { name: '提交评审', exact: true }).click() + await reviewDialog.waitFor() + expect(await reviewDialog.getByRole('textbox').inputValue()).toBe('') + expect(await reviewDialog.getByRole('radio', { name: '评论', exact: true }).isChecked()).toBe(true) + await page.keyboard.press('Escape') // GitHub refuses the diff of pull request 9 as too large, so its view offers the web page instead of a retry. scaffold.ctx.forgeSession.track(session, DRAFT) @@ -627,3 +879,380 @@ it.skipIf(MODE === 'record')('shows the local changes of the Session by scope an expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) }) + + +it.skipIf(MODE === 'record')('retains a wrapped code line and tree width across tab and Session switches', async () => { + const { page, scaffold, root, tripwire } = await launchPanel('web-e2e-forge-reading-position') + const paths = ['src/before-0.ts', 'src/before-1.ts', 'src/before-2.ts', 'src/server.ts', 'src/after.ts'] + const patch = paths.map(path => [ + `diff --git a/${path} b/${path}`, 'index 1111111..2222222 100644', `--- a/${path}`, `+++ b/${path}`, + '@@ -1,650 +1,650 @@', '-const oldValue = true;', '+const newValue = true;', + ...Array.from({ length: 649 }, (_, index) => ` const value_${index + 2} = "${'x'.repeat(24 + (index + 2) % 70)}";`), '', + ].join('\n')).join('\n') + await writeFile(join(root, 'pr-7.diff'), patch) + const { settled } = await sendPrompt(page, scaffold) + const sessionId = await settled + const session = scaffold.ctx.sessions.get(sessionId) + if (session === undefined) throw new Error('Session must stay loaded') + scaffold.ctx.forgeSession.track(session, PULL) + // Keep the file tree and the Session sidebar visible together on every platform. + await page.setViewportSize({ width: 2400, height: 700 }) + await page.locator('[data-forge-header]').click() + await page.locator(`[data-forge-change="${PULL}"]`).getByRole('button', { name: /查看 #7 的变更/ }).click() + const view = page.locator('[data-forge-changes="change"]') + const scroll = view.locator('[data-forge-changes-scroll]') + const file = view.locator('[data-forge-diff-file="src/server.ts"]') + const code = file.locator('[data-review-code][data-new-line="300"]') + const separator = view.getByRole('separator', { name: '调整文件树宽度', exact: true }) + const beforeFile = view.locator('[data-forge-diff-file="src/before-0.ts"]') + // ResizeObserver runs between rendering frames, including when an ancestor becomes hidden. + const layout = () => page.evaluate(() => new Promise((resolve) => { + requestAnimationFrame(() => { requestAnimationFrame(() => { resolve() }) }) + })) + const reading = () => file.evaluate((element) => { + const viewport = element.closest('[data-forge-changes-scroll]')! + const top = Math.max(viewport.getBoundingClientRect().top, element.firstElementChild!.getBoundingClientRect().bottom) + const line = [...element.querySelectorAll('[data-review-code][data-new-line]')] + .find(row => row.getBoundingClientRect().bottom > top + 1)! + return { path: element.getAttribute('data-forge-diff-file'), line: line.dataset.newLine, + offset: line.getBoundingClientRect().top - top, scrollTop: viewport.scrollTop } + }) + await code.waitFor() + for (const width of [240, 320]) { + if (width === 320) { + for (let step = 0; step < 4; step += 1) await separator.press('ArrowLeft') + await beforeFile.getByRole('button', { name: '收起 before-0.ts', exact: true }).click() + } + await layout() + await code.evaluate((element) => { + const viewport = element.closest('[data-forge-changes-scroll]')! + const header = element.closest('[data-forge-diff-file]')!.firstElementChild! + const top = viewport.getBoundingClientRect().top + header.getBoundingClientRect().height + viewport.scrollTop += element.getBoundingClientRect().top - top + 7 + }) + await layout() + const position = await reading() + expect(position.line).toBe('300') + expect(position.offset).toBe(-7) + for (const destination of ['tab', 'session', 'tab', 'session']) { + if (destination === 'tab') await page.getByRole('tab').filter({ hasText: '变更请求' }).click() + else await page.getByRole('button', { name: '新建会话', exact: true }).last().click() + await expect.poll(() => view.isVisible()).toBe(false) + await layout() + expect(await scroll.evaluate(element => element.clientWidth)).toBe(0) + if (destination === 'tab') await page.getByRole('tab').filter({ hasText: '#7 的变更' }).click() + else await page.locator(`[data-row-key="session:${sessionId}"]`).click() + await view.waitFor() + await layout() + expect(await separator.getAttribute('aria-valuenow')).toBe(String(width)) + expect(await reading()).toEqual(position) + expect(await beforeFile.getAttribute('data-expanded')).toBe(width === 240 ? '' : null) + } + } + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) +}) + +it.skipIf(MODE === 'record')('uses horizontal arrows to navigate files while preserving vertical scrolling and comment editing', async () => { + const { scaffold, page, project, tripwire } = await launchPanel('web-e2e-forge-file-keys') + await git(project, 'update-ref', 'refs/remotes/origin/main', 'HEAD') + await git(project, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main') + for (const path of ['a.txt', 'b.txt', 'c.txt']) { + await writeFile(join(project, path), Array.from({ length: path === 'c.txt' ? 1 : 80 }, (_, line) => `line ${line + 1}\n`).join('')) + } + const { settled } = await sendPrompt(page, scaffold) + await settled + await page.locator('[data-forge-header]').click() + await page.locator('[data-forge-panel]').getByRole('button', { name: '刷新状态', exact: true }).click() + await page.locator('[data-forge-local]').getByRole('button', { name: /查看本会话的变更:3 个文件/ }).click() + const view = page.locator('[data-forge-changes="local"]') + const scroll = view.getByRole('region', { name: '全部 3 个文件', exact: true }) + await scroll.waitFor() + await page.getByRole('button', { name: '全屏', exact: true }).click() + await page.setViewportSize({ width: 1280, height: 500 }) + const tree = view.getByRole('navigation') + const a = view.locator('[data-forge-diff-file="a.txt"]') + const b = view.locator('[data-forge-diff-file="b.txt"]') + const c = view.locator('[data-forge-diff-file="c.txt"]') + await b.getByRole('button', { name: '收起 b.txt', exact: true }).click() + await scroll.evaluate((element) => { element.scrollTop = 0 }) + await scroll.focus() + await page.keyboard.press('ArrowLeft') + expect(await scroll.evaluate(element => element.scrollTop)).toBe(0) + expect(await scroll.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none') + await page.keyboard.press('ArrowDown') + await expect.poll(() => scroll.evaluate(element => element.scrollTop)).toBeGreaterThan(0) + await scroll.evaluate((element) => { element.scrollTop = 0 }) + await page.keyboard.press('ArrowRight') + await expect.poll(() => tree.getByRole('button', { name: 'b.txt', exact: true }).getAttribute('aria-current')).toBe('true') + expect(await b.getAttribute('data-expanded')).toBe('') + expect(await scroll.evaluate(element => document.activeElement === element)).toBe(true) + await page.keyboard.press('ArrowRight') + await expect.poll(() => tree.getByRole('button', { name: 'c.txt', exact: true }).getAttribute('aria-current')).toBe('true') + const lastPosition = await scroll.evaluate(element => element.scrollTop) + const lastBox = await c.boundingBox(), viewport = await scroll.boundingBox() + if (lastBox === null || viewport === null) throw new Error('The last file and scroll viewport must be rendered') + expect(lastBox.y).toBeGreaterThan(viewport.y) + expect(lastBox.y).toBeLessThan(viewport.y + viewport.height) + await page.keyboard.press('ArrowRight') + expect(await scroll.evaluate(element => element.scrollTop)).toBe(lastPosition) + await page.keyboard.press('ArrowLeft') + await expect.poll(() => tree.getByRole('button', { name: 'b.txt', exact: true }).getAttribute('aria-current')).toBe('true') + await page.keyboard.press('ArrowLeft') + await expect.poll(() => tree.getByRole('button', { name: 'a.txt', exact: true }).getAttribute('aria-current')).toBe('true') + await b.evaluate((element) => { + const viewport = element.closest('[data-forge-changes-scroll]')! + viewport.scrollTop += element.getBoundingClientRect().top - viewport.getBoundingClientRect().top + 100 + }) + await page.keyboard.press('ArrowRight') + await expect.poll(() => tree.getByRole('button', { name: 'c.txt', exact: true }).getAttribute('aria-current')).toBe('true') + await tree.getByRole('button', { name: 'a.txt', exact: true }).click() + await a.getByRole('button', { name: '选择第 R1 行', exact: true }).click() + await a.getByRole('button', { name: '添加评论', exact: true }).click() + const input = a.getByRole('textbox', { name: '留下评论…', exact: true }) + await input.fill('keep editing') + await input.press('ArrowLeft') + expect(await input.evaluate(element => (element as HTMLTextAreaElement).selectionStart)).toBe(11) + await input.press('ArrowRight') + expect(await input.inputValue()).toBe('keep editing') + expect(await tree.getByRole('button', { name: 'a.txt', exact: true }).getAttribute('aria-current')).toBe('true') + expect(await input.evaluate(element => document.activeElement === element)).toBe(true) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) +}) + +it.skipIf(MODE === 'record')('keeps localized status columns separate and long file lists inside one vertical scroller', async () => { + const { scaffold, page, project, tripwire } = await launchPanel('web-e2e-forge-layout') + await page.locator('[data-forge-current-branch]').waitFor() + await compareOrRefreshGolden(join(EXPECTED, 'worktree-option.expected.md'), + await captureStableAria(page, '[data-forge-worktree]', scaffold.workspaceCwd), MODE) + await page.getByRole('button', { name: '收起侧边栏', exact: true }).click() + await page.setViewportSize({ width: 390, height: 844 }) + await expect.poll(() => page.locator('[data-forge-worktree]') + .evaluate(element => element.getBoundingClientRect().right)).toBeLessThanOrEqual(390) + await page.setViewportSize({ width: 1680, height: 1000 }) + await git(project, 'update-ref', 'refs/remotes/origin/main', 'HEAD') + await git(project, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main') + await mkdir(join(project, 'src')) + await Promise.all(Array.from({ length: 60 }, (_, index) => writeFile(join(project, 'src', `file-${index}.ts`), `export const value = ${index}\n`))) + const { settled } = await sendPrompt(page, scaffold) + const session = scaffold.ctx.sessions.get(await settled) + if (session === undefined) throw new Error('The Session must stay loaded') + scaffold.ctx.forgeSession.track(session, PULL) + await page.locator('[data-forge-header]').click() + const panel = page.locator('[data-forge-panel]') + await panel.getByRole('button', { name: '刷新状态', exact: true }).click() + const local = panel.locator('[data-forge-local]') + await local.locator('button[aria-expanded]').click() + await expect.poll(() => local.locator('li').count(), { timeout: 20_000 }).toBe(60) + for (const width of [1280, 390]) { + await page.setViewportSize({ width, height: 700 }) + const outerScrollers = await panel.evaluate((element) => { + const scrollers: string[] = [] + for (let ancestor = element.parentElement; ancestor !== null; ancestor = ancestor.parentElement) { + if (/auto|scroll/.test(getComputedStyle(ancestor).overflowY) && ancestor.scrollHeight > ancestor.clientHeight + 1) { + scrollers.push(ancestor.className) + } + } + return scrollers + }) + expect(outerScrollers).toEqual([]) + expect(await panel.evaluate(element => element.scrollHeight > element.clientHeight)).toBe(true) + await local.locator('li').last().scrollIntoViewIfNeeded() + expect(await local.locator('li').last().isVisible()).toBe(true) + } + await local.locator('button[aria-expanded]').click() + const change = panel.locator(`[data-forge-change="${PULL}"]`) + for (const [locale, labels] of [ + ['zh', ['检查', '评审', '合并', '变更']], + ['en', ['Checks', 'Review', 'Merge', 'Changes']], + ['de', ['Prüfungen', 'Review', 'Zusammenführen', 'Änderungen']], + ] as const) { + await scaffold.ctx.settings.update('locale', { preference: locale }) + await change.getByText(labels[0], { exact: true }).waitFor() + if (locale === 'en') { + await compareOrRefreshGolden(join(EXPECTED, 'local-card.expected.md'), + await captureStableAria(page, '[data-forge-local]', scaffold.workspaceCwd), MODE) + } + for (const width of [1280, 390]) { + await page.setViewportSize({ width, height: 700 }) + await expect.poll(() => change.evaluate(element => element.getBoundingClientRect().right)).toBeLessThanOrEqual(width) + await change.getByText(labels[3], { exact: true }).hover() + for (const label of labels) { + const geometry = await change.getByText(label, { exact: true }).evaluate((element) => { + const value = element.nextElementSibling + if (value === null) throw new Error('Status label must have a value') + const range = document.createRange() + range.selectNodeContents(element) + const name = range.getBoundingClientRect() + const content = value.getBoundingClientRect() + const row = element.parentElement! + const icon = element.previousElementSibling! + const rowRect = row.getBoundingClientRect() + return { + gap: content.left - name.right, + iconInset: icon.getBoundingClientRect().left - rowRect.left, + rowInset: rowRect.left - row.closest('[data-forge-change]')!.getBoundingClientRect().left, + labelWidth: element.getBoundingClientRect().width, + valueOffset: content.left - rowRect.left, + right: content.right, + width: document.documentElement.clientWidth, + } + }) + expect(geometry.gap).toBeGreaterThanOrEqual(8) + expect(geometry.iconInset).toBe(8) + expect(geometry.rowInset).toBeGreaterThanOrEqual(6) + expect(geometry.rowInset).toBeLessThanOrEqual(7) + if (locale === 'zh') { + expect(geometry.labelWidth).toBe(32) + expect(geometry.valueOffset).toBe(76) + } + expect(geometry.right).toBeLessThanOrEqual(geometry.width) + } + } + } + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) +}) + +it.skipIf(MODE === 'record')('keeps long branch details accessible by touch and keyboard across the hero row breakpoints', async () => { + const branch = 'feature/a-long-current-branch-name-for-mobile-review' + const { page, scaffold, tripwire } = await launchPanel('web-e2e-forge-branch-details', 'normal', { branch, hasTouch: true }) + const option = page.locator('[data-forge-worktree]') + const chip = option.locator('[data-forge-current-branch]') + await chip.waitFor() + await page.getByRole('button', { name: '收起侧边栏', exact: true }).click() + for (const [locale, label] of [['zh', `当前分支:${branch}`], ['en', `Current branch: ${branch}`], ['de', `Aktueller Branch: ${branch}`]] as const) { + await scaffold.ctx.settings.update('locale', { preference: locale }) + await option.getByRole('button', { name: label, exact: true }).waitFor() + for (const width of [390, 600, 601, 700, 1280]) { + await page.setViewportSize({ width, height: 844 }) + await chip.scrollIntoViewIfNeeded() + await expect.poll(() => option.evaluate((element) => { + const row = element.closest('[class*="heroWorkspaceRow"]')! + const ancestors: Element[] = [] + for (let node: Element | null = row; node !== null; node = node.parentElement) ancestors.push(node) + return { + fits: element.getBoundingClientRect().right <= innerWidth, + overflow: ancestors.filter(node => node.scrollWidth > node.clientWidth + 1).map(node => node.className), + } + })).toEqual({ fits: true, overflow: [] }) + } + } + await scaffold.ctx.settings.update('locale', { preference: 'en' }) + await option.getByRole('button', { name: `Current branch: ${branch}`, exact: true }).waitFor() + await page.setViewportSize({ width: 390, height: 844 }) + expect(await chip.locator('span').evaluate(element => element.scrollWidth > element.clientWidth)).toBe(true) + const tooltip = page.getByRole('tooltip', { exact: true, name: `Current branch: ${branch}` }) + await chip.tap() + await tooltip.waitFor() + await compareOrRefreshGolden(join(EXPECTED, 'branch-details.expected.md'), await tooltip.ariaSnapshot(), MODE) + await chip.tap() + await tooltip.waitFor({ state: 'hidden' }) + await chip.tap() + await tooltip.waitFor() + await page.locator('[class*="heroWorkspaceRow"]').tap({ position: { x: 1, y: 1 } }) + await tooltip.waitFor({ state: 'hidden' }) + await chip.focus() + await page.keyboard.press('Tab') + await page.keyboard.press('Shift+Tab') + await tooltip.waitFor() + await page.keyboard.press('Escape') + await tooltip.waitFor({ state: 'hidden' }) + await page.keyboard.press('Enter') + await tooltip.waitFor() + await page.keyboard.press('Escape') + await tooltip.waitFor({ state: 'hidden' }) + expect(await option.getByRole('checkbox').isChecked()).toBe(false) + expect(await page.getByRole('menu').count()).toBe(0) + await scaffold.ctx.settings.update('locale', { preference: 'zh' }) + await option.getByRole('checkbox', { name: '新建工作树', exact: true }).waitFor() + const { settled } = await sendPrompt(page, scaffold) + await settled + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) +}) + +it.skipIf(MODE === 'record').each(['darwin', 'win32'] as const)('closes diff menus when the %s desktop shortcut deactivates a retained tab', async (platform) => { + const { page, scaffold, project, tripwire } = await launchPanel(`web-e2e-forge-menu-${platform}`, 'normal', { desktop: platform }) + await git(project, 'update-ref', 'refs/remotes/origin/main', 'HEAD') + await git(project, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main') + await writeFile(join(project, 'README.md'), 'review this change\n') + const { settled } = await sendPrompt(page, scaffold) + const sessionId = await settled + await page.locator('[data-forge-header]').click() + await page.locator('[data-forge-panel]').getByRole('button', { name: '刷新状态', exact: true }).click() + await page.locator('[data-forge-local]').getByRole('button', { name: /查看本会话的变更:1 个文件/ }).click() + const view = page.locator('[data-forge-changes="local"]') + await view.locator('[data-forge-diff-file="README.md"]').waitFor() + for (const name of ['分支', '全部 1 个文件', 'README.md 的更多操作']) { + await view.getByRole('button', { name, exact: true }).click() + await page.getByRole('menu').waitFor() + await page.evaluate((mac) => { window.forgeKeyboard.deliver({ + kind: 'keyboard', frameName: '', revision: window.forgeKeyboard.revision, code: 'KeyN', + meta: mac, control: !mac, alt: false, shift: false, repeat: false, + }) }, platform === 'darwin') + await expect.poll(() => view.isVisible()).toBe(false) + await expect.poll(() => page.getByRole('menu').count()).toBe(0) + await page.locator(`[data-row-key="session:${sessionId}"]`).click() + await view.waitFor() + await expect.poll(() => page.getByRole('menu').count()).toBe(0) + } + expect(tripwire.warnings).toEqual([]) +}) + +it.skipIf(MODE === 'record').each([120, 4000])('types a review comment beside %i changed lines', async (count) => { + const { scaffold, page, root, tripwire } = await launchPanel('web-e2e-forge-review-typing') + const patch = ['diff --git a/src/typing.ts b/src/typing.ts', 'index 1111111..2222222 100644', + '--- a/src/typing.ts', '+++ b/src/typing.ts', `@@ -1,${count} +1,${count} @@`, + ...Array.from({ length: count }, (_, index) => { + const context = index % 80 === 0 ? 'wrapped context '.repeat(600) : index % 5 === 0 ? 'context '.repeat(30) : '' + return `-export const value${index} = [${index}, '${context}']\n+export const value${index} = [${index + 1}, '${context}']` + }), ''].join('\n') + await writeFile(join(root, 'pr-7.diff'), patch) + const { settled } = await sendPrompt(page, scaffold) + const sessionId = await settled + const session = scaffold.ctx.sessions.get(sessionId) + if (session === undefined) throw new Error('Session must stay loaded') + scaffold.ctx.forgeSession.track(session, PULL) + await page.locator('[data-forge-header]').click() + const change = page.locator(`[data-forge-change="${PULL}"]`) + await change.getByRole('button', { name: '查看 #7 的变更:新增 128 行,删除 14 行', exact: true }).click() + const file = page.locator('[data-forge-diff-file="src/typing.ts"]') + const collapsed = file.getByRole('button', { name: /^文件较大,/ }) + await file.waitFor() + if (count > 120) await collapsed.click() + await file.getByRole('button', { name: '选择第 R5 行', exact: true }).click() + await file.getByRole('button', { name: '添加评论', exact: true }).click() + const input = file.getByRole('textbox', { name: '留下评论…', exact: true }) + await input.waitFor() + const field = await input.boundingBox() + if (field === null) throw new Error('Comment field must be visible') + await page.mouse.move(field.x + field.width - 3, field.y + field.height - 3) + await page.mouse.down() + await page.mouse.move(field.x + field.width - 3, field.y + field.height + 77, { steps: 5 }) + await page.mouse.up() + const resized = await input.boundingBox() + if (resized === null) throw new Error('Resized comment field must remain visible') + expect(resized.height).toBeGreaterThan(field.height + 50) + await input.fill('Review a wrapped line. '.repeat(80)) + expect(await input.evaluate(element => element.scrollHeight > element.clientHeight)).toBe(true) + expect(await input.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + await input.fill('') + await page.keyboard.type('Check the boundary') + await page.keyboard.press('Enter') + const cdp = await page.context().newCDPSession(page) + try { + await cdp.send('Input.imeSetComposition', { text: '检查', selectionStart: 2, selectionEnd: 2 }) + expect(await input.inputValue()).toBe('Check the boundary\n检查') + expect(await file.locator('[data-forge-review-thread]').count()).toBe(0) + await cdp.send('Input.insertText', { text: '检查边界条件' }) + } finally { await cdp.detach() } + expect(await input.inputValue()).toBe('Check the boundary\n检查边界条件') + await input.fill(' \n') + expect(await file.getByRole('button', { name: '开始评审', exact: true }).isDisabled()).toBe(true) + await input.fill('检查边界条件') + await file.getByRole('button', { name: '开始评审', exact: true }).click() + await file.locator('[data-forge-review-thread="pending"]').getByText('检查边界条件', { exact: true }).waitFor() + expect(tripwire.pageErrors).toEqual([]) +}) diff --git a/apps/web/tests/hover-input.e2e.ts b/apps/web/tests/hover-input.e2e.ts index cae5220a89..ccba69a0c7 100644 --- a/apps/web/tests/hover-input.e2e.ts +++ b/apps/web/tests/hover-input.e2e.ts @@ -95,18 +95,24 @@ it.each([false, true])('keeps workspace and archived search actions reachable wi expect(await shortcut.count()).toBe(1) expect(await page.evaluate(() => matchMedia('(pointer: coarse)').matches)).toBe(hasTouch) expect(await shortcut.isVisible()).toBe(!hasTouch) + if (hasTouch) { + await page.keyboard.press('Escape') + await page.setViewportSize({ width: 390, height: 900 }) + await page.locator('[data-sidebar-drawer]').waitFor() + await page.locator('[data-shell-leading]').getByRole('button', { name: 'Open sidebar', exact: true }).tap() + await activate(actions) + await archive.waitFor() + } for (const key of ['ArrowDown', 'Tab']) { await page.keyboard.press(key) expect(await page.locator('html').getAttribute('data-input-modality')).toBe('keyboard') expect(await page.getByRole('menu').evaluate(menu => menu.contains(document.activeElement))).toBe(true) expect(await shortcut.isVisible()).toBe(true) if (hasTouch) { - await page.setViewportSize({ width: 390, height: 900 }) await expect.poll(() => shortcut.evaluate((element) => { const bounds = element.getBoundingClientRect() return bounds.left >= 0 && bounds.right <= window.innerWidth })).toBe(true) - await page.setViewportSize({ width: 1280, height: 900 }) } await page.keyboard.press('Escape') if (!hasTouch) await row.hover() @@ -114,6 +120,13 @@ it.each([false, true])('keeps workspace and archived search actions reachable wi await archive.waitFor() expect(await shortcut.isVisible()).toBe(!hasTouch) } + if (hasTouch) { + await page.keyboard.press('Escape') + await page.setViewportSize({ width: 1280, height: 900 }) + await page.locator('[data-sidebar-drawer]').waitFor({ state: 'detached' }) + await activate(actions) + await archive.waitFor() + } await activate(archive) await row.waitFor({ state: 'detached' }) await activate(page.getByRole('button', { name: 'View options', exact: true })) @@ -224,19 +237,23 @@ it.each([false, true])('keeps actions and focus usable with touch=%s across layo await page.getByRole('tooltip').waitFor({ state: 'detached' }) const blockedSplit = panel.locator('[data-dockkit-split-button]').last() expect(await blockedSplit.isDisabled()).toBe(true) - const hit = await blockedSplit.evaluate((button) => { - const bounds = button.getBoundingClientRect() - const x = bounds.x + bounds.width / 2 - const y = bounds.y + bounds.height / 2 - const target = document.elementFromPoint(x, y) - return { x, y, wrapperReceivesInput: target === button.parentElement, cursor: target && getComputedStyle(target).cursor } - }) - expect(hit.wrapperReceivesInput).toBe(true) - expect(['auto', 'default']).toContain(hit.cursor) - // Coordinate input exercises CSS hit testing over the disabled button itself. - if (hasTouch) await page.touchscreen.tap(hit.x, hit.y) - else await page.mouse.click(hit.x, hit.y) - await page.getByRole('tooltip').filter({ hasText: 'Two panes is the limit' }).waitFor() + if (hasTouch) { + expect(await blockedSplit.isVisible()).toBe(false) + expect(await page.getByRole('tooltip').count()).toBe(0) + } else { + const hit = await blockedSplit.evaluate((button) => { + const bounds = button.getBoundingClientRect() + const x = bounds.x + bounds.width / 2 + const y = bounds.y + bounds.height / 2 + const target = document.elementFromPoint(x, y) + return { x, y, wrapperReceivesInput: target === button.parentElement, cursor: target && getComputedStyle(target).cursor } + }) + expect(hit.wrapperReceivesInput).toBe(true) + expect(['auto', 'default']).toContain(hit.cursor) + // Coordinate input exercises CSS hit testing over the disabled button itself. + await page.mouse.click(hit.x, hit.y) + await page.getByRole('tooltip').filter({ hasText: 'Two panes is the limit' }).waitFor() + } expect(await panel.locator('[data-dockkit-pane]').count()).toBe(2) expect(consoleWatch.pageErrors).toEqual([]) }) diff --git a/apps/web/tests/layout-bottom.e2e.ts b/apps/web/tests/layout-bottom.e2e.ts index ee213925b3..61c7f4f405 100644 --- a/apps/web/tests/layout-bottom.e2e.ts +++ b/apps/web/tests/layout-bottom.e2e.ts @@ -42,7 +42,8 @@ it('reserves bottom content height without remounting it on main-panel navigatio const viewportHeight = page.viewportSize()!.height expect(result.bottom).toEqual({ top: viewportHeight - height, height, bottom: viewportHeight }) expect(result.main).toEqual({ top: caption, height: viewportHeight - caption - height, bottom: viewportHeight - height }) - expect(result.sidebar).toEqual(result.main) + expect(result.sidebar).toEqual(page.viewportSize()!.width < 768 + ? { top: 0, height: viewportHeight, bottom: viewportHeight } : result.main) expect(result.rightbar).toEqual(result.main) expect(result.overlay).toEqual({ top: 0, height: viewportHeight, bottom: viewportHeight }) for (const handle of result.handles) expect(handle).toEqual(result.main) diff --git a/apps/web/tests/markdown-file-links.e2e.ts b/apps/web/tests/markdown-file-links.e2e.ts index 62b83dfafb..b3beacb3b9 100644 --- a/apps/web/tests/markdown-file-links.e2e.ts +++ b/apps/web/tests/markdown-file-links.e2e.ts @@ -156,6 +156,56 @@ describe('web e2e: Markdown file links', () => { await assertFixtureInventory(SNAPSHOT_DIR, ['session.v3.jsonl', 'links.expected.md', 'preview.expected.md']) }) + it('replays colon line suffixes as locations and shares tabs with fragment links', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-colon-file-links')) + const fixture = new URL('./fixtures/markdown-colon-file-links/session.v3.jsonl', import.meta.url) + const id = await seedSession(scaffold, await readFile(fixture, 'utf8'), 'colon-file-links') + await page.reload({ waitUntil: 'load' }) + const workspace = page.getByRole('treeitem').first() + await workspace.waitFor() + if (await workspace.getAttribute('aria-expanded') !== 'true') await workspace.click() + await page.locator(`[data-row-key="session:${id}"]`).click() + + const relative = page.getByRole('button', { name: 'Relative colon:24–30', exact: true }) + await relative.waitFor() + expect(await page.getByRole('button', { name: 'Development server', exact: true }).count()).toBe(0) + expect(await page.getByRole('link', { name: 'Development server', exact: true }).getAttribute('href')).toBe('localhost:3000') + expect(await page.getByRole('link', { name: 'Bare URL', exact: true }).getAttribute('href')).toBe('index.ts:24') + expect(await page.getByRole('link', { name: 'Blocked script', exact: true }).count()).toBe(0) + expect(await page.getByRole('button', { name: 'Blocked script', exact: true }).count()).toBe(0) + expect(await page.getByRole('link', { name: 'Mail', exact: true }).getAttribute('href')).toBe('mailto:10086') + await compareOrRefreshGolden( + fileURLToPath(new URL('./expected/markdown-colon-file-links.md', import.meta.url)), + await relative.locator('..').ariaSnapshot(), MODE, + ) + const column = page.locator('[data-rightbar-col]') + for (const label of ['Zero line', 'Reversed range']) { + await page.getByRole('button', { name: label, exact: true }).click() + await expect.poll(() => column.locator('[data-textpreview-path]').textContent()) + .toBe(join(scaffold.workspaceCwd, 'src/example.txt')) + await expect.poll(() => column.locator('[data-textpreview-line="1"]').textContent()).toBe('source line 1\n') + expect(await column.locator('[data-dockkit-tab-title]').allTextContents()).toEqual(['example.txt']) + } + for (const [label, line] of [['Relative colon:24–30', 24], ['Absolute colon:30', 30], ['Column:24:5', 24], ['Fragment:12', 12]] as const) { + const link = page.getByRole('button', { name: label, exact: true }) + await link.focus() + await link.press('Enter') + await expect.poll(() => column.locator(`[data-textpreview-target="${line}"]`).textContent()) + .toBe(`source line ${line}\n`) + await expect.poll(() => column.locator('[data-textpreview-path]').textContent()) + .toBe(join(scaffold.workspaceCwd, 'src/example.txt')) + expect(await column.locator('[data-dockkit-tab-title]').allTextContents()).toEqual(['example.txt']) + } + await page.reload({ waitUntil: 'load' }) + await expect.poll(() => column.locator('[data-textpreview-path]').textContent()) + .toBe(join(scaffold.workspaceCwd, 'src/example.txt')) + await relative.click() + await expect.poll(() => column.locator('[data-textpreview-target="24"]').textContent()).toBe('source line 24\n') + expect(await column.locator('[data-dockkit-tab-title]').allTextContents()).toEqual(['example.txt']) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + it('opens parent-path prose and read-card links while retaining each address across reload', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-parent-file-links')) // Return through the parent into this owned temp root; no shared parent file is created. diff --git a/apps/web/tests/markdown-wide-table.e2e.ts b/apps/web/tests/markdown-wide-table.e2e.ts index bdd7c5da6f..eba073c473 100644 --- a/apps/web/tests/markdown-wide-table.e2e.ts +++ b/apps/web/tests/markdown-wide-table.e2e.ts @@ -216,8 +216,8 @@ async function awaitTableLayout(target: Page): Promise { if (element === null) return false const tracks = getComputedStyle(element).gridTemplateColumns.split(' ').map(Number.parseFloat) const root = element.querySelector('div[data-phase]') - // Mirrored from ui-layout's SIDEBAR_COLLAPSED; these tests use the Host compiler face. - return tracks[0] === 56 && tracks.at(-1) === 0 + // Desktop retains the rail; the mobile drawer leaves the whole width to the transcript. + return tracks[0] === (element.hasAttribute('data-sidebar-drawer') ? 0 : 56) && tracks.at(-1) === 0 && element.getAnimations().every(animation => animation.playState === 'finished' || animation.playState === 'idle') && root !== null diff --git a/apps/web/tests/model-badge.e2e.ts b/apps/web/tests/model-badge.e2e.ts new file mode 100644 index 0000000000..4fb6e6aaab --- /dev/null +++ b/apps/web/tests/model-badge.e2e.ts @@ -0,0 +1,185 @@ +/** Catalog badges reach every selector placement through the Host catalog without changing catalog identity. */ +import { fileURLToPath } from 'node:url' +import { LlmAdapter, type LlmModelBadge, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { parseModelListing } from '@deepseek-ai/dsh-llm-deepseek' +import { chromium, type Browser, type Page } from 'playwright' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { + captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts' + +const NAME = 'DeepSeek-V4.1-Flash' +const LONG_NAME = 'DeepSeek model with a very long release and capability description that exceeds the selector width' +const FIXTURE = fileURLToPath(new URL('./fixtures/model-badge/session.v4.jsonl', import.meta.url)) +const EXPECTED = fileURLToPath(new URL('./expected/model-badge/models.expected.md', import.meta.url)) +const MODE = webSnapshotMode() + +/** DeepSeek fixture declarations pass through the production wire decoder before replay. */ +function listedModel(id: string, name: string, badge: unknown) { + const entry = parseModelListing({ data: [{ id, name, client_metadata: { 'deepseek-harness': { badge } } }] }).entries[0]! + return { id, name, ...(entry.badge?.kind === 'badge' ? { badge: entry.badge.badge } : {}) } +} + +describe.skipIf(MODE === 'record')('web e2e: catalog model badges', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({ + extraOverlayPath: fileURLToPath(new URL('./fixtures/model-badge/account.overlay.yml', import.meta.url)), + replayFixture: FIXTURE, + replayProvidersOnly: true, + replayProviders: [ + { id: 'deepseek-account', name: 'DeepSeek Account', models: [ + listedModel('deepseek-flash', NAME, { text: ' 1017 ', tone: 'info' }), + ] }, + { id: 'deepseek-official', name: 'DeepSeek', models: [ + listedModel('deepseek-flash', NAME, { text: { en: ' New ', zh: ' 新 ' }, tone: 'success' }), + { id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' }, + listedModel('long', LONG_NAME, { text: 'Preview', tone: 'future-tone' }), + listedModel('invalid-badge', 'Invalid badge model', { text: { en: 'New', zh: '\u200b' } }), + ] }, + { id: 'external', name: 'External', models: [{ id: 'deepseek-flash', name: NAME }] }, + ], + }) + browser = await chromium.launch() + page = await browser.newPage({ viewport: { width: 1440, height: 900 }, locale: ZH_BROWSER_LOCALE }) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await connectFreshWorkspaceZh(page, scaffold.workspaceCwd) + }) + + afterAll(async () => { + try { await browser?.close() } + finally { await scaffold?.close() } + }) + + it('shows each catalog badge in all placements while preserving the chosen provider and model id', async () => { + const trigger = page.getByRole('button', { name: /^选择模型/ }) + await trigger.getByText('新', { exact: true }).waitFor() + expect(await trigger.textContent()).toContain(`新 ${NAME}`) + expect(await trigger.getAttribute('aria-label')).toBe(`选择模型,当前 ${NAME}`) + expect(await trigger.getByText('新', { exact: true }).getAttribute('data-tone')).toBe('success') + try { + await trigger.click() + const current = page.getByRole('menuitem', { name: /^模型/ }) + expect(await current.getByText('新', { exact: true }).count()).toBe(1) + await current.click() + const menu = page.getByRole('menu', { name: '模型', exact: true }) + await compareOrRefreshGolden(EXPECTED, await captureStableAria(page, '[role="menu"][aria-label="模型"]', scaffold.workspaceCwd), MODE) + expect(await menu.getByRole('group', { name: 'External', exact: true }).locator('[data-tone]').count()).toBe(0) + expect(await menu.getByRole('menuitemradio', { name: 'Invalid badge model', exact: true }).locator('[data-tone]').count()).toBe(0) + expect(await menu.getByRole('menuitemradio', { name: `${LONG_NAME} Preview`, exact: true }).locator('[data-tone]').getAttribute('data-tone')).toBe('neutral') + const account = menu.getByRole('group', { name: 'DeepSeek 账号', exact: true }) + expect(await account.getByText('1017', { exact: true }).getAttribute('data-tone')).toBe('info') + await account.getByRole('menuitemradio', { name: `${NAME} 1017`, exact: true }).click() + await expect.poll(() => scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-account', model: 'deepseek-flash' }) + await expect.poll(() => trigger.textContent()).toContain(`1017 ${NAME}`) + const catalog = await scaffold.ctx.llm.listModels('deepseek-account') + expect(catalog.map(model => ({ id: model.id, name: model.name }))).toEqual([{ id: 'deepseek-flash', name: NAME }]) + } finally { + await page.keyboard.press('Escape') + await page.keyboard.press('Escape') + } + }) + + it('updates and clears badges in an already-open page and model menu', async () => { + const provider = 'live-badge' + class CatalogAdapter extends LlmAdapter { + badge: LlmModelBadge | undefined = { text: 'New', tone: 'info' } + override async listModels(route: string) { + return [{ provider: route, id: 'live', name: 'Live model', ...(this.badge === undefined ? {} : { badge: this.badge }) }] + } + override async *stream(): AsyncIterable { throw new Error('catalog-only scenario') } + } + const adapter = new CatalogAdapter() + const unregister = scaffold.ctx.llm.registerAdapter([provider], adapter) + const trigger = page.getByRole('button', { name: /^选择模型/ }) + try { + await trigger.click() + await page.getByRole('menuitem', { name: /^模型/ }).click() + await page.getByRole('menuitemradio', { name: 'Live model New', exact: true }).click() + await expect.poll(() => trigger.textContent()).toContain('New Live model') + await trigger.click() + await page.getByRole('menuitem', { name: /^模型/ }).click() + adapter.badge = { text: 'Updated', tone: 'success' } + scaffold.ctx.emit('llm/models-updated', provider) + await expect.poll(() => trigger.textContent()).toContain('Updated Live model') + const updated = page.getByRole('menuitemradio', { name: 'Live model Updated', exact: true }) + await updated.waitFor() + expect(await updated.locator('[data-tone]').getAttribute('data-tone')).toBe('success') + adapter.badge = undefined + scaffold.ctx.emit('llm/models-updated', provider) + await expect.poll(() => trigger.textContent()).toBe('Live model') + const cleared = page.getByRole('menuitemradio', { name: 'Live model', exact: true }) + await cleared.waitFor() + expect(await cleared.locator('[data-tone]').count()).toBe(0) + } finally { + await page.keyboard.press('Escape') + await page.keyboard.press('Escape') + await trigger.click() + await page.getByRole('menuitem', { name: /^模型/ }).click() + await page.getByRole('menuitemradio', { name: `${NAME} 1017`, exact: true }).click() + unregister() + } + }) + + it('ellipsizes long names while keeping badges visible in every placement and both theme palettes', async () => { + const trigger = page.getByRole('button', { name: /^选择模型/ }) + try { + await trigger.click() + await page.getByRole('menuitem', { name: /^模型/ }).click() + await page.getByRole('menuitemradio', { name: `${LONG_NAME} Preview`, exact: true }).click() + await expect.poll(() => trigger.textContent()).toBe(`Preview ${LONG_NAME}`) + const triggerBadge = trigger.getByText('Preview', { exact: true }) + const triggerBox = (await trigger.boundingBox())! + const triggerBadgeBox = (await triggerBadge.boundingBox())! + expect(triggerBadgeBox.x).toBeGreaterThanOrEqual(triggerBox.x) + expect(triggerBadgeBox.x + triggerBadgeBox.width).toBeLessThanOrEqual(triggerBox.x + triggerBox.width) + const triggerName = trigger.getByText(LONG_NAME, { exact: true }) + expect(await triggerName.evaluate(node => node.scrollWidth > node.clientWidth && getComputedStyle(node).textOverflow === 'ellipsis')).toBe(true) + + await page.setViewportSize({ width: 390, height: 844 }) + await trigger.click() + const current = page.getByRole('menuitem', { name: /^模型/ }) + const rootBadgeBox = (await current.getByText('Preview', { exact: true }).boundingBox())! + const rootBox = (await current.boundingBox())! + expect(rootBadgeBox.x).toBeGreaterThanOrEqual(rootBox.x) + expect(rootBadgeBox.x + rootBadgeBox.width).toBeLessThanOrEqual(rootBox.x + rootBox.width) + expect(await current.getByText(LONG_NAME, { exact: true }).evaluate(node => node.scrollWidth > node.clientWidth)).toBe(true) + await current.click() + const menu = page.getByRole('menu', { name: '模型', exact: true }) + const option = menu.getByRole('menuitemradio', { name: `${LONG_NAME} Preview`, exact: true }) + const badge = option.getByText('Preview', { exact: true }) + const box = await badge.boundingBox() + expect(box).not.toBeNull() + expect(box!.x).toBeGreaterThanOrEqual(12) + expect(box!.x + box!.width).toBeLessThanOrEqual(378) + expect(await option.getByText(LONG_NAME, { exact: true }).evaluate(node => node.scrollWidth > node.clientWidth)).toBe(true) + const colors = await badge.evaluate((node) => { + const body = node.ownerDocument.body + const previous = body.getAttribute('data-ds-dark-theme') + try { + return [false, true].map((dark) => { + body.toggleAttribute('data-ds-dark-theme', dark) + const style = getComputedStyle(node) + return { color: style.color, background: style.backgroundColor } + }) + } finally { + if (previous === null) body.removeAttribute('data-ds-dark-theme') + else body.setAttribute('data-ds-dark-theme', previous) + } + }) + expect(colors[0]).not.toEqual(colors[1]) + for (const color of colors) expect(color.color).not.toBe(color.background) + } finally { + await page.keyboard.press('Escape') + await page.keyboard.press('Escape') + await page.setViewportSize({ width: 1440, height: 900 }) + } + expect(tripwire.pageErrors).toEqual([]) + }) +}) diff --git a/apps/web/tests/model-input-layout.ts b/apps/web/tests/model-input-layout.ts index e911bb3812..5cdacbfabd 100644 --- a/apps/web/tests/model-input-layout.ts +++ b/apps/web/tests/model-input-layout.ts @@ -27,5 +27,11 @@ export async function assertModelInputLayout(page: Page, editor: Locator): Promi } } finally { await page.setViewportSize(original) + // The frame reports its own width to the layout store from a rAF-throttled + // ResizeObserver, so the restored viewport is only in force once the shell + // has left drawer mode; a capture taken in between reads the narrow frame. + if (await page.locator('[data-sidebar-drawer]').count() > 0) { + await expect.poll(async () => page.locator('[data-sidebar-drawer]').count(), { timeout: 5_000 }).toBe(0) + } } } diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts index ae8726c23f..9632042b34 100644 --- a/apps/web/tests/navigation-panes.e2e.ts +++ b/apps/web/tests/navigation-panes.e2e.ts @@ -12,7 +12,10 @@ import { join } from 'node:path' import type { Browser, Page, Response } from 'playwright' import { chromium } from 'playwright' import { strFromU8, unzipSync } from 'fflate' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed, onTestFinished } from 'vitest' +import { ToolCallId, createToolResultMessage } from '@deepseek-ai/dsh-llm' +import type SessionToolResults from '@deepseek-ai/dsh-client-ui-trajectory' +import { defineTool } from '@deepseek-ai/dsh-tools' import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay' import { SESSION_FORMAT_VERSION, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' import { @@ -33,6 +36,9 @@ const CODE_CARD_DIR = fileURLToPath(new URL('./expected/navigation-panes', impor const COMMAND_WRAP_EXPECTED = join(CODE_CARD_DIR, 'command-wrap.expected.md') const CODE_CARD_EXPECTED = join(CODE_CARD_DIR, 'code-card.expected.md') const COPY_MARKDOWN_EXPECTED = join(CODE_CARD_DIR, 'copy-markdown.expected.md') +const EVICTED_RESULT_EXPECTED = join(CODE_CARD_DIR, 'live-result-evicted.expected.md') +const LIVE_RESULT_EXPECTED = join(CODE_CARD_DIR, 'live-result.expected.md') +const RUNNING_RESULT_EXPECTED = join(CODE_CARD_DIR, 'live-result-running.expected.md') const MODE = webSnapshotMode() const SEED_ID = 'navigation-panes-web-e2e' const EXPORTED_LOG_FILE = `session.v${SESSION_FORMAT_VERSION}.jsonl` @@ -306,6 +312,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { await page.getByRole('tab', { name: 'Result' }).click() await expect.poll(() => page.getByText('NAVIGATION_OK', { exact: false }).count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1) expect(await page.locator('[data-timeline-span="message"][data-assistant-timing="true"]').count()).toBeGreaterThan(0) + await page.getByText('Raw results are only available for live tool calls', { exact: true }).waitFor() const snapshot = (await captureStableAria(page, '[class*="viewArea"]', scaffold.workspaceCwd)) .split(SEED_ID).join('{{seededId}}') await compareOrRefreshGolden(TRAJECTORY_EXPECTED, snapshot, MODE) @@ -624,10 +631,218 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { it.skipIf(MODE === 'record')('keeps the recorded fixture inventory exact', async () => { await assertFixtureInventory(CODE_CARD_DIR, [ 'code-card.expected.md', 'command-wrap.expected.md', 'copy-markdown.expected.md', + 'live-result-evicted.expected.md', 'live-result.expected.md', 'live-result-running.expected.md', ]) await assertFixtureInventory(SNAPSHOT_DIR, [ 'session.v3.jsonl', 'search-id-results.expected.md', 'search-results.expected.md', 'trajectory.expected.md', 'terminal-card.expected.md', 'timing.expected.md', ]) }) + it.skipIf(MODE === 'record')('inspects a live tool value without adding it to the Session journal', async () => { + await ensureSeedOpen(page) + await expect.poll(() => scaffold.ctx.agents.get(SessionId(SEED_ID)) !== undefined).toBe(true) + const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))! + const unregister = scaffold.ctx.tools.register(defineTool({ + name: 'live_result_probe', description: 'Live result inspection fixture.', parameters: {}, + output: { schema: { type: 'json' }, render: () => [{ type: 'text', text: 'LIVE_RESULT_RENDERED' }] }, + execute: async () => ({ answer: 42 }), + })) + onTestFinished(unregister) + const callId = ToolCallId('live-result-probe') + let resultEvent: SessionEvent | undefined + const off = scaffold.ctx.on('session/event', (_session, event) => { + if (event.type === 'tool/result' && event.data.message.toolCallId === callId) resultEvent = event + }, { global: true }) + onTestFinished(() => { off() }) + const batch = await scaffold.ctx.tools.execute({ + agent, signal: new AbortController().signal, log: { turn: 2, step: 1 }, + calls: [{ type: 'tool-call', id: callId, name: 'live_result_probe', arguments: '{}' }], + }) + expect(batch.results[0]?.result.isError).toBe(false) + expect(resultEvent).toBeDefined() + expect(resultEvent?.data).not.toHaveProperty('value') + const abort = new AbortController() + const observed = (scaffold.ctx.get('sessionToolResults') as SessionToolResults).watch( + { sessionId: agent.id, seq: resultEvent!.seq }, abort.signal, + )[Symbol.asyncIterator]() + try { + expect((await observed.next()).value).toEqual({ available: true, value: { answer: 42 } }) + } finally { abort.abort(); await observed.return?.() } + + await page.getByRole('tab', { name: 'Trajectory' }).click() + await page.locator('tr[data-kind="tool"]').filter({ hasText: 'live_result_probe' }).click() + const details = page.getByRole('complementary', { name: 'Event details' }) + await details.getByRole('tab', { name: 'Result', exact: true }).click() + await details.getByText('LIVE_RESULT_RENDERED', { exact: true }).waitFor() + const rawSection = details.getByRole('region', { name: 'Raw result', exact: true }) + const renderedSection = details.getByRole('region', { name: 'Rendered result', exact: true }) + await expect.poll(() => rawSection.textContent()).toContain('answer:') + expect(await renderedSection.textContent()).toContain('LIVE_RESULT_RENDERED') + expect(await details.getByRole('button', { name: 'Raw result', exact: true }).count()).toBe(0) + await compareOrRefreshGolden( + LIVE_RESULT_EXPECTED, await captureStableAria(page, '#trajectory-detail-panel', scaffold.workspaceCwd), MODE, + ) + const unregisterJson = scaffold.ctx.tools.register(defineTool({ + name: 'json_result_probe', description: 'JSON result inspection fixture.', parameters: {}, + output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }] }, + execute: async () => Array.from({ length: 80 }, (_, index) => index), + })) + onTestFinished(unregisterJson) + const jsonBatch = await scaffold.ctx.tools.execute({ + agent, signal: new AbortController().signal, log: { turn: 2, step: 1 }, + calls: [{ type: 'tool-call', id: ToolCallId('json-result-probe'), name: 'json_result_probe', arguments: '{}' }], + }) + expect(jsonBatch.results[0]?.result.isError).toBe(false) + await page.locator('tr[data-kind="tool"]').filter({ hasText: 'json_result_probe' }).click() + await rawSection.getByRole('tree').waitFor() + await renderedSection.getByRole('tree').waitFor() + const originalViewport = page.viewportSize()! + try { + for (const width of [originalViewport.width, 390]) { + await page.setViewportSize({ width, height: 600 }) + const geometry = await details.getByRole('tabpanel').evaluate((panel) => { + const sections = [...panel.querySelectorAll('section[aria-label]')] + const first = sections[0]!.getBoundingClientRect() + const second = sections[1]!.getBoundingClientRect() + return { + gap: second.top - first.bottom, + headingFonts: sections.map(section => getComputedStyle(section.querySelector('h3')!).fontSize), + overflowingDescendants: [...panel.querySelectorAll('*')].filter(node => + /auto|scroll/.test(getComputedStyle(node).overflowY) && node.scrollHeight > node.clientHeight + 1).length, + scrolls: panel.scrollHeight > panel.clientHeight, + horizontalOverflow: panel.scrollWidth > panel.clientWidth + 1, + headingsAligned: first.left === second.left, + } + }) + expect(geometry).toEqual({ + gap: 2, headingFonts: ['13px', '13px'], overflowingDescendants: 0, + scrolls: true, horizontalOverflow: false, headingsAligned: true, + }) + await details.getByRole('tabpanel').evaluate((panel) => { panel.scrollTop = panel.scrollHeight }) + expect(await details.getByRole('tabpanel').evaluate(panel => panel.scrollTop)).toBeGreaterThan(0) + } + } finally { + await page.setViewportSize(originalViewport) + } + await page.locator('tr[data-kind="tool"]').filter({ hasText: 'live_result_probe' }).click() + await expect.poll(() => rawSection.textContent()).toContain('answer:') + const newer = await scaffold.ctx.tools.execute({ + agent, signal: new AbortController().signal, + calls: Array.from({ length: 1000 }, (_, index) => ({ + type: 'tool-call' as const, id: ToolCallId(`newer-result-${index}`), name: 'live_result_probe', arguments: '{}', + })), + }) + expect(newer.results).toHaveLength(1000) + expect(newer.results.every(entry => !entry.result.isError)).toBe(true) + await details.getByText('Raw results are only available for live tool calls', { exact: true }).waitFor() + // Keep hover and focus tooltips out of the recorded result panel. + await details.getByText('Raw results are only available for live tool calls', { exact: true }).click() + await details.getByRole('tooltip').waitFor({ state: 'hidden' }) + await compareOrRefreshGolden( + EVICTED_RESULT_EXPECTED, await captureStableAria(page, '#trajectory-detail-panel', scaffold.workspaceCwd), MODE, + ) + await details.getByText('LIVE_RESULT_RENDERED', { exact: true }).waitFor() + }) + + it.skipIf(MODE === 'record')('keeps long raw strings and PTC output readable in one result scroller', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-long-tool-results')) + await ensureSeedOpen(page) + await expect.poll(() => scaffold.ctx.agents.get(SessionId(SEED_ID)) !== undefined).toBe(true) + const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))! + agent.session.append('turn/start', { turn: 3 }) + onTestFinished(() => { + agent.session.append('step/end', { turn: 3, step: 1 }) + agent.session.append('turn/end', { turn: 3, reason: { kind: 'completed' } }) + }) + agent.session.append('step/start', { turn: 3, step: 1 }) + const callId = ToolCallId('long-ptc-result') + const call = agent.session.append('tool/call', { + turn: 3, step: 1, callId, name: 'run_code', + arguments: JSON.stringify({ code: 'console.log("PTC output")', description: 'Long program output' }), + }) + await page.getByRole('tab', { name: 'Trajectory' }).click() + await page.locator('tr[data-kind="tool"]').filter({ hasText: 'Long program output' }).click() + const details = page.getByRole('complementary', { name: 'Event details' }) + await details.getByRole('tab', { name: 'Result', exact: true }).click() + const raw = details.getByRole('region', { name: 'Raw result', exact: true }) + const rendered = details.getByRole('region', { name: 'Rendered result', exact: true }) + await raw.getByText('Running…', { exact: true }).waitFor() + await compareOrRefreshGolden( + RUNNING_RESULT_EXPECTED, await captureStableAria(page, '#trajectory-detail-panel', scaffold.workspaceCwd), MODE, + ) + const ptcOutput = [...Array.from({ length: 100 }, (_, i) => `PTC_LINE_${i}`), 'PTC_END'].join('\n') + agent.session.append('tool/result', { + turn: 3, step: 1, + message: createToolResultMessage({ callId, content: [{ type: 'text', text: ptcOutput }], isError: false }), + }, { surfaceOp: 'append', sourceEventSeqs: [call.seq] }) + await rendered.getByText(ptcOutput, { exact: true }).waitFor() + const stringValue = 'raw_output_'.repeat(300) + 'RAW_END' + const probes = [ + { name: 'long_string_probe', value: stringValue }, + { name: 'long_json_probe', value: { detail: 'json_output_'.repeat(120) } }, + ] + for (const probe of probes) { + const unregister = scaffold.ctx.tools.register(defineTool({ + name: probe.name, description: 'Long result layout fixture.', parameters: {}, + output: { + schema: { type: 'json' }, + render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? 'STRING_RENDERED' : JSON.stringify(value) }], + }, + execute: async () => probe.value, + })) + onTestFinished(unregister) + const batch = await scaffold.ctx.tools.execute({ + agent, signal: new AbortController().signal, log: { turn: 3, step: 1 }, + calls: [{ type: 'tool-call', id: ToolCallId(probe.name), name: probe.name, arguments: '{}' }], + }) + expect(batch.results[0]?.result.isError).toBe(false) + } + const viewport = page.viewportSize()! + try { + for (const width of [viewport.width, 390]) { + await page.setViewportSize({ width, height: 600 }) + for (const name of ['Long program output', ...probes.map(probe => probe.name)]) { + await details.getByRole('button', { name: 'Close details' }).click() + await page.locator('tr[data-kind="tool"]').filter({ hasText: name }).click() + await details.getByRole('tab', { name: 'Result', exact: true }).click() + if (name === 'long_json_probe') { + await raw.getByRole('treeitem').waitFor() + const heights = await Promise.all([raw, rendered].map(section => + section.getByRole('treeitem').evaluate(row => row.getBoundingClientRect().height))) + expect(heights[0]).toBeGreaterThan(3 * 16) + expect(heights[0]).toBe(heights[1]) + } else { + const body = name === 'Long program output' ? rendered : raw + await expect.poll(() => body.locator('pre').textContent()).toBe( + name === 'Long program output' ? ptcOutput : JSON.stringify(stringValue), + ) + } + const panel = details.getByRole('tabpanel') + const geometry = await panel.evaluate((element) => { + element.scrollTop = element.scrollHeight + const bounds = element.getBoundingClientRect() + const pre = element.querySelector('pre') + let tailVisible = true + if (pre?.lastChild !== null && pre?.lastChild !== undefined) { + const range = document.createRange() + range.selectNodeContents(pre) + range.setStart(pre.lastChild, (pre.lastChild.textContent?.length ?? 1) - 1) + const tail = range.getBoundingClientRect() + tailVisible = tail.top >= bounds.top && tail.bottom <= bounds.bottom + } + return { + scrolls: element.scrollTop > 0 && getComputedStyle(element).overflowY === 'auto', + tailVisible, + overflowingDescendants: [...element.querySelectorAll('*')].filter(node => + /auto|scroll/.test(getComputedStyle(node).overflowY) && node.scrollHeight > node.clientHeight + 1).length, + horizontalOverflow: element.scrollWidth > element.clientWidth + 1, + } + }) + expect(geometry).toEqual({ scrolls: true, tailVisible: true, overflowingDescendants: 0, horizontalOverflow: false }) + } + } + } finally { + await page.setViewportSize(viewport) + } + }) }) diff --git a/apps/web/tests/plan-control-row.e2e.ts b/apps/web/tests/plan-control-row.e2e.ts index c32960a14e..5d8f5b39ae 100644 --- a/apps/web/tests/plan-control-row.e2e.ts +++ b/apps/web/tests/plan-control-row.e2e.ts @@ -62,7 +62,17 @@ async function controlLayout(page: Page) { /** Wait for the responsive column and control row to finish changing width. */ async function resizeControls(page: Page, width: number): Promise { + const previousWidth = page.viewportSize()!.width await page.setViewportSize({ width, height: VIEWPORT.height }) + if (width >= 768 && width < 1024) { + const frame = page.locator('[data-sidebar-collapsed][data-rightbar-collapsed]') + if (previousWidth >= 1024) await frame.waitFor() + const open = page.getByRole('button', { name: 'Open sidebar', exact: true }) + if (await open.isVisible()) { + await open.click() + await expect.poll(() => frame.count()).toBe(0) + } + } await page.locator('[data-composer-card]').evaluate(async (card) => { let previous = card.getBoundingClientRect().width let stable = 0 @@ -201,7 +211,7 @@ describe('web e2e: plan chip click area at the narrow viewport', () => { // Find the content-dependent transition on this platform's fonts, never a // committed pixel threshold. The old 360px rule cannot satisfy this case. let narrowViewport: number | undefined - for (let width = 1100; width >= 650; width -= 10) { + for (let width = 1100; width >= 770; width -= 10) { await resizeControls(page, width) const layout = await controlLayout(page) if (layout.width > 360 && layout.icon && !layout.text) { @@ -267,7 +277,7 @@ describe('web e2e: plan chip click area at the narrow viewport', () => { await selectModel(page, LONG_MODEL) await expectControlLayout(page, false) - await resizeControls(page, 440) + await resizeControls(page, 384) await expectControlLayout(page, true) const trigger = page.getByRole('button', { name: /Select model/ }) expect(await trigger.getAttribute('title')).toContain(LONG_MODEL) diff --git a/apps/web/tests/plugin-config.e2e.ts b/apps/web/tests/plugin-config.e2e.ts index 39bd1ce0a5..8e78f856eb 100644 --- a/apps/web/tests/plugin-config.e2e.ts +++ b/apps/web/tests/plugin-config.e2e.ts @@ -110,9 +110,15 @@ describe('web e2e: plugin configuration pages', () => { await more.focus() await page.keyboard.press('Enter') await panel.getByRole('heading', { name: '实验性插件', exact: true, level: 1 }).waitFor() + // The More list omits the shipped bundles whose own rows do not run (the visibility rule lives in + // ui-plugin-manager), so it holds fewer cards than the installed optional-bundle list. const bundles = await scaffold.ctx.pluginManager.listBundles() + const hiddenBundles = new Set([ + '@deepseek-ai/dsh-experimental-terminal-bundle', + '@deepseek-ai/dsh-experimental-liveshare', + ]) expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package]').count()) - .toBe(bundles.filter(bundle => bundle.name !== '@deepseek-ai/dsh-experimental-terminal-bundle' + .toBe(bundles.filter(bundle => !hiddenBundles.has(bundle.name) && bundle.official && (bundle.listedByDefault !== false || bundle.enabled || bundle.installed)).length) expect(await basic.count()).toBe(0) expect(await extensions.count()).toBe(0) diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index d968ac13e0..7963b22779 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -2,7 +2,7 @@ // managed scaffold profile: installed bundles, their rows, and bundle enablement. Zero // model calls: everything is client state, seeded Session state, profile files, and the settings // document, so there is no fixture and a stray stream would fail loud on the open llm seam. -import { mkdtemp, readFile } from 'node:fs/promises' +import { mkdtemp, readFile, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { fileURLToPath } from 'node:url' import type { Browser, Locator, Page } from 'playwright' @@ -268,8 +268,8 @@ describe('web e2e: plugin manager', () => { await bar.getByRole('tablist').waitFor() await add.focus() for (const [width, icons, narrowTabs, wraps] of [ - [1280, false, false, false], [600, true, false, false], - [470, true, true, false], [390, true, true, true], [300, true, true, true], + [1280, false, false, false], [544, true, false, false], + [390, true, true, false], [320, true, true, true], [244, true, true, true], ] as const) { await probe.setViewportSize({ width, height: 480 }) await body.evaluate((element) => { element.scrollTop = 180 }) @@ -840,16 +840,26 @@ describe('web e2e: plugin manager', () => { expect(await panel.getByRole('switch', { name: '启用 自定义插件索引', exact: true }).getAttribute('aria-checked')).toBe('false') expect(await panel.getByRole('switch', { name: '启用 开发者工具', exact: true }).count()).toBe(0) expect(await panel.getByText(/Cordis|Chrome DevTools/).count()).toBe(0) + // A bundle the installation ships but the profile runs no row of stays out of both lists: the + // LiveShare card appears only once its own plugins run (covered by the visibility scenario below). + expect(await panel.locator('[data-plugin-group="extensions"] [data-plugin-package="@deepseek-ai/dsh-experimental-liveshare"]').count()).toBe(0) await panel.getByRole('button', { name: '更多', exact: true }).click() await panel.getByRole('heading', { name: '实验性插件', exact: true, level: 1 }).waitFor() expect(await panel.getByText(experimentalBundleDescription('inspector-profile', 'zh'), { exact: true }).count()).toBe(1) const bundles = await scaffold.ctx.pluginManager.listBundles() expect(bundles.some(bundle => bundle.name === '@deepseek-ai/dsh-experimental-terminal-bundle')).toBe(true) expect(await panel.getByRole('switch', { name: '启用 持久终端', exact: true }).count()).toBe(0) + // The More list also omits LiveShare until its rows run, so the offered count is one lower than + // the eligible installed optional-bundle list. + expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package="@deepseek-ai/dsh-experimental-liveshare"]').count()).toBe(0) expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package]').count()) .toBe(bundles.filter(bundle => bundle.name !== '@deepseek-ai/dsh-experimental-terminal-bundle' + && bundle.name !== '@deepseek-ai/dsh-experimental-liveshare' && bundle.official && (bundle.listedByDefault !== false || bundle.enabled || bundle.installed)).length) - for (const name of ['@deepseek-ai/dsh-cua-bundle', '@deepseek-ai/dsh-experimental-session-search', '@deepseek-ai/dsh-experimental-ralph-bundle']) { + expect(bundles.find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle')) + .toMatchObject({ enabled: false, optional: false, availability: 'missing', listedByDefault: false }) + expect(await panel.locator('[data-plugin-package="@deepseek-ai/dsh-cua-bundle"]').count()).toBe(0) + for (const name of ['@deepseek-ai/dsh-experimental-session-search', '@deepseek-ai/dsh-experimental-ralph-bundle']) { expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === name)) .toMatchObject({ enabled: false, optional: true, availability: 'installation', listedByDefault: false }) expect(await panel.locator(`[data-plugin-package="${name}"]`).count()).toBe(0) @@ -1433,6 +1443,76 @@ describe('web e2e: plugin manager', () => { expect(tripwire.pageErrors).toEqual([]) }, 90_000) + it('reveals LiveShare only while its plugin rows run, and hides it again when they stop', async () => { + // A dedicated scaffold keeps this profile patch away from the shared fixture profile above. + const visibilityScaffold = await launchWebScaffold({ profile: { packages: [] } }) + const context = await browser.newContext({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) + try { + const probe = await context.newPage() + onTestFailed(() => saveFailureShot(probe, 'web-e2e-plugin-manager-liveshare-visibility')) + const consoleWatch = watchConsole(probe) + await probe.goto(visibilityScaffold.authenticatedUrl, { waitUntil: 'load' }) + await probe.getByRole('button', { name: '插件', exact: true }).click() + const panel = probe.locator('[data-plugin-panel]') + await panel.getByRole('heading', { name: '插件', exact: true }).waitFor({ timeout: 20_000 }) + const liveShare = '@deepseek-ai/dsh-experimental-liveshare' + const card = panel.locator(`[data-plugin-package="${liveShare}"]`) + // The profile is a managed one (the plugin manager lists its bundles) whose base bundle keeps HMR + // on, so writing the patch file recomposes the running loader; each step first waits for that + // recomposition and then re-reads the Host bundle list through the page's own Refresh. + const patchFile = join(visibilityScaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml') + const declarationsRow = (): boolean => [...visibilityScaffold.ctx.loader.entries()] + .some(entry => entry.options.id === 'liveshare-viewer-declarations') + const refresh = panel.getByRole('button', { name: '刷新', exact: true }) + + // Default install: the package ships, but no row it declares runs, so neither list offers it. + await expect.poll(() => card.count(), { + timeout: 20_000, message: 'the default profile lists a LiveShare card before its rows run', + }).toBe(0) + await panel.getByRole('button', { name: '更多', exact: true }).click() + await probe.getByRole('heading', { name: '实验性插件', exact: true, level: 1 }).waitFor() + await expect.poll(() => card.count(), { + timeout: 20_000, message: 'the default More list offers a LiveShare card before its rows run', + }).toBe(0) + await panel.getByRole('button', { name: '返回插件', exact: true }).click() + + // Enabling through cordis.patch.yml runs one of the bundle's declared rows while the profile still + // selects no such bundle: the card joins the experimental list. + await writeFile(patchFile, [ + '- insert:', + ' - id: liveshare-viewer-declarations', + " name: '@deepseek-ai/dsh-experimental-liveshare/viewer-declarations'", + '', + ].join('\n')) + await expect.poll(declarationsRow, { + timeout: 30_000, message: 'the profile patch did not compose the LiveShare declarations row', + }).toBe(true) + await refresh.click() + await expect.poll(() => card.count(), { + timeout: 30_000, message: 'the patch-enabled bundle did not join the experimental list', + }).toBe(1) + + // Removing the row stops it: the card leaves both lists again. + await writeFile(patchFile, '[]\n') + await expect.poll(declarationsRow, { + timeout: 30_000, message: 'the profile patch did not remove the LiveShare declarations row', + }).toBe(false) + await refresh.click() + await expect.poll(() => card.count(), { + timeout: 30_000, message: 'the card stayed listed after its rows stopped', + }).toBe(0) + await panel.getByRole('button', { name: '更多', exact: true }).click() + await probe.getByRole('heading', { name: '实验性插件', exact: true, level: 1 }).waitFor() + await expect.poll(() => card.count(), { + timeout: 20_000, message: 'the More list kept the card after its rows stopped', + }).toBe(0) + expect(consoleWatch.pageErrors).toEqual([]) + } finally { + await context.close() + await visibilityScaffold.close() + } + }, 120_000) + it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => { expect(tripwire.warnings).toEqual([]) await assertFixtureInventory(SNAPSHOT_DIR, [ diff --git a/apps/web/tests/plugin-sticky-motion.e2e.ts b/apps/web/tests/plugin-sticky-motion.e2e.ts index ada88dbe66..86fecc4656 100644 --- a/apps/web/tests/plugin-sticky-motion.e2e.ts +++ b/apps/web/tests/plugin-sticky-motion.e2e.ts @@ -83,8 +83,8 @@ it('preserves both header layouts and follows the scroll position without changi await page.locator('[data-plugin-bar][data-natural]').waitFor() await page.evaluate(() => document.fonts.ready) // Without tabs, whether the actions fit beside the title at 470px depends on the platform font, - // so that width is pinned only with tabs. - for (const width of hasTabs ? [1280, 600, 470, 390] : [1280, 600, 390]) { + // so that width is pinned only with tabs. At 320px the tabs and actions wrap onto separate rows. + for (const width of hasTabs ? [1280, 600, 470, 390, 320] : [1280, 600, 390]) { await page.setViewportSize({ width, height: 480 }) await scrollTo(page, 0) const resting = await reference(page, false) @@ -95,7 +95,7 @@ it('preserves both header layouts and follows the scroll position without changi expect(initial.tabs).toEqual(resting.tabs) if (hasTabs && width <= 600) { const tabs = initial.tabs! - if (width === 390) { + if (width === 320) { expect(initial.toolbar.y).toBeGreaterThanOrEqual(tabs.y + tabs.height) } else { expect(initial.toolbar.x).toBeGreaterThanOrEqual(tabs.x + tabs.width) diff --git a/apps/web/tests/present-svg.e2e.ts b/apps/web/tests/present-svg.e2e.ts index 1d46fc88b8..0e9f2da75c 100644 --- a/apps/web/tests/present-svg.e2e.ts +++ b/apps/web/tests/present-svg.e2e.ts @@ -87,7 +87,8 @@ describe('web e2e: requested SVG is explicitly delivered', () => { scaffold = await launchWebScaffold({ compareReplaySession: true, extraOverlayPath: fileURLToPath(new URL('./present-svg.overlay.yml', import.meta.url)), - ...(replayOverride === undefined ? {} : { replayFixture: FIXTURE, replayOverride }), + // Replay the large SVG incrementally to avoid a zero-delay traffic burst. + ...(replayOverride === undefined ? {} : { replayFixture: FIXTURE, replayOverride, paceMs: 5 }), }) browser = await chromium.launch() page = await browser.newPage({ diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index daa7944c36..3cf7695139 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -672,7 +672,11 @@ async function toggleOptionalBundles(page: Page): Promise { inactive: string[] restored: boolean }> = [] - for (const entry of bundles.filter(candidate => candidate.optional)) { + // The packed worker composition cannot mount the Liveshare declaration row and its share host in + // the same patch batch without racing the declaration service; the ordinary Web scaffold covers + // that bundle's enable/disable path (plugin-manager.e2e.ts), so the packed toggle list skips it. + for (const entry of bundles.filter(candidate => candidate.optional + && candidate.name !== '@deepseek-ai/dsh-experimental-liveshare')) { const enable = await remote<{ application: string; error?: unknown }>('pluginManager/setBundleEnabled', { name: entry.name, enabled: true }) const plugins = await remote('pluginManager/listPlugins', {}) const disable = await remote<{ application: string }>('pluginManager/setBundleEnabled', { name: entry.name, enabled: false }) @@ -690,6 +694,7 @@ async function toggleOptionalBundles(page: Page): Promise { }) } return { + cua: bundles.filter(entry => entry.name === '@deepseek-ai/dsh-cua-bundle'), optional: bundles.filter(entry => entry.optional).map(entry => ({ name: entry.name, enabled: entry.enabled, availability: entry.availability, error: entry.error ?? null, title: typeof entry.meta?.title, icon: typeof entry.meta?.icon, metaError: entry.meta?.error ?? null, @@ -701,18 +706,16 @@ async function toggleOptionalBundles(page: Page): Promise { }) expect(result.optional.map(entry => entry.name).sort()).toEqual([...OPTIONAL_BUNDLES].sort()) expect(result.optional.filter(entry => entry.enabled || entry.availability !== 'installation')).toEqual([]) - expect(result.optional.filter(entry => entry.error !== null)).toEqual([ - expect.objectContaining({ name: '@deepseek-ai/dsh-cua-bundle', error: { code: 'unsupported-platform' } }), - ]) + expect(result.optional.filter(entry => entry.error !== null)).toEqual([]) + expect(result.cua).toHaveLength(1) + expect(result.cua[0]).toMatchObject({ name: '@deepseek-ai/dsh-cua-bundle', optional: false, + availability: 'missing', error: { code: 'unsupported-platform' } }) // Metadata resolves through the Worker's Node internal modules: every optional bundle shows its title and icon. expect(result.optional.filter(entry => entry.metaError !== null || entry.title !== 'object' || entry.icon !== 'string')).toEqual([]) expect(result.inventoryErrors).toEqual([]) console.info(`preview boot: optional bundle toggles ${JSON.stringify(result.toggles.map(({ name, enable, disable, error }) => ({ name, enable, disable, error })))}`) - expect(result.toggles.find(toggle => toggle.name === '@deepseek-ai/dsh-cua-bundle')).toMatchObject({ - enable: 'failed', disable: 'applied', error: { code: 'unsupported-platform' }, - changed: 0, inactive: [], restored: true, - }) - const supported = result.toggles.filter(toggle => toggle.name !== '@deepseek-ai/dsh-cua-bundle') + expect(result.toggles.find(toggle => toggle.name === '@deepseek-ai/dsh-cua-bundle')).toBeUndefined() + const supported = result.toggles expect(supported.filter(toggle => toggle.enable !== 'applied' || toggle.disable !== 'applied') .map(({ name, enable, disable }) => ({ name, enable, disable }))).toEqual([]) // Each bundle changes Loader rows, every enabled row it changes runs, and switching it off restores the prior rows. diff --git a/apps/web/tests/question-composer-recommended.e2e.ts b/apps/web/tests/question-composer-recommended.e2e.ts new file mode 100644 index 0000000000..96ebc2c15a --- /dev/null +++ b/apps/web/tests/question-composer-recommended.e2e.ts @@ -0,0 +1,92 @@ +/** A multi-select question opens with every option marked as recommended already chosen, wherever it sits in the list. */ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium, type Browser, type Page } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import type { SessionEvent } from '@deepseek-ai/dsh-session' +import { + assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, + launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { + connectFreshWorkspace, newEnglishPage, pinBrowserClock, pinHostClock, saveFailureShot, +} from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/question-composer-recommended', import.meta.url)) +const FIXTURE = join(SNAPSHOT_DIR, 'session.v4.jsonl') +const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md') +const MODE = webSnapshotMode() + +const PROMPT = 'Use the ask_user_question tool to ask me exactly one multi-select question with id "checks", question "Which checks should run before merging?", header "Checks", and three options in this order: label "Unit tests (Recommended)", label "Full e2e suite", and label "Type check (Recommended)". Set multi_select to true. After I answer, reply with the single word DONE and stop.' + +describe('web e2e: recommended multi-select options', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + const sessionEvents: SessionEvent[] = [] + let hostClock: ReturnType | undefined + + beforeAll(async () => { + if (MODE !== 'record') hostClock = pinHostClock() + scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15, compareReplaySession: true }) + scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) }) + browser = await chromium.launch() + page = await newEnglishPage(browser) + if (MODE !== 'record') await pinBrowserClock(page) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + await connectFreshWorkspace(page, scaffold.workspaceCwd) + }, 120_000) + + afterAll(async () => { + try { + await browser?.close() + } finally { + try { await scaffold?.close() } + finally { hostClock?.mockRestore() } + } + }) + + it('preselects every recommended option and submits them unchanged', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-question-recommended')) + if (MODE !== 'record') { + expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT]) + } + const input = page.locator('[data-composer-input]').first() + await input.waitFor({ timeout: 10_000 }) + const settled = scaffold.whenTurnSettled(MODE === 'record' ? 180_000 : 30_000) + await input.fill(PROMPT) + await input.press('Enter') + + const composer = page.locator('[data-question-key]') + await composer.waitFor({ timeout: MODE === 'record' ? 120_000 : 30_000 }) + await composer.getByRole('heading', { name: 'Which checks should run before merging?' }).waitFor({ timeout: 10_000 }) + const checked = await composer.getByRole('checkbox').evaluateAll(nodes => + nodes.map(node => node.getAttribute('aria-checked'))) + expect(checked).toEqual(['true', 'false', 'true']) + if (MODE !== 'record') { + const snapshot = await captureStableAria(page, '[data-question-key]', scaffold.workspaceCwd) + await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) + } + + await composer.getByRole('button', { name: 'Submit', exact: true }).click() + const sessionId = await settled + if (MODE === 'record') { + await recordFixture(scaffold, sessionId, FIXTURE) + return + } + const answerText = sessionEvents.filter(event => event.type === 'tool/result') + .flatMap(event => event.data.message.content.filter(item => item.type === 'text').map(item => item.text)) + .at(-1) + expect(JSON.parse(answerText ?? '')).toEqual({ + answers: [{ id: 'checks', selected: ['Unit tests (Recommended)', 'Type check (Recommended)'] }], + }) + await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(1) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + await assertFixtureInventory(SNAPSHOT_DIR, ['session.v4.jsonl', 'ui.expected.md']) + }, 200_000) +}) diff --git a/apps/web/tests/schedule-after.e2e.ts b/apps/web/tests/schedule-after.e2e.ts index fb45d6c76a..b452ce6c5f 100644 --- a/apps/web/tests/schedule-after.e2e.ts +++ b/apps/web/tests/schedule-after.e2e.ts @@ -868,6 +868,7 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => { await page.getByRole('menuitem', { name: 'Delete task', exact: true }).click() const confirmation = page.getByRole('dialog', { name: 'Delete this task?' }) await confirmation.getByRole('button', { name: 'Cancel', exact: true }).click() + expect(await moreActions.evaluate(button => button === document.activeElement)).toBe(true) expect((await scaffold.ctx.schedule.catalog()).some(task => task.id === other.id)).toBe(true) await moreActions.click() await page.getByRole('menuitem', { name: 'Delete task', exact: true }).click() diff --git a/apps/web/tests/schedule-header.e2e.ts b/apps/web/tests/schedule-header.e2e.ts index fc79fd571c..631fa3e464 100644 --- a/apps/web/tests/schedule-header.e2e.ts +++ b/apps/web/tests/schedule-header.e2e.ts @@ -58,10 +58,8 @@ it('keeps the automation title intact before moving New to another line', async facts.push(`${language} ${width}px: title and New single-line, New beside the title; contained; New retains focus`) } if (language === 'en') { - // 509px collapses the left sidebar to its rail; expanding it leaves the - // page column narrow enough (measured 172px) to force the wrap. - await page.setViewportSize({ width: 509, height: 1000 }) - await page.getByRole('button', { name: 'Open sidebar', exact: true }).first().click() + // Constrain this page independently of the shell's drawer breakpoint. + await header.evaluate((element) => { element.style.width = '220px' }) await expect.poll(() => header.evaluate((element) => { const title = element.querySelector('h1')! const button = element.querySelector('button')! @@ -76,7 +74,7 @@ it('keeps the automation title intact before moving New to another line', async : buttonRect.top >= titleRect.bottom - 1 ? 'below' as const : 'overlapping' as const, } })).toEqual({ titleSingleLine: true, placement: 'below' }) - facts.push('en 509px with sidebar expanded: title single-line; New moves below the title') + facts.push('en 220px page column: title single-line; New moves below the title') } expect(consoleWatch.pageErrors).toEqual([]) } finally { diff --git a/apps/web/tests/schedule-phone.e2e.ts b/apps/web/tests/schedule-phone.e2e.ts index b28049ba3d..e82104196a 100644 --- a/apps/web/tests/schedule-phone.e2e.ts +++ b/apps/web/tests/schedule-phone.e2e.ts @@ -7,6 +7,7 @@ import { expect, it } from 'vitest' import { SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-schedule' import { launchWebScaffold, watchConsole } from './scaffold.ts' +import { revealNavigationDrawer } from './support.ts' const PHONE_TASK_SESSION = SessionId('schedule-phone-web-e2e') const PHONE_TASK_TITLE = 'Review overdue deployment' @@ -25,6 +26,7 @@ interface ViewFacts { /** Open the Automation tasks page and return its panel. */ async function openTaskManager(page: Page, url: string): Promise { await page.goto(url, { waitUntil: 'load' }) + await revealNavigationDrawer(page, 'zh') await page.getByRole('button', { name: '自动化任务', exact: true }).click() const panel = page.getByTestId('task-manager-page') await panel.getByRole('heading', { name: '自动化任务', exact: true }).waitFor({ timeout: 30_000 }) diff --git a/apps/web/tests/sidebar-alignment.e2e.ts b/apps/web/tests/sidebar-alignment.e2e.ts new file mode 100644 index 0000000000..6ea3f3dedb --- /dev/null +++ b/apps/web/tests/sidebar-alignment.e2e.ts @@ -0,0 +1,561 @@ +/** Browser geometry for sidebar controls, text clearance, and drag previews. */ +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import type { Browser, Locator, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed, vi } from 'vitest' +import type { AccountView } from '@deepseek-ai/dsh-deepseek-account/types' +import type {} from '@deepseek-ai/dsh-deepseek-account' +import type {} from '@deepseek-ai/dsh-api-session-controller' +import { + compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole, + webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const SEED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url)) +const GEOMETRY_EXPECTED = fileURLToPath(new URL('./expected/sidebar-alignment/geometry.expected.md', import.meta.url)) +const MODE = webSnapshotMode() +const ACTION_TARGET_SIZE = 24 +const ACTION_LAYOUT_SIZE = 16 +const ACTION_CENTER_SPACING = 29 +const ACTION_TRAILING_INSET = 7 +const SEARCH_HEIGHT = 32 +const SEARCH_TRANSITION_MS = 180 +const HEADER_LABEL_OFFSET = 7 +const HEADER_ACTION_RIGHT_OFFSETS = { search: -61, view: -29, add: 3 } + +/** + * Measure painted glyphs and pointer targets independently of the row's layout cells. + * @param row - visible, hovered Workspace or Session row. + * @returns row bounds, action geometry, and browser hit-test results. + */ +async function measureRowActions(row: Locator) { + return row.evaluate((element) => { + const rowRect = element.getBoundingClientRect() + const title = element.querySelector('[class*="title"]') + if (title === null) throw new Error('row title not in the DOM') + const buttons = [...element.querySelectorAll('button')] + return { + height: rowRect.height, + overflows: element.scrollWidth > element.clientWidth, + titleRight: title.getBoundingClientRect().right, + actions: buttons.map((button) => { + const rect = button.getBoundingClientRect() + const glyph = button.querySelector('svg') + if (glyph === null) throw new Error('row action glyph not in the DOM') + const glyphRect = glyph.getBoundingClientRect() + const style = getComputedStyle(button) + const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } + // Side samples cover the outer target; corner samples stay inside the rounded border. + const points = [ + { x: center.x - 11.5, y: center.y }, + { x: center.x + 11.5, y: center.y }, + { x: center.x, y: center.y - 11.5 }, + { x: center.x, y: center.y + 11.5 }, + { x: center.x - 9.5, y: center.y - 9.5 }, + { x: center.x + 9.5, y: center.y - 9.5 }, + { x: center.x - 9.5, y: center.y + 9.5 }, + { x: center.x + 9.5, y: center.y + 9.5 }, + ] + const hits = (x: number, y: number): boolean => { + const target = document.elementFromPoint(x, y) + return target !== null && button.contains(target) + } + return { + label: button.getAttribute('aria-label'), + width: rect.width, + height: rect.height, + layoutWidth: rect.width + Number.parseFloat(style.marginLeft) + Number.parseFloat(style.marginRight), + layoutHeight: rect.height + Number.parseFloat(style.marginTop) + Number.parseFloat(style.marginBottom), + margins: [style.marginTop, style.marginRight, style.marginBottom, style.marginLeft], + pseudoContent: getComputedStyle(button, '::after').content, + center, + rowCenterOffset: center.y - (rowRect.y + rowRect.height / 2), + trailingInset: rowRect.right - rect.right, + glyphWidth: glyphRect.width, + glyphHeight: glyphRect.height, + glyphCenterOffset: { + x: glyphRect.x + glyphRect.width / 2 - center.x, + y: glyphRect.y + glyphRect.height / 2 - center.y, + }, + insideRow: center.x - 12 >= rowRect.left && center.x + 12 <= rowRect.right + && center.y - 12 >= rowRect.top && center.y + 12 <= rowRect.bottom, + points, + edgeHits: points.map(({ x, y }) => hits(x, y)), + // Clear Chromium's boundary rounding while still rejecting an oversized 28px target. + outsideHits: [ + hits(center.x - 13.5, center.y), hits(center.x + 13.5, center.y), + hits(center.x, center.y - 13.5), hits(center.x, center.y + 13.5), + ], + } + }), + } + }) +} + +describe('web e2e: sidebar search alignment and row action targets', () => { + const workspaceTitle = 'Sidebar geometry workspace' + const sessionTitle = 'Sidebar geometry session' + const archivedTitle = 'Archived geometry session with a long title that fills its sidebar row' + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + const workspace = await scaffold.ctx.workspaceRegistry.create(scaffold.workspaceCwd, workspaceTitle) + const id = await seedSession(scaffold, await readFile(SEED, 'utf8'), 'sidebar-action-geometry') + await workspace.attachSession(id) + await scaffold.ctx.sessionController.rename({ sessionId: id, title: sessionTitle }) + const archivedId = await seedSession(scaffold, await readFile(SEED, 'utf8'), 'sidebar-search-geometry') + await workspace.attachSession(archivedId) + await scaffold.ctx.sessionController.rename({ sessionId: archivedId, title: archivedTitle }) + await scaffold.ctx.workspaceRegistry.archiveSession(archivedId) + browser = await chromium.launch() + }) + + beforeEach(async () => { + page = await newEnglishPage(browser, 800) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + const workspace = page.locator('[data-row-key^="workspace:"]').filter({ hasText: workspaceTitle }) + await workspace.waitFor({ timeout: 30_000 }) + await expect.poll(async () => { + if (await workspace.getAttribute('aria-expanded') !== 'true') { + await workspace.getByText(workspaceTitle, { exact: true }).click() + } + return workspace.getAttribute('aria-expanded') + }, { timeout: 10_000 }).toBe('true') + await page.locator('[data-row-key="session:sidebar-action-geometry"]').waitFor() + }) + + afterEach(async () => { + await page?.close() + }) + + afterAll(async () => { + try { await browser?.close() } finally { await scaffold?.close() } + }) + + it('aligns the 32px expanded search with both edges of New session', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-search-alignment')) + const searchButton = page.getByRole('button', { name: 'Search sessions', exact: true }) + const input = page.getByPlaceholder('Search session names', { exact: true }) + const search = input.locator('..') + const newSession = page.locator('button[class*="newSession"]') + const measure = async () => { + const searchBox = await search.boundingBox() + const newSessionBox = await newSession.boundingBox() + if (searchBox === null || newSessionBox === null) throw new Error('sidebar controls have no layout box') + return { + height: searchBox.height, + leftOffset: searchBox.x - newSessionBox.x, + rightOffset: searchBox.x + searchBox.width - newSessionBox.x - newSessionBox.width, + } + } + for (const dark of [false, true]) { + await page.evaluate(dark => document.body.toggleAttribute('data-ds-dark-theme', dark), dark) + await expect.poll(async () => { + const reference = await newSession.boundingBox() + if (reference === null) throw new Error('New session has no layout box') + const controls = [searchButton, page.getByRole('button', { name: 'View options', exact: true }), page.getByRole('button', { name: 'Add workspace', exact: true })] + const boxes = await Promise.all(controls.map(control => control.boundingBox())) + return boxes.map(box => box === null ? null : box.x + box.width - reference.x - reference.width) + }).toEqual(Object.values(HEADER_ACTION_RIGHT_OFFSETS)) + const labelBox = await page.locator('[class*="sectionHeader"] [class*="sectionLabel"]').boundingBox() + const referenceBox = await newSession.boundingBox() + if (labelBox === null || referenceBox === null) throw new Error('Sidebar heading has no layout box') + expect(labelBox.x - referenceBox.x).toBe(HEADER_LABEL_OFFSET) + await searchButton.click() + await expect.poll(() => searchButton.getAttribute('aria-expanded')).toBe('true') + await expect.poll(measure, { timeout: 10_000 }).toEqual({ height: SEARCH_HEIGHT, leftOffset: 0, rightOffset: 0 }) + expect(await input.evaluate(element => element === document.activeElement)).toBe(true) + await input.press('Escape') + await expect.poll(() => searchButton.getAttribute('aria-expanded')).toBe('false') + } + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) + + it('interpolates header spacing with the search expansion and collapse', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-search-transition')) + const header = page.locator('[class*="sectionHeader"]').filter({ has: page.getByRole('button', { name: 'Search sessions', exact: true }) }) + for (const expand of [true, false]) { + const samples = await header.evaluate(async (element, { expand, duration }) => { + const button = element.querySelector('button[aria-label="Search sessions"]') + const input = element.querySelector('input') + if (button === null || input === null) throw new Error('Search controls are missing') + // Pause transitions at their first frame, before scheduler delays can consume their duration. + const pause = (event: Event): void => { + if (event.target instanceof Element) { + for (const animation of event.target.getAnimations()) animation.pause() + } + } + element.addEventListener('transitionrun', pause, true) + try { + await new Promise((resolve) => { + element.addEventListener('transitionrun', () => { resolve() }, { once: true }) + if (expand) button.click() + else input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) + }) + const animations = element.getAnimations({ subtree: true }) + const headerTransitions = element.getAnimations().map(animation => ({ + property: animation instanceof CSSTransition ? animation.transitionProperty : '', + duration: animation.effect?.getTiming().duration, + })) + const frames = [0, 0.25, 0.5, 0.75, 1].map((progress) => { + for (const animation of animations) { animation.pause(); animation.currentTime = duration * progress } + const style = getComputedStyle(element) + return { + marginRight: Number.parseFloat(style.marginRight), + paddingLeft: Number.parseFloat(style.paddingLeft), + paddingRight: Number.parseFloat(style.paddingRight), + gap: Number.parseFloat(style.columnGap), + width: element.getBoundingClientRect().width, + } + }) + for (const animation of animations) animation.finish() + return { headerTransitions, frames } + } finally { + element.removeEventListener('transitionrun', pause, true) + } + }, { expand, duration: SEARCH_TRANSITION_MS }) + for (const property of ['margin-right', 'padding-left', 'padding-right', 'column-gap']) { + expect(samples.headerTransitions).toContainEqual({ property, duration: SEARCH_TRANSITION_MS }) + } + const first = samples.frames[0]! + const last = samples.frames.at(-1)! + expect(first).toMatchObject(expand + ? { marginRight: -4, paddingLeft: 12, paddingRight: 1, gap: 4 } + : { marginRight: 0, paddingLeft: 4, paddingRight: 4, gap: 0 }) + expect(last).toMatchObject(expand + ? { marginRight: 0, paddingLeft: 4, paddingRight: 4, gap: 0 } + : { marginRight: -4, paddingLeft: 12, paddingRight: 1, gap: 4 }) + for (const frame of samples.frames.slice(1, -1)) { + for (const key of ['marginRight', 'paddingLeft', 'paddingRight', 'gap', 'width'] as const) { + expect(frame[key]).toBeGreaterThan(Math.min(first[key], last[key])) + expect(frame[key]).toBeLessThan(Math.max(first[key], last[key])) + } + const progress = (frame.marginRight - first.marginRight) / (last.marginRight - first.marginRight) + for (const key of ['paddingLeft', 'paddingRight', 'gap'] as const) { + expect((frame[key] - first[key]) / (last[key] - first[key])).toBeCloseTo(progress, 2) + } + } + } + }) + + it.each([ + { usedSearch: false, entry: 'toggle' }, + { usedSearch: true, entry: 'toggle' }, + { usedSearch: false, entry: 'search' }, + { usedSearch: true, entry: 'search' }, + { usedSearch: true, entry: 'toggle-with-query' }, + ] as const)('lands the header at its final rail-to-wide geometry ($entry, prior search: $usedSearch)', async ({ usedSearch, entry }) => { + onTestFailed(() => saveFailureShot(page, `web-e2e-sidebar-rail-${entry}-${String(usedSearch)}`)) + if (usedSearch) { + await page.getByRole('button', { name: 'Search sessions', exact: true }).click() + const input = page.getByPlaceholder('Search session names', { exact: true }) + if (entry === 'toggle-with-query') await input.fill(sessionTitle) + else await input.press('Escape') + } + await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).click() + await expect.poll(() => page.getByRole('button', { name: 'Account menu', exact: true }).getAttribute('data-collapsed')).toBe('true') + const add = page.getByRole('button', { name: 'Add workspace', exact: true }) + const samples = await add.evaluate(async (button, entry) => { + const header = button.closest('[class*="sectionHeader"]') + const root = header?.parentElement + if (header === null || root === null || root === undefined) throw new Error('Workspace header is missing') + const railClass = root.className + const measure = () => { + const origin = root.getBoundingClientRect() + const bounds = header.getBoundingClientRect() + const addBounds = button.getBoundingClientRect() + const style = getComputedStyle(header) + return { + left: bounds.left - origin.left, right: bounds.right - origin.left, + top: bounds.top - origin.top, height: bounds.height, + // Search hides this cluster with its own animation; only compare the visible add control. + add: entry === 'toggle' + ? { left: addBounds.left - origin.left, right: addBounds.right - origin.left } + : null, + marginRight: Number.parseFloat(style.marginRight), + paddingLeft: Number.parseFloat(style.paddingLeft), paddingRight: Number.parseFloat(style.paddingRight), + gap: Number.parseFloat(style.columnGap), + transitionDuration: style.transitionDuration, + searchExpanded: header.querySelector('button[aria-label="Search sessions"]')?.getAttribute('aria-expanded'), + } + } + const first = await new Promise>((resolve) => { + // Observe the first rendered wide frame after the rail gesture's React updates. + const observer = new MutationObserver(() => { + if (root.className === railClass) return + observer.disconnect() + requestAnimationFrame(() => { resolve(measure()) }) + }) + observer.observe(root, { attributes: true, attributeFilter: ['class'] }) + const trigger = entry === 'search' + ? root.querySelector('button[aria-label="Search sessions"]') + : document.querySelector('button[aria-label="Open sidebar"]') + if (trigger === null) { observer.disconnect(); throw new Error('Rail expansion trigger is missing') } + trigger.click() + }) + const headerTransitions = header.getAnimations() + .filter(animation => animation instanceof CSSTransition) + .map(animation => animation.transitionProperty) + await Promise.all(root.getAnimations({ subtree: true }) + .filter(animation => animation.effect?.getTiming().iterations !== Infinity) + .map(animation => animation.finished.then(() => undefined, () => undefined))) + return { + first, last: measure(), headerTransitions, + sameHeader: root.querySelector('[class*="sectionHeader"]') === header, + sameAdd: root.querySelector('button[aria-label="Add workspace"]') === button, + } + }, entry) + expect(samples.headerTransitions).toEqual([]) + expect(samples.first.transitionDuration).toBe('0s') + expect(samples.first).toEqual(samples.last) + expect(samples.first).toMatchObject(entry !== 'toggle' + ? { marginRight: 0, paddingLeft: 4, paddingRight: 4, gap: 0, searchExpanded: 'true' } + : { marginRight: -4, paddingLeft: 12, paddingRight: 1, gap: 4, searchExpanded: 'false' }) + expect(samples.sameHeader).toBe(true) + expect(samples.sameAdd).toBe(true) + if (entry === 'search') { + await expect.poll(() => page.getByPlaceholder('Search session names', { exact: true }) + .evaluate(element => element === document.activeElement)).toBe(true) + } + expect(tripwire.pageErrors).toEqual([]) + }) + + it('keeps a clipped search title clear of the unarchive target', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-search-title-clearance')) + await page.getByRole('button', { name: 'View options', exact: true }).click() + await page.getByRole('menuitem', { name: 'All conversations (show archived)', exact: true }).click() + await expect.poll(() => page.locator('[data-row-key="session:sidebar-search-geometry"]').textContent()).toContain(archivedTitle) + await page.getByRole('button', { name: 'Search sessions', exact: true }).click() + await page.getByPlaceholder('Search session names', { exact: true }).fill('Archived geometry') + const row = page.getByRole('treeitem').filter({ hasText: archivedTitle }) + await row.hover() + await row.getByRole('button', { name: 'Unarchive session', exact: true }).waitFor() + const geometry = await row.evaluate((element) => { + const title = element.querySelector('[class*="searchResultTitle"]') + const button = element.querySelector('button') + if (title === null || button === null) throw new Error('Search result title or action is missing') + const titleRect = title.getBoundingClientRect() + const buttonRect = button.getBoundingClientRect() + const titleEdge = document.elementFromPoint(titleRect.right - 0.5, titleRect.top + titleRect.height / 2) + return { + clipped: title.scrollWidth > title.clientWidth, + labelGap: Number.parseFloat(getComputedStyle(title).marginRight), + targetWidth: buttonRect.width, + clearance: buttonRect.left - titleRect.right, + titleEdgeHitsTitle: titleEdge !== null && title.contains(titleEdge), + overflows: element.scrollWidth > element.clientWidth, + } + }) + expect(geometry).toEqual({ + clipped: true, labelGap: 6, targetWidth: ACTION_TARGET_SIZE, clearance: 2, titleEdgeHitsTitle: true, overflows: false, + }) + await compareOrRefreshGolden(GEOMETRY_EXPECTED, [ + '# Sidebar alignment', '', + `- search title is clipped: ${String(geometry.clipped)}`, + `- unarchive hit target width: ${geometry.targetWidth}px`, + `- clipped search title label gap: ${geometry.labelGap}px`, + `- clipped search title clearance from unarchive hit target: ${geometry.clearance}px`, + `- clipped search title edge hit-tests to title: ${String(geometry.titleEdgeHitsTitle)}`, + ].join('\n'), MODE) + expect(tripwire.pageErrors).toEqual([]) + }) + + it.each(['session', 'workspace'] as const)('retains %s row spacing in the native drag preview', async (kind) => { + onTestFailed(() => saveFailureShot(page, `web-e2e-sidebar-${kind}-drag-preview`)) + const row = kind === 'session' + ? page.locator('[data-row-key="session:sidebar-action-geometry"]') + : page.locator('[data-row-key^="workspace:"]').filter({ hasText: workspaceTitle }) + await row.evaluate((element) => { + element.parentElement!.style.setProperty('--dsh-workspace-indent', '24px') + element.parentElement!.style.setProperty('--unrelated-preview-variable', '17px') + }) + const box = await row.boundingBox() + if (box === null) throw new Error('Drag source has no layout box') + await page.mouse.move(box.x + 40, box.y + box.height / 2) + await page.mouse.down() + try { + await page.mouse.move(box.x + 45, box.y + box.height + 10, { steps: 8 }) + const preview = page.locator('body > [inert][aria-hidden="true"] > [role="treeitem"]') + await preview.waitFor({ state: 'attached' }) + const spacing = (target: Locator) => target.evaluate((element) => { + const style = getComputedStyle(element) + const slot = [...element.querySelectorAll('[class*="slot"]')].find(candidate => candidate.getBoundingClientRect().width > 0) + if (slot === undefined) throw new Error('Row leading slot is missing') + return { + paddingStart: style.paddingInlineStart, paddingEnd: style.paddingInlineEnd, + borderStart: style.borderInlineStartWidth, borderEnd: style.borderInlineEndWidth, + gap: style.columnGap, + width: element.getBoundingClientRect().width, + height: element.getBoundingClientRect().height, + slotOffset: slot.getBoundingClientRect().left - element.getBoundingClientRect().left, + } + }) + expect(await spacing(preview)).toEqual(await spacing(row)) + expect(await preview.evaluate(element => [...element.parentElement!.style].filter(name => name.startsWith('--')).sort())) + .toEqual(['--dsh-sidebar-label-gap', '--dsh-sidebar-paint-inset', '--dsh-sidebar-row-padding']) + const titleOffset = (target: Locator) => target.evaluate((element) => { + const title = element.querySelector('[class*="title"]') + if (title === null) throw new Error('Row title is missing') + return title.getBoundingClientRect().left - element.getBoundingClientRect().left + }) + expect(await titleOffset(preview)).toBe(await titleOffset(row)) + } finally { + await page.keyboard.press('Escape') + await page.mouse.up() + } + await page.locator('body > [inert][aria-hidden="true"] > [role="treeitem"]').waitFor({ state: 'detached' }) + expect(tripwire.pageErrors).toEqual([]) + }) + + it.each([false, true])('keeps account footer geometry in wide and rail layouts (signed in: %s)', async (signedIn) => { + onTestFailed(() => saveFailureShot(page, `web-e2e-sidebar-account-${String(signedIn)}`)) + const account = scaffold.ctx.deepseekAccount + const state: AccountView = { + status: 'credential-stored', attempt: null, + links: { usageUrl: 'https://platform.deepseek.com/usage', topUpUrl: 'https://platform.deepseek.com/top_up' }, + } + // Only Platform identity is synthetic; the account provider's RPC and client renderer remain composed. + const mocks = signedIn ? [ + vi.spyOn(account, 'getState').mockResolvedValue(state), + vi.spyOn(account, 'getProfile').mockResolvedValue({ + status: 'ready', value: { id: null, name: 'Sidebar account', contact: null, avatarUrl: null }, + }), + vi.spyOn(account, 'getBalance').mockResolvedValue({ status: 'ready', value: [], bonusWallets: [] }), + vi.spyOn(account, 'watch').mockImplementation(async function* (signal) { + yield state + if (signal.aborted) return + await new Promise((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) }) + }), + ] : [] + try { + if (signedIn) await page.reload({ waitUntil: 'load' }) + const accountButton = page.getByRole('button', { name: 'Account menu', exact: true }) + await expect.poll(() => accountButton.getAttribute('data-signed-out')).toBe(String(!signedIn)) + const measure = () => accountButton.evaluate((element) => { + const button = element.getBoundingClientRect() + const style = getComputedStyle(element) + const newSession = document.querySelector('button[class*="newSession"]') + if (newSession === null) throw new Error('New session reference is missing') + const reference = newSession.getBoundingClientRect() + const footer = element.closest('[class*="footArea"]')?.getBoundingClientRect() + if (footer === undefined) throw new Error('Account footer is missing') + return { + height: button.height, width: button.width, + footerCenterOffset: (button.left + button.right - footer.left - footer.right) / 2, + leftOffset: button.left - reference.left, rightOffset: button.right - reference.right, + paintedLeftOffset: button.left + Number.parseFloat(style.borderLeftWidth) - reference.left, + paintedRightOffset: button.right - Number.parseFloat(style.borderRightWidth) - reference.right, + } + }) + const wide = await measure() + expect(wide.height).toBe(signedIn ? 44 : 32) + expect(wide.leftOffset).toBe(signedIn ? -2 : -4) + expect(wide.rightOffset).toBe(signedIn ? 2 : 4) + expect(wide.paintedLeftOffset).toBe(signedIn ? -2 : -1) + expect(wide.paintedRightOffset).toBe(signedIn ? 2 : 1) + await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).click() + await expect.poll(() => accountButton.getAttribute('data-collapsed')).toBe('true') + await expect.poll(async () => { + const rect = await accountButton.boundingBox() + return rect === null ? null : { width: rect.width, height: rect.height } + }).toEqual({ width: 36, height: 36 }) + await page.locator('button[class*="newSession"]').evaluate(async (element) => { + await Promise.all(element.getAnimations().map(animation => animation.finished)) + }) + const rail = await measure() + expect(rail.footerCenterOffset).toBe(0) + await compareOrRefreshGolden( + fileURLToPath(new URL(`./expected/sidebar-alignment/account-${signedIn ? 'signed-in' : 'signed-out'}.expected.md`, import.meta.url)), + JSON.stringify({ wide, rail }, null, 2), MODE, + ) + expect(tripwire.pageErrors).toEqual([]) + } finally { + await page.close() + mocks.forEach((mock) => { mock.mockRestore() }) + } + }) + + it.each(['session', 'workspace'] as const)('keeps %s action glyphs in 16px layout space with 24px buttons', async (kind) => { + onTestFailed(() => saveFailureShot(page, `web-e2e-sidebar-${kind}-action-targets`)) + const row = kind === 'session' + ? page.locator('[data-row-key="session:sidebar-action-geometry"]') + : page.locator('[data-row-key^="workspace:"]').filter({ hasText: workspaceTitle }) + const restingBox = await row.boundingBox() + await row.hover() + const labels = kind === 'session' + ? [`Session actions for ${sessionTitle}`, 'Archive session', 'Pin session'] as const + : [`Workspace actions for ${workspaceTitle}`, `New session in ${workspaceTitle}`] as const + await row.getByRole('button', { name: labels[0], exact: true }).waitFor() + const metrics = await measureRowActions(row) + expect(await row.boundingBox()).toEqual(restingBox) + expect(metrics.height).toBe(kind === 'session' ? 32 : 34) + expect(metrics.overflows).toBe(false) + expect(metrics.actions.map(action => action.label)).toEqual(labels) + for (const [index, action] of metrics.actions.entries()) { + expect(action.width).toBe(ACTION_TARGET_SIZE) + expect(action.height).toBe(ACTION_TARGET_SIZE) + expect(action.layoutWidth).toBe(ACTION_LAYOUT_SIZE) + expect(action.layoutHeight).toBe(ACTION_LAYOUT_SIZE) + expect(action.margins).toEqual(['-4px', '-4px', '-4px', '-4px']) + expect(action.pseudoContent).toBe('none') + expect(action.rowCenterOffset).toBe(0) + expect(action.glyphWidth).toBe(index === 0 ? 16 : 14) + expect(action.glyphHeight).toBe(index === 0 ? 16 : 14) + expect(action.glyphCenterOffset).toEqual({ x: 0, y: 0 }) + expect(action.insideRow).toBe(true) + expect(action.edgeHits).toEqual(Array(8).fill(true)) + expect(action.outsideHits).toEqual([false, false, false, false]) + const previous = metrics.actions[index - 1] + if (previous !== undefined) { + expect(action.center.x - previous.center.x).toBe(ACTION_CENTER_SPACING) + expect(previous.center.x + 12).toBeLessThan(action.center.x - 12) + } else { + expect(metrics.titleRight).toBeLessThanOrEqual(action.center.x - 12) + } + } + expect(metrics.actions.at(-1)?.trailingInset).toBe(ACTION_TRAILING_INSET) + + if (kind === 'session') expect(await row.getAttribute('aria-selected')).toBe('false') + const selectedRows = page.locator('[role="tree"][aria-label="Sessions"] [aria-selected="true"]') + const selectedKeys = await selectedRows.evaluateAll(rows => rows.map(row => row.getAttribute('data-row-key'))) + const expanded = await row.getAttribute('aria-expanded') + const menuButton = row.getByRole('button', { name: labels[0], exact: true }) + const menu = page.getByRole('menu') + const menuAction = kind === 'session' ? 'Pin session' : 'Rename' + const first = metrics.actions[0] + if (first === undefined) throw new Error('row has no menu trigger') + for (const point of first.points.slice(0, 4)) { + // Edge clicks exercise the enlarged target rather than the glyph's center. + await page.mouse.click(point.x, point.y) + await menu.getByRole('menuitem', { name: menuAction, exact: true }).waitFor() + expect(await selectedRows.evaluateAll(rows => rows.map(row => row.getAttribute('data-row-key')))).toEqual(selectedKeys) + expect(await row.getAttribute('aria-expanded')).toBe(expanded) + await page.keyboard.press('Escape') + await menu.waitFor({ state: 'detached' }) + await menuButton.hover() + } + const tooltipAnchor = row.getByRole('button', { name: labels[1], exact: true }) + const tooltip = page.getByRole('tooltip').filter({ hasText: kind === 'session' ? 'Archive' : 'New session' }) + await tooltipAnchor.hover() + await tooltip.waitFor({ timeout: 10_000 }) + const anchorBox = await tooltipAnchor.boundingBox() + const tooltipBox = await tooltip.boundingBox() + if (anchorBox === null || tooltipBox === null) throw new Error('hover tooltip or its anchor has no layout box') + expect(tooltipBox.y).toBe(anchorBox.y + anchorBox.height + 8) + expect(tooltipBox.x + tooltipBox.width).toBe(anchorBox.x + anchorBox.width) + await page.mouse.move(1000, 400) + await tooltip.waitFor({ state: 'detached' }) + expect(await tooltipAnchor.isVisible()).toBe(false) + expect(await page.getByRole('tooltip').count()).toBe(0) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }) +}) diff --git a/apps/web/tests/sidebar-drawer.e2e.ts b/apps/web/tests/sidebar-drawer.e2e.ts new file mode 100644 index 0000000000..99b5c3fd43 --- /dev/null +++ b/apps/web/tests/sidebar-drawer.e2e.ts @@ -0,0 +1,737 @@ +/** Narrow sidebar geometry, navigation, focus and retained content in the shipped profile. */ +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import type { Locator } from 'playwright' +import { chromium } from 'playwright' +import { expect, it } from 'vitest' +import { compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole, webSnapshotMode } from './scaffold.ts' + +/** A returning rail uses its compact layout without wide-content or icon-entry animations. */ +async function assertCompactRail(sidebar: Locator): Promise { + expect(await sidebar.getByRole('button', { name: 'New session', exact: true }).count()).toBe(1) + expect(await sidebar.evaluate((el) => { + const root = el.firstElementChild! + return { + fits: root.getBoundingClientRect().width <= el.parentElement!.getBoundingClientRect().width, + animations: root.getAnimations({ subtree: true }) + .filter((animation): animation is CSSAnimation => + animation instanceof CSSAnimation && /wide-in|rail-in|rail-fade-in/.test(animation.animationName)) + .map(animation => animation.animationName), + } + })).toEqual({ fits: true, animations: [] }) +} + +it.each(['light', 'dark'] as const)('keeps the macOS drawer opaque and excludes inert content from window drag regions in %s mode', async (colorScheme) => { + const scaffold = await launchWebScaffold({ + 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: 680 }, colorScheme, locale: 'en-US' }) + await page.goto(scaffold.authenticatedUrl) + const opener = page.getByRole('button', { name: 'Open sidebar', exact: true }) + await opener.waitFor() + await page.evaluate(() => { document.documentElement.dataset.platform = 'darwin' }) + await opener.click() + const drawer = page.getByRole('dialog', { name: 'Navigation', exact: true }) + await drawer.waitFor() + const column = page.locator('[data-slot="sidebar"]').locator('..') + const background = await column.evaluate((el) => { + const style = getComputedStyle(el) + return { color: style.backgroundColor, image: style.backgroundImage } + }) + expect(background.image).toBe('none') + expect(background.color).toMatch(/^rgb\(/) + expect(await drawer.getByRole('button', { name: 'New session', exact: true }).last().evaluate((el) => { + const actual = getComputedStyle(el).backgroundColor + const root = document.documentElement + const platform = root.dataset.platform + delete root.dataset.platform + try { + return actual === getComputedStyle(el).backgroundColor + } finally { + root.dataset.platform = platform + } + })).toBe(true) + const inactiveContent = page.locator('[inert], [inert] *') + expect(await inactiveContent.count()).toBeGreaterThan(0) + expect(await inactiveContent.evaluateAll(elements => elements + .filter(el => getComputedStyle(el).getPropertyValue('app-region') !== 'none') + .map(el => `${el.tagName}.${el.getAttribute('class')}: ${getComputedStyle(el).getPropertyValue('app-region')}`))) + .toEqual([]) + expect(await drawer.locator('[data-window-drag]').first().evaluate(el => getComputedStyle(el).getPropertyValue('app-region'))) + .toBe('drag') + const collapse = drawer.getByRole('button', { name: 'Collapse sidebar', exact: true }) + await collapse.click() + await drawer.waitFor({ state: 'hidden' }) + await page.setViewportSize({ width: 1200, height: 800 }) + const sidebar = page.locator('[data-slot="sidebar"]').locator('..') + await expect.poll(() => sidebar.evaluate(el => getComputedStyle(el).backgroundImage)).not.toBe('none') + const activeDragRows = page.locator('[data-window-drag]:not([inert] [data-window-drag])') + expect(await activeDragRows.count()).toBeGreaterThan(0) + expect(await activeDragRows.evaluateAll(rows => rows.every(row => getComputedStyle(row).getPropertyValue('app-region') === 'drag'))) + .toBe(true) + } finally { + await browser.close() + } + } finally { + await scaffold.close() + } +}) + +it('hides a collapsed rail immediately when it becomes a drawer', async () => { + const scaffold = await launchWebScaffold({ + 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: 768, height: 844 }, locale: 'en-US' }) + await page.goto(scaffold.authenticatedUrl) + await page.getByRole('button', { name: 'Open sidebar', exact: true }).waitFor() + const sidebar = page.locator('[data-slot="sidebar"]') + const sidebarColumn = sidebar.locator('..') + await expect.poll(async () => (await sidebarColumn.boundingBox())?.width).toBe(56) + await sidebarColumn.evaluate((el) => { + el.parentElement!.style.setProperty('--ds-transition-duration-slow', '100s') + }) + await page.setViewportSize({ width: 767, height: 844 }) + await page.locator('[data-sidebar-drawer]').waitFor() + const state = await sidebarColumn.evaluate(el => ({ + visibility: getComputedStyle(el).visibility, + transitions: el.getAnimations().filter(animation => animation instanceof CSSTransition) + .map(animation => animation.transitionProperty), + })) + expect(state).toEqual({ visibility: 'hidden', transitions: [] }) + await page.setViewportSize({ width: 768, height: 844 }) + await page.locator('[data-sidebar-drawer]').waitFor({ state: 'detached' }) + await assertCompactRail(sidebar) + await page.setViewportSize({ width: 767, height: 844 }) + await page.locator('[data-sidebar-drawer]').waitFor() + await page.getByRole('button', { name: 'Open sidebar', exact: true }).click() + const drawer = page.getByRole('dialog', { name: 'Navigation', exact: true }) + await drawer.waitFor() + expect(await sidebarColumn.evaluate(el => el.getAnimations().some(animation => + animation instanceof CSSTransition && animation.transitionProperty === 'transform'))).toBe(true) + await sidebarColumn.evaluate((el) => { + for (const animation of el.parentElement!.getAnimations({ subtree: true })) animation.finish() + }) + await drawer.getByRole('button', { name: 'Collapse sidebar', exact: true }).click() + expect(await sidebarColumn.evaluate(el => el.getAnimations().some(animation => + animation instanceof CSSTransition && animation.transitionProperty === 'transform'))).toBe(true) + await sidebarColumn.evaluate((el) => { + for (const animation of el.parentElement!.getAnimations({ subtree: true })) animation.finish() + }) + await page.setViewportSize({ width: 768, height: 844 }) + await page.locator('[data-sidebar-drawer]').waitFor({ state: 'detached' }) + await assertCompactRail(sidebar) + } finally { + await browser.close() + } + } finally { + await scaffold.close() + } +}) + +it('retains the closing slide when an expanded sidebar becomes a collapsed drawer', async () => { + const scaffold = await launchWebScaffold({ + 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: 1024, height: 844 }, locale: 'en-US' }) + await page.goto(scaffold.authenticatedUrl) + await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).waitFor() + const sidebarColumn = page.locator('[data-slot="sidebar"]').locator('..') + await expect.poll(async () => (await sidebarColumn.boundingBox())?.width).toBe(280) + await sidebarColumn.evaluate((el) => { + el.parentElement!.style.setProperty('--ds-transition-duration-slow', '100s') + }) + await page.setViewportSize({ width: 767, height: 844 }) + await page.locator('[data-sidebar-drawer][data-sidebar-collapsed]').waitFor() + expect(await sidebarColumn.evaluate(el => el.getAnimations().some(animation => + animation instanceof CSSTransition && animation.transitionProperty === 'transform'))).toBe(true) + } finally { + await browser.close() + } + } finally { + await scaffold.close() + } +}) + +/** Sample the CSS fade at its midpoint without depending on the host's frame rate. */ +async function assertMaskFade(frame: Locator, from: number, to: number): Promise { + const sample = await frame.locator('[data-sidebar-mask]').evaluate((el) => { + const fade = el.getAnimations().find(animation => animation instanceof CSSTransition && animation.transitionProperty === 'opacity') + if (fade === undefined || !(fade.effect instanceof KeyframeEffect)) throw new Error('Drawer mask opacity transition is missing') + fade.pause() + fade.currentTime = Number(fade.effect.getComputedTiming().duration) / 2 + return { + endpoints: fade.effect.getKeyframes().map(keyframe => Number(keyframe.opacity)), + opacity: Number(getComputedStyle(el).opacity), + } + }) + expect(sample.endpoints).toEqual([from, to]) + expect(sample.opacity).toBeGreaterThan(0) + expect(sample.opacity).toBeLessThan(1) + await frame.evaluate((el) => { + for (const animation of el.getAnimations({ subtree: true })) { + if (animation instanceof CSSTransition) animation.finish() + } + el.style.removeProperty('--ds-transition-duration-slow') + }) +} + +/** Sample both width transitions on the same clock to detect detached sidebar controls. */ +async function assertFoldResize(toggle: Locator, from: number, to: number): Promise { + await expect.poll(() => toggle.evaluate((button, target) => { + const frame = button.closest('[data-animating]') + return frame !== null && parseFloat(frame.style.gridTemplateColumns) === target && frame.getAnimations().some(animation => + animation instanceof CSSTransition && animation.transitionProperty === 'grid-template-columns') + }, to)).toBe(true) + const sample = await toggle.evaluate((button) => { + const frame = button.closest('[data-animating]')! + const transitions = frame.getAnimations({ subtree: true }).filter((animation): animation is CSSTransition => + animation instanceof CSSTransition && ['grid-template-columns', 'width'].includes(animation.transitionProperty)) + const grid = transitions.find(animation => animation.transitionProperty === 'grid-template-columns') + if (grid === undefined) throw new Error('Fold width transition is missing') + const at = (progress: number) => { + for (const transition of transitions) { + transition.pause() + transition.currentTime = Number(transition.effect!.getComputedTiming().duration) * progress + } + const width = parseFloat(getComputedStyle(frame).gridTemplateColumns) + return { width, gap: frame.getBoundingClientRect().left + width - button.getBoundingClientRect().right } + } + const start = at(0) + const middle = at(0.5) + const end = at(1) + for (const transition of transitions) transition.finish() + frame.style.removeProperty('--ds-transition-duration-slow') + return { endpoints: [start.width, end.width], start, middle } + }) + expect(sample.endpoints).toEqual([from, to]) + expect(sample.middle.width).toBeGreaterThan(Math.min(from, to)) + expect(sample.middle.width).toBeLessThan(Math.max(from, to)) + expect(sample.middle.gap).toBeCloseTo(sample.start.gap, 1) +} + +it('overlays the conversation and closes after navigation without losing its sidebar content', async () => { + const scaffold = await launchWebScaffold({ + extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)), + }) + try { + const workspace = await scaffold.ctx.workspaceRegistry.create(scaffold.workspaceCwd) + const seed = await readFile(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url), 'utf8') + const sessionId = await seedSession(scaffold, seed, 'session-c4421b66-252e-44c0-80ed-6e320abeba25') + await workspace.attachSession(sessionId) + await scaffold.ctx.sessionController.rename({ sessionId, title: 'Drawer navigation' }) + const browser = await chromium.launch() + try { + const page = await browser.newPage({ viewport: { width: 390, height: 844 }, hasTouch: true, locale: 'en-US' }) + const console = watchConsole(page) + await page.goto(scaffold.authenticatedUrl) + const opener = page.getByRole('button', { name: 'Open sidebar', exact: true }) + await opener.waitFor() + const frame = page.locator('[data-sidebar-drawer]') + expect(await page.locator('[data-shell-leading]').getByRole('button').count()).toBe(1) + expect(await frame.evaluate(el => getComputedStyle(el).getPropertyValue('--dsh-frame-leading-clearance').trim())) + .toBe('48px') + await expect.poll(() => frame.getAttribute('data-sidebar-collapsed')).toBe('true') + const sidebar = page.locator('[data-slot="sidebar"]') + const original = await sidebar.elementHandle() + const main = page.locator('[data-slot="main"]').locator('..') + const closed = await main.boundingBox() + // A long test-owned duration keeps the transition available until its + // animation clock is sampled and finished explicitly. + await frame.evaluate((el) => { el.style.setProperty('--ds-transition-duration-slow', '100s') }) + await opener.tap() + const drawer = page.getByRole('dialog', { name: 'Navigation', exact: true }) + await drawer.waitFor() + await assertMaskFade(frame, 0, 1) + await expect.poll(() => main.boundingBox()).toEqual(closed) + expect(closed?.width).toBe(390) + expect(await main.evaluate(el => el.closest('[inert]') !== null)).toBe(true) + expect(await page.locator('[data-side="sidebar"]').count()).toBe(0) + const workspaceRow = drawer.locator('[data-row-key^="workspace:"]').first() + const workspaceButtons = workspaceRow.getByRole('button', { includeHidden: true }) + expect(await workspaceRow.getAttribute('aria-expanded')).toBe('true') + expect(await workspaceButtons.count()).toBe(2) + for (const button of await workspaceButtons.all()) await button.waitFor() + await workspaceRow.tap() + expect(await workspaceRow.getAttribute('aria-expanded')).toBe('false') + for (const button of await workspaceButtons.all()) expect(await button.isVisible()).toBe(false) + await workspaceRow.tap() + for (const button of await workspaceButtons.all()) await button.waitFor() + await drawer.getByRole('button', { name: 'Search sessions', exact: true }).tap() + const search = drawer.getByPlaceholder('Search session names') + expect(await search.evaluate(el => getComputedStyle(el).fontSize)).toBe('16px') + await drawer.getByRole('button', { name: 'Clear search', exact: true }).tap() + await frame.evaluate((el) => { el.style.setProperty('--ds-transition-duration-slow', '100s') }) + await page.getByRole('treeitem').filter({ has: page.getByText('Drawer navigation', { exact: true }) }).tap() + await assertMaskFade(frame, 1, 0) + await drawer.waitFor({ state: 'hidden' }) + await page.getByRole('navigation', { name: 'Session hierarchy' }).getByText('Drawer navigation', { exact: true }).waitFor() + const chatTab = page.getByRole('tab', { name: 'Chat', exact: true }) + const tabInset = () => chatTab.evaluate((tab) => { + const label = document.createRange() + label.selectNodeContents(tab) + return label.getBoundingClientRect().left - tab.closest('header')!.getBoundingClientRect().left + }) + expect(await tabInset()).toBeCloseTo(20, 1) + // Restore CSS-probe markers in the same synchronous task, before rendering. + const desktopInsets = await chatTab.evaluate((tab) => { + const root = document.documentElement + try { + return ['darwin', 'win32'].map((platform) => { + root.dataset.platform = platform + const label = document.createRange() + label.selectNodeContents(tab) + return label.getBoundingClientRect().left - tab.closest('header')!.getBoundingClientRect().left + }) + } finally { + delete root.dataset.platform + } + }) + for (const inset of desktopInsets) expect(inset).toBeCloseTo(28, 1) + await page.setViewportSize({ width: 768, height: 844 }) + await frame.waitFor({ state: 'detached' }) + expect(await tabInset()).toBeCloseTo(28, 1) + await page.setViewportSize({ width: 390, height: 844 }) + await frame.waitFor() + await opener.click() + const selectedRow = drawer.getByRole('treeitem', { selected: true }) + const more = selectedRow.getByRole('button', { name: 'Session actions for Drawer navigation', exact: true }) + await more.waitFor() + expect(await selectedRow.getByRole('button', { name: 'Archive session', exact: true }).isVisible()).toBe(false) + expect(await selectedRow.getByRole('button', { name: 'Pin session', exact: true }).isVisible()).toBe(false) + expect(await selectedRow.getByRole('button').count()).toBe(1) + await more.tap() + const menu = page.getByRole('menu') + await menu.getByRole('menuitem', { name: 'Archive session', exact: true }).waitFor() + await menu.getByRole('menuitem', { name: 'Pin session', exact: true }).tap() + await menu.waitFor({ state: 'hidden' }) + const unpin = selectedRow.getByRole('button', { name: 'Unpin session', exact: true }) + await expect.poll(() => selectedRow.getByLabel('Pinned', { exact: true }).count()).toBe(1) + expect(await unpin.isVisible()).toBe(true) + const timestamp = selectedRow.getByText(/^(?:now|\d+min)$/, { exact: true }) + expect(await timestamp.count()).toBe(1) + expect(await timestamp.textContent()).toMatch(/^(?:now|\d+min)$/) + expect(await timestamp.isVisible()).toBe(false) + const unpinBox = (await unpin.boundingBox())! + const moreBox = (await more.boundingBox())! + const folderMoreBox = (await workspaceButtons.nth(0).boundingBox())! + const folderNewBox = (await workspaceButtons.nth(1).boundingBox())! + expect(moreBox.x - unpinBox.x).toBeCloseTo(folderNewBox.x - folderMoreBox.x, 1) + await unpin.tap() + await expect.poll(() => selectedRow.getByLabel('Pinned', { exact: true }).count()).toBe(0) + expect(await drawer.isVisible()).toBe(true) + await more.tap() + await menu.getByRole('menuitem', { name: 'Pin session', exact: true }).tap() + await menu.waitFor({ state: 'hidden' }) + await unpin.waitFor() + await more.tap() + await menu.getByRole('menuitem', { name: 'Unpin session', exact: true }).tap() + await menu.waitFor({ state: 'hidden' }) + await expect.poll(() => selectedRow.getByLabel('Pinned', { exact: true }).count()).toBe(0) + await drawer.getByRole('button', { name: 'Plugins', exact: true }).tap() + await drawer.waitFor({ state: 'hidden' }) + const pluginPanel = page.locator('[data-plugin-panel]') + await pluginPanel.waitFor() + for (const width of [390, 767, 768]) { + await page.setViewportSize({ width, height: 844 }) + expect(await pluginPanel.evaluate(el => Number.parseFloat(getComputedStyle(el).paddingLeft))) + .toBeCloseTo(width < 768 ? 20 : width * 0.04, 1) + } + await page.setViewportSize({ width: 390, height: 844 }) + await frame.waitFor() + await opener.click() + await drawer.getByRole('button', { name: 'Automation tasks', exact: true }).tap() + await drawer.waitFor({ state: 'hidden' }) + const tasksContent = page.getByTestId('task-manager-page').locator('[class*="pageContent"]') + await tasksContent.waitFor() + for (const width of [390, 767, 768]) { + await page.setViewportSize({ width, height: 844 }) + expect(await tasksContent.evaluate(el => Number.parseFloat(getComputedStyle(el).paddingLeft))) + .toBeCloseTo(width < 768 ? 20 : width * 0.04, 1) + } + await page.setViewportSize({ width: 390, height: 844 }) + await frame.waitFor() + await opener.click() + await drawer.waitFor() + await page.touchscreen.tap(370, 420) + await drawer.waitFor({ state: 'hidden' }) + await opener.click() + await page.keyboard.press('Escape') + await drawer.waitFor({ state: 'hidden' }) + expect(await opener.evaluate(el => el === document.activeElement)).toBe(true) + expect(await sidebar.evaluate((el, previous) => el === previous, original)).toBe(true) + await original?.dispose() + + await page.setViewportSize({ width: 768, height: 844 }) + await expect.poll(() => frame.count()).toBe(0) + await expect.poll(async () => Math.round((await main.boundingBox())!.width)).toBe(712) + await page.setViewportSize({ width: 767, height: 844 }) + await expect.poll(() => frame.count()).toBe(1) + + await page.emulateMedia({ reducedMotion: 'reduce' }) + await opener.tap() + const mask = frame.locator('[data-sidebar-mask]') + expect(await mask.evaluate(el => getComputedStyle(el).opacity)).toBe('1') + expect(await mask.evaluate(el => el.getAnimations().length)).toBe(0) + await drawer.getByRole('button', { name: 'Collapse sidebar', exact: true }).tap() + expect(await mask.evaluate(el => getComputedStyle(el).opacity)).toBe('0') + await page.emulateMedia({ reducedMotion: 'no-preference' }) + await expect.poll(async () => Math.round((await main.boundingBox())!.width)).toBe(767) + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(767) + await page.evaluate(() => { document.documentElement.dataset.platform = 'darwin' }) + await expect.poll(() => main.evaluate(el => getComputedStyle(el).paddingTop)).toBe('0px') + await page.evaluate(() => { delete document.documentElement.dataset.platform }) + await expect.poll(() => main.evaluate(el => getComputedStyle(el).paddingTop)).toBe('48px') + + await page.setViewportSize({ width: 390, height: 844 }) + await drawer.waitFor({ state: 'hidden' }) + await opener.tap() + await drawer.getByRole('button', { name: 'New session', exact: true }).last().tap() + await drawer.waitFor({ state: 'hidden' }) + const home = page.locator('[data-phase="hero"]') + await home.waitFor() + const editor = home.locator('[contenteditable="true"]') + await editor.tap() + const homeEditorBox = await editor.boundingBox() + // Chromium cannot raise a phone keyboard; simulate document panning + // before the visual viewport publishes the matching offset. + const restoreViewport = await page.evaluateHandle(() => { + const viewport = window.visualViewport! + const scrollY = Object.getOwnPropertyDescriptor(window, 'scrollY')! + Object.defineProperty(window, 'scrollY', { configurable: true, value: 80 }) + Object.defineProperties(viewport, { + height: { configurable: true, value: 360 }, + offsetTop: { configurable: true, value: 0 }, + }) + viewport.dispatchEvent(new Event('resize')) + viewport.dispatchEvent(new Event('scroll')) + window.dispatchEvent(new Event('scroll')) + return () => { + Object.defineProperty(window, 'scrollY', scrollY) + Reflect.deleteProperty(viewport, 'height') + Reflect.deleteProperty(viewport, 'offsetTop') + window.dispatchEvent(new Event('scroll')) + viewport.dispatchEvent(new Event('resize')) + } + }) + const root = page.locator('#root') + await expect.poll(() => root.boundingBox()).toEqual({ x: 0, y: 0, width: 390, height: 844 }) + expect(await editor.boundingBox()).toEqual(homeEditorBox) + await page.evaluate(() => { + Object.defineProperty(window.visualViewport!, 'offsetTop', { configurable: true, value: 80 }) + window.visualViewport!.dispatchEvent(new Event('scroll')) + }) + await expect.poll(() => root.boundingBox()).toEqual({ x: 0, y: 0, width: 390, height: 844 }) + await expect.poll(async () => (await main.boundingBox())?.height).toBe(844) + await opener.tap() + await expect.poll(async () => (await drawer.boundingBox())?.height).toBe(844) + const searchWithKeyboard = drawer.getByPlaceholder('Search session names') + await drawer.getByRole('button', { name: 'Search sessions', exact: true }).tap() + await searchWithKeyboard.tap() + await searchWithKeyboard.fill('retained search') + await drawer.getByRole('button', { name: 'Account menu', exact: true }).tap() + await page.getByRole('menuitem', { name: 'Settings', exact: true }).tap() + const settings = page.getByRole('dialog', { name: 'Settings', exact: true }) + await settings.waitFor() + const settingsBox = (await settings.boundingBox())! + expect(settingsBox.x).toBeGreaterThanOrEqual(0) + expect(settingsBox.y).toBeGreaterThanOrEqual(0) + expect(settingsBox.x + settingsBox.width).toBeLessThanOrEqual(390) + expect(settingsBox.y + settingsBox.height).toBeLessThanOrEqual(844) + const closeSettings = settings.getByRole('button', { name: 'Close', exact: true }) + const closeBox = (await closeSettings.boundingBox())! + expect(closeBox.y).toBeGreaterThanOrEqual(0) + expect(closeBox.y + closeBox.height).toBeLessThanOrEqual(844) + await closeSettings.tap() + await settings.waitFor({ state: 'hidden' }) + await expect.poll(() => searchWithKeyboard.inputValue()).toBe('retained search') + await restoreViewport.evaluate((restore) => { restore() }) + await restoreViewport.dispose() + await expect.poll(() => root.boundingBox()).toEqual({ x: 0, y: 0, width: 390, height: 844 }) + await expect.poll(async () => (await drawer.boundingBox())?.height).toBe(844) + await frame.evaluate((el) => { el.scrollLeft = 10 }) + expect(await frame.evaluate(el => el.scrollLeft)).toBe(0) + await expect.poll(() => mask.boundingBox()).toEqual({ x: 0, y: 0, width: 390, height: 844 }) + await expect.poll(() => drawer.evaluate((el) => { + const frame = el.closest('[data-sidebar-drawer]')! + let column = el + while (column.parentElement !== frame) column = column.parentElement! + const { y, height } = column.getBoundingClientRect() + return { y, height } + })).toEqual({ y: 0, height: 844 }) + await expect.poll(async () => (await drawer.boundingBox())?.height).toBe(844) + await page.setViewportSize({ width: 844, height: 390 }) + await expect.poll(async () => (await root.boundingBox())?.height).toBe(390) + await expect.poll(() => frame.count()).toBe(0) + await page.setViewportSize({ width: 390, height: 844 }) + await expect.poll(async () => (await root.boundingBox())?.height).toBe(844) + await expect.poll(() => frame.count()).toBe(1) + + const posture = await page.context().newCDPSession(page) + try { + await page.setViewportSize({ width: 951, height: 669 }) + await expect.poll(() => frame.count()).toBe(0) + await posture.send('Emulation.setDevicePostureOverride', { posture: { type: 'folded' } }) + await opener.tap() + await expect.poll(() => main.boundingBox()).toEqual({ x: 475.5, y: 0, width: 475.5, height: 669 }) + expect(await page.locator('[data-side="sidebar"]').count()).toBe(0) + const foldSearch = page.getByPlaceholder('Search session names') + if (!(await foldSearch.isVisible())) await page.getByRole('button', { name: 'Search sessions', exact: true }).tap() + await foldSearch.fill('fold search') + const wideFrame = page.locator('[data-rightbar-collapsed]') + const collapseSidebar = page.getByRole('button', { name: 'Collapse sidebar', exact: true }) + await wideFrame.evaluate((el) => { el.style.setProperty('--ds-transition-duration-slow', '100s') }) + await posture.send('Emulation.clearDevicePostureOverride') + await assertFoldResize(collapseSidebar, 475.5, 280) + await expect.poll(() => main.boundingBox()).toEqual({ x: 280, y: 0, width: 671, height: 669 }) + expect(await page.locator('[data-side="sidebar"]').count()).toBe(1) + expect(await foldSearch.evaluate(el => el === document.activeElement)).toBe(true) + expect(await foldSearch.inputValue()).toBe('fold search') + await wideFrame.evaluate((el) => { el.style.setProperty('--ds-transition-duration-slow', '100s') }) + await posture.send('Emulation.setDevicePostureOverride', { posture: { type: 'folded' } }) + await assertFoldResize(collapseSidebar, 280, 475.5) + await expect.poll(async () => (await main.boundingBox())?.x).toBe(475.5) + expect(await foldSearch.evaluate(el => el === document.activeElement)).toBe(true) + await page.emulateMedia({ reducedMotion: 'reduce' }) + await posture.send('Emulation.clearDevicePostureOverride') + await expect.poll(async () => (await main.boundingBox())?.x).toBe(280) + // Scroll-driven text fades remain registered even with reduced motion. + expect(await wideFrame.evaluate(el => el.getAnimations({ subtree: true }) + .filter(animation => animation.timeline === document.timeline).length)).toBe(0) + await posture.send('Emulation.setDevicePostureOverride', { posture: { type: 'folded' } }) + await expect.poll(async () => (await main.boundingBox())?.x).toBe(475.5) + await page.emulateMedia({ reducedMotion: 'no-preference' }) + await page.setViewportSize({ width: 669, height: 951 }) + await expect.poll(() => frame.getAttribute('data-sidebar-collapsed')).toBe('true') + await drawer.waitFor({ state: 'hidden' }) + await opener.tap() + await expect.poll(async () => (await drawer.boundingBox())?.width).toBe(280) + expect(await main.boundingBox()).toEqual({ x: 0, y: 0, width: 669, height: 951 }) + } finally { + await posture.detach() + } + + const pointerPage = await browser.newPage({ viewport: { width: 390, height: 844 }, locale: 'en-US' }) + try { + await pointerPage.goto(scaffold.authenticatedUrl) + await pointerPage.getByRole('button', { name: 'Open sidebar', exact: true }).click() + expect(await pointerPage.getByPlaceholder('Search session names').evaluate(el => getComputedStyle(el).fontSize)).toBe('13px') + const pointerWorkspace = pointerPage.locator('[data-row-key^="workspace:"]').first() + const pointerWorkspaceButtons = pointerWorkspace.getByRole('button', { includeHidden: true }) + await pointerWorkspace.click() + expect(await pointerWorkspace.getAttribute('aria-expanded')).toBe('false') + const collapseSidebar = pointerPage.getByRole('button', { name: 'Collapse sidebar', exact: true }) + await collapseSidebar.focus() + await collapseSidebar.hover() + for (const button of await pointerWorkspaceButtons.all()) expect(await button.isVisible()).toBe(false) + await pointerWorkspace.hover() + for (const button of await pointerWorkspaceButtons.all()) await button.waitFor() + await pointerWorkspace.click() + for (const label of ['Collapse sidebar', 'View options']) { + await pointerPage.getByRole('button', { name: label, exact: true }).hover() + const tooltip = pointerPage.getByRole('tooltip').filter({ hasText: label }) + await tooltip.waitFor() + expect(await tooltip.evaluate(el => el.closest('[data-slot="sidebar"]'))).toBeNull() + const box = (await tooltip.boundingBox())! + expect(box.x).toBeGreaterThanOrEqual(12) + expect(box.x + box.width).toBeLessThanOrEqual(378) + } + const pointerRow = pointerPage.getByRole('treeitem').filter({ has: pointerPage.getByText('Drawer navigation', { exact: true }) }) + await pointerRow.click() + await pointerPage.getByRole('dialog', { name: 'Navigation', exact: true }).waitFor({ state: 'hidden' }) + await pointerPage.getByRole('button', { name: 'Open sidebar', exact: true }).click() + await pointerPage.getByRole('button', { name: 'Collapse sidebar', exact: true }).hover() + await expect.poll(() => pointerRow.getByRole('button', { name: 'Session actions for Drawer navigation', exact: true }).isVisible()).toBe(false) + await pointerRow.hover() + for (const label of ['Session actions for Drawer navigation', 'Archive session', 'Pin session']) { + await pointerRow.getByRole('button', { name: label, exact: true }).waitFor() + } + await pointerPage.getByRole('button', { name: 'Collapse sidebar', exact: true }).click() + const pointerDrawer = pointerPage.getByRole('dialog', { name: 'Navigation', exact: true }) + await pointerDrawer.waitFor({ state: 'hidden' }) + await pointerPage.setViewportSize({ width: 767, height: 844 }) + await pointerPage.getByRole('button', { name: 'Open sidebar', exact: true }).press('Enter') + await pointerDrawer.waitFor() + expect(await pointerDrawer.evaluate(el => el.contains(document.activeElement))).toBe(true) + await pointerPage.getByRole('button', { name: 'Search sessions', exact: true }).click() + await expect.poll(() => pointerPage.getByPlaceholder('Search session names') + .evaluate(el => el === document.activeElement)).toBe(true) + await pointerPage.setViewportSize({ width: 768, height: 844 }) + await pointerPage.locator('[data-sidebar-drawer]').waitFor({ state: 'detached' }) + const pointerSidebar = pointerPage.locator('[data-slot="sidebar"]') + await expect.poll(() => pointerSidebar.getByRole('button', { name: 'New session', exact: true }).count()).toBe(1) + const railToggle = pointerSidebar.getByRole('button', { name: 'Open sidebar', exact: true }) + expect(await railToggle.evaluate(el => el === document.activeElement)).toBe(true) + } finally { + await pointerPage.close() + } + expect(console.pageErrors).toEqual([]) + expect(console.warnings).toEqual([]) + const result = [ + '390px: closed sidebar has no rail; main width 390px', + 'open: main geometry retained; background inert; no resize handle', + 'close: Session navigation, Plugins navigation, mask and Escape', + 'retained: sidebar DOM and opener focus', + '768px: 56px rail; main width 712px; drawer focus transfers to the rail toggle', + 'breakpoint motion: collapsed rail hides immediately and returns compact without crossfades; expanded sidebar and explicit toggles retain their slides', + '767px: hidden drawer; main width 767px; no page overflow', + 'global panels: browser top inset 48px; macOS keeps its existing clearance', + 'tooltips: sidebar and workspace controls escape the drawer and fit the viewport', + 'search input: 16px on touch; 13px with a fine pointer', + 'simulated keyboard: homepage and drawer retain 844px layout height without following the 360px visual viewport; Settings stays within the viewport', + 'keyboard close: layout height stays 844px; sidebar search retained; Settings close remains reachable', + 'shell coverage: drawer backing and mask cover 844px without synthetic safe-area insets', + 'rotation: 844x390 then 390x844 restores shell height and sidebar breakpoint', + 'folded landscape: 951px frame splits at 475.5px without a resize handle; unfolding restores 280px and search focus; portrait retains the drawer', + 'fold transitions: sidebar controls follow the divider in both directions; reduced motion changes width without animation', + 'drawer mask: opacity transitions from 0 to 1 on open and 1 to 0 on close', + 'reduced motion: drawer and mask settle without transitions', + 'Web header: only the sidebar opener remains, with 48px leading clearance', + 'below 768px: plugin and automation pages use 20px insets; Web view tabs align at 20px, while Desktop and wider Web tabs retain 28px', + 'touch selected Session: More remains visible; update time is hidden; pinned rows offer Unpin before More with Workspace action spacing', + 'touch Workspace: actions appear only while expanded, including after folding the focused row', + 'fine pointer: folded Workspace actions remain available on hover', + 'fine pointer: selected Session actions stay hidden until hover, including at 390px', + ].join('\n') + await compareOrRefreshGolden(fileURLToPath(new URL('./expected/sidebar-drawer.expected.md', import.meta.url)), result, webSnapshotMode()) + } finally { + await browser.close() + } + } finally { + await scaffold.close() + } +}) + +it('dismisses retained sidebar menus at the drawer breakpoint and preserves their rows', async () => { + const scaffold = await launchWebScaffold({ + extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)), + }) + try { + const workspace = await scaffold.ctx.workspaceRegistry.create(scaffold.workspaceCwd) + const seed = await readFile(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url), 'utf8') + const sessionId = await seedSession(scaffold, seed, 'session-c4421b66-252e-44c0-80ed-6e320abeba25') + await workspace.attachSession(sessionId) + await scaffold.ctx.sessionController.rename({ sessionId, title: 'Menu regression' }) + const browser = await chromium.launch() + try { + const page = await browser.newPage({ viewport: { width: 1024, height: 844 }, locale: 'en-US' }) + await page.goto(scaffold.authenticatedUrl) + const row = page.locator(`[data-row-key="session:${sessionId}"]`) + await row.waitFor() + const original = await row.elementHandle() + for (const label of ['View options', 'Session actions for Menu regression']) { + await page.setViewportSize({ width: 1024, height: 844 }) + await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).waitFor() + if (label.startsWith('Session')) await row.hover() + await page.getByRole('button', { name: label, exact: true }).click() + await page.getByRole('menu').waitFor() + await page.setViewportSize({ width: 767, height: 844 }) + await page.getByRole('menu').waitFor({ state: 'hidden' }) + const opener = page.getByRole('button', { name: 'Open sidebar', exact: true }) + await expect.poll(() => opener.evaluate(element => element === document.activeElement)).toBe(true) + await opener.click() + await row.waitFor() + expect(await row.evaluate((element, before) => element === before, original)).toBe(true) + } + await original?.dispose() + await compareOrRefreshGolden(fileURLToPath(new URL('./expected/sidebar-menu-dismissal.expected.md', import.meta.url)), [ + 'View options and Session menus close when the sidebar becomes an inert drawer.', + 'Focus returns to the visible sidebar opener; retained Session rows keep their DOM identity.', + ].join('\n'), webSnapshotMode()) + } finally { await browser.close() } + } finally { await scaffold.close() } +}) + +it('keeps drawer shortcuts and hit targets above fullscreen and floating right panels', async () => { + const scaffold = await launchWebScaffold({ + extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)), + }) + try { + const workspace = await scaffold.ctx.workspaceRegistry.create(scaffold.workspaceCwd) + const seed = await readFile(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url), 'utf8') + const sessionId = await seedSession(scaffold, seed, 'session-c4421b66-252e-44c0-80ed-6e320abeba25') + await workspace.attachSession(sessionId) + await scaffold.ctx.sessionController.rename({ sessionId, title: 'Drawer overlay checks' }) + const browser = await chromium.launch() + try { + const page = await browser.newPage({ viewport: { width: 1280, height: 844 }, locale: 'en-US' }) + await page.addInitScript(() => { Object.defineProperty(navigator, 'platform', { value: 'MacIntel' }) }) + await page.goto(scaffold.authenticatedUrl) + const row = page.getByRole('treeitem').filter({ has: page.getByText('Drawer overlay checks', { exact: true }) }) + await row.click() + await page.locator('[data-sidebar-right-expand]').click() + await page.locator('[data-sidebar-right-guide-entry="files"]').click() + const panel = page.locator('[data-sidebar-right-open]') + const primary = await page.evaluate(() => /mac/i.test(navigator.platform) ? 'Meta' : 'Control') + const toggle = `${primary}+Alt+b` + await row.hover() + const card = page.getByRole('button', { name: 'Copy: Drawer overlay checks', exact: true }) + await card.waitFor() + await page.setViewportSize({ width: 390, height: 844 }) + await card.waitFor({ state: 'hidden' }) + expect(await row.count()).toBe(1) + const drawer = page.getByRole('dialog', { name: 'Navigation', exact: true }) + await expect.poll(() => panel.getAttribute('data-sidebar-right-panel')).toBe('fullscreen') + const assertAboveRightPanel = async (coveredPoint?: { x: number; y: number }) => { + await page.locator('[data-sidebar-drawer][data-sidebar-collapsed]').waitFor() + await page.keyboard.press(toggle) + await drawer.waitFor() + const collapse = drawer.getByRole('button', { name: 'Collapse sidebar', exact: true }) + await expect.poll(() => collapse.evaluate(el => document.activeElement === el)).toBe(true) + await expect.poll(() => drawer.evaluate((el) => { + const box = el.getBoundingClientRect() + return el.contains(document.elementFromPoint(box.left + 140, 30)) + })).toBe(true) + expect(await page.evaluate(() => document.elementFromPoint(380, 420)?.hasAttribute('data-sidebar-mask'))).toBe(true) + if (coveredPoint !== undefined) { + expect(await drawer.evaluate((el, point) => { + const hit = document.elementFromPoint(point.x, point.y) + return el.contains(hit) || hit?.hasAttribute('data-sidebar-mask') + }, coveredPoint)).toBe(true) + } + await page.keyboard.press(`${primary}+Alt+k`) + await expect.poll(() => drawer.getByPlaceholder('Search session names').evaluate(el => document.activeElement === el)).toBe(true) + await page.keyboard.press(toggle) + await drawer.waitFor({ state: 'hidden' }) + expect(await panel.evaluate(el => el.closest('[inert]') === null)).toBe(true) + } + await page.keyboard.press(`${primary}+Alt+k`) + await drawer.waitFor() + await expect.poll(() => drawer.getByPlaceholder('Search session names') + .evaluate(el => el === document.activeElement)).toBe(true) + await page.keyboard.press(toggle) + await drawer.waitFor({ state: 'hidden' }) + await assertAboveRightPanel() + await page.setViewportSize({ width: 1280, height: 844 }) + await expect.poll(() => panel.getAttribute('data-sidebar-right-panel')).toBe('push') + const tab = panel.locator('[data-dockkit-tab]').filter({ hasText: 'Files' }) + const bounds = (await tab.boundingBox())! + await page.mouse.move(bounds.x + 6, bounds.y + bounds.height / 2) + await page.mouse.down() + try { + await page.mouse.move(190, 400, { steps: 8 }) + } finally { await page.mouse.up() } + const floating = page.locator('[data-dockkit-float]') + await floating.waitFor() + await page.setViewportSize({ width: 390, height: 844 }) + const box = (await floating.boundingBox())! + expect(box.x).toBeLessThan(390) + expect(box.x + box.width).toBeGreaterThan(0) + const point = { x: (Math.max(0, box.x) + Math.min(390, box.x + box.width)) / 2, y: box.y + 40 } + await expect.poll(() => floating.evaluate((el, at) => el.contains(document.elementFromPoint(at.x, at.y)), point)).toBe(true) + await assertAboveRightPanel(point) + } finally { + await browser.close() + } + } finally { + await scaffold.close() + } +}) diff --git a/apps/web/tests/sidebar-scrollbar.e2e.ts b/apps/web/tests/sidebar-scrollbar.e2e.ts index f15a893682..afcedbf0ca 100644 --- a/apps/web/tests/sidebar-scrollbar.e2e.ts +++ b/apps/web/tests/sidebar-scrollbar.e2e.ts @@ -37,6 +37,7 @@ interface ListMetrics { band: number scrollbarEdgeOffset: number rowEdgeInset: number + rowPaintInset: number clientRight: number borderRight: number timeRight: number @@ -93,8 +94,8 @@ function measureList(page: Page): Promise { const pseudoWidth = getComputedStyle(list, '::-webkit-scrollbar').width const barWidth = pseudoWidth === 'auto' ? 15 : Number.parseFloat(pseudoWidth) const listRect = list.getBoundingClientRect() - const sidebarEdge = list.parentElement?.getBoundingClientRect().right - if (sidebarEdge === undefined) throw new Error('sidebar session list has no layout parent') + const listParentRight = list.parentElement?.getBoundingClientRect().right + if (listParentRight === undefined) throw new Error('sidebar session list has no layout parent') return { gutter: style.scrollbarGutter, width: pseudoWidth, @@ -106,8 +107,9 @@ function measureList(page: Page): Promise { hoverToken: resolve('--dsh-scrollbar-thumb-hover'), overflows: list.scrollHeight > list.clientHeight, band: listRect.width - list.clientWidth, - scrollbarEdgeOffset: sidebarEdge - listRect.right, - rowEdgeInset: sidebarEdge - row.getBoundingClientRect().right, + scrollbarEdgeOffset: listParentRight - listRect.right, + rowEdgeInset: listParentRight - row.getBoundingClientRect().right, + rowPaintInset: listParentRight - row.getBoundingClientRect().right + Number.parseFloat(getComputedStyle(row).borderRightWidth), clientRight: listRect.left + list.clientWidth, borderRight: listRect.right, timeRight: time.getBoundingClientRect().right, @@ -129,17 +131,18 @@ function measureList(page: Page): Promise { * @param page - the page under test. * @returns the list overflow state and first row's trailing inset. */ -function measureRowInset(page: Page): Promise> { +function measureRowInset(page: Page): Promise> { return page.evaluate(() => { const list = document.querySelector('[role="tree"][aria-label="Sessions"]') if (list === null) throw new Error('sidebar session list not in the DOM') const row = list.querySelector('[role="treeitem"]') if (row === null) throw new Error('no row in the sidebar list') - const sidebarEdge = list.parentElement?.getBoundingClientRect().right - if (sidebarEdge === undefined) throw new Error('sidebar session list has no layout parent') + const listParentRight = list.parentElement?.getBoundingClientRect().right + if (listParentRight === undefined) throw new Error('sidebar session list has no layout parent') return { overflows: list.scrollHeight > list.clientHeight, - rowEdgeInset: sidebarEdge - row.getBoundingClientRect().right, + rowEdgeInset: listParentRight - row.getBoundingClientRect().right, + rowPaintInset: listParentRight - row.getBoundingClientRect().right + Number.parseFloat(getComputedStyle(row).borderRightWidth), } }) } @@ -192,8 +195,9 @@ function renderGeometry(light: PaletteMetrics, dark: PaletteMetrics): string { `- --dsh-scrollbar-thumb-hover, pointer over the list: ${metrics.hoverToken}`, `- list overflows: ${String(metrics.overflows)}`, `- reserved band: ${String(metrics.band)}px`, - `- scrollbar inset from the sidebar edge: ${String(metrics.scrollbarEdgeOffset)}px`, - `- row background inset from the sidebar edge: ${String(metrics.rowEdgeInset)}px`, + `- scrollbar inset from the list parent edge: ${String(metrics.scrollbarEdgeOffset)}px`, + `- row hitbox inset from the list parent edge: ${String(metrics.rowEdgeInset)}px`, + `- row background inset from the list parent edge: ${String(metrics.rowPaintInset)}px`, `- relative time covered by the bar: ${String(metrics.timeCoveredBy)}px`, `- relative time ends inside the content area: ${String(metrics.timeRight <= metrics.clientRight)}`, `- content area ends before the border box: ${String(metrics.clientRight < metrics.borderRight)}`, @@ -321,7 +325,8 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum // Pin presence, not width, because the width is platform-dependent. expect(metrics.band).toBeGreaterThan(0) expect(metrics.scrollbarEdgeOffset).toBe(2) - expect(metrics.rowEdgeInset).toBe(12) + expect(metrics.rowEdgeInset).toBe(8) + expect(metrics.rowPaintInset).toBe(11) // Measure against the bar because overlay scrollbars do not move the client edge. expect(metrics.timeCoveredBy).toBe(0) expect(metrics.timeRight).toBeLessThanOrEqual(metrics.clientRight) @@ -359,12 +364,12 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum it('keeps the row background inset when overflow disappears', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-stable-inset')) - expect(await measureRowInset(page)).toEqual({ overflows: true, rowEdgeInset: 12 }) + expect(await measureRowInset(page)).toEqual({ overflows: true, rowEdgeInset: 8, rowPaintInset: 11 }) const bucket = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..') await bucket.click() try { await expect.poll(async () => (await measureRowInset(page)).overflows, { timeout: 10_000 }).toBe(false) - expect(await measureRowInset(page)).toEqual({ overflows: false, rowEdgeInset: 12 }) + expect(await measureRowInset(page)).toEqual({ overflows: false, rowEdgeInset: 8, rowPaintInset: 11 }) } finally { await expandSeededSessions(page) } diff --git a/apps/web/tests/startup-rpc-budget.e2e.ts b/apps/web/tests/startup-rpc-budget.e2e.ts index f6655ffbf3..80b2630328 100644 --- a/apps/web/tests/startup-rpc-budget.e2e.ts +++ b/apps/web/tests/startup-rpc-budget.e2e.ts @@ -1,6 +1,6 @@ -// Cold boot may issue at most two settings/describe calls regardless of client -// plugin count. No model call or replay fixture is involved. -import type { Browser, Page } from 'playwright' +// Cold boot bounds shared settings reads and the Web Search credential reads +// caused by their publication waves. No model call or replay fixture is involved. +import type { Browser, Page, Request, Route } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { launchWebScaffold, watchConsole, type WebScaffold } from './scaffold.ts' @@ -8,6 +8,8 @@ import { newEnglishPage } from './support.ts' /** One eager read plus one first-connection reset closes the pre-subscription commit window. */ const DESCRIBE_BUDGET = 2 +/** One eager Web Search credential read plus one per settings publication wave. */ +const WEB_SEARCH_CREDENTIAL_BUDGET = 3 let scaffold: WebScaffold let browser: Browser @@ -25,20 +27,58 @@ afterAll(async () => { }) describe('startup RPC budget', () => { - it('keeps cold-boot settings.describe at the mirror count', async () => { + it('keeps settings and Web Search credential reads at their publication counts', async () => { page = await newEnglishPage(browser) watchConsole(page) const calls: string[] = [] + const credentials: string[][] = [] + const pending = new Set() + const webSearchRequests = new Set() + const credentialStarted = Promise.withResolvers() + let settingsFinished = 0 + let webSearchFinished = 0 + // Both settings answers must reach the card after it subscribes, regardless + // of plugin load order. The eager credential request proves that subscription. + const holdSettings = async (route: Route): Promise => { + await credentialStarted.promise + await route.continue() + } + await page.route('**/api/settings/describe', holdSettings) page.on('request', (request) => { const url = new URL(request.url()) - if (url.pathname.startsWith('/api/')) calls.push(url.pathname.slice('/api/'.length)) + if (!url.pathname.startsWith('/api/')) return + const method = url.pathname.slice('/api/'.length) + calls.push(method) + pending.add(request) + if (method === 'credentials/describe') { + const body = request.postDataJSON() as { payload: { args: { refs: string[] } } } + const refs = body.payload.args.refs + credentials.push(refs) + // Models requests batch references; the Web Search card asks for this one key. + if (refs.length === 1 && refs[0] === 'DEEPSEEK_API_KEY') { + webSearchRequests.add(request) + credentialStarted.resolve(undefined) + } + } }) - await page.goto(scaffold.authenticatedUrl) - // Boot settles when the workspace picker is interactive; the trailing wait - // absorbs the first-connection reset wave the budget must include. - await page.getByRole('textbox', { name: 'Choose workspace' }).waitFor({ timeout: 30_000 }) - await page.waitForTimeout(3000) - const describeCount = calls.filter(method => method === 'settings/describe').length - expect(describeCount, `startup /api calls:\n${calls.join('\n')}`).toBe(DESCRIBE_BUDGET) + page.on('requestfinished', (request) => { + pending.delete(request) + if (new URL(request.url()).pathname === '/api/settings/describe') settingsFinished++ + if (webSearchRequests.has(request)) webSearchFinished++ + }) + page.on('requestfailed', request => pending.delete(request)) + try { + await page.goto(scaffold.authenticatedUrl) + await page.getByRole('textbox', { name: 'Choose workspace' }).waitFor({ timeout: 30_000 }) + await expect.poll(() => settingsFinished, { timeout: 30_000 }).toBeGreaterThanOrEqual(DESCRIBE_BUDGET) + await expect.poll(() => webSearchFinished, { timeout: 30_000 }).toBeGreaterThanOrEqual(WEB_SEARCH_CREDENTIAL_BUDGET) + await expect.poll(() => pending.size, { timeout: 30_000 }).toBe(0) + const describeCount = calls.filter(method => method === 'settings/describe').length + expect(describeCount, `startup /api calls:\n${calls.join('\n')}`).toBe(DESCRIBE_BUDGET) + expect(webSearchRequests.size, `startup credential references:\n${JSON.stringify(credentials)}`).toBe(WEB_SEARCH_CREDENTIAL_BUDGET) + } finally { + credentialStarted.resolve(undefined) + await page.unrouteAll({ behavior: 'wait' }) + } }) }) diff --git a/apps/web/tests/stats-truncated-tooltip.e2e.ts b/apps/web/tests/stats-truncated-tooltip.e2e.ts index 18e7d5e64c..fc3e25b419 100644 --- a/apps/web/tests/stats-truncated-tooltip.e2e.ts +++ b/apps/web/tests/stats-truncated-tooltip.e2e.ts @@ -92,7 +92,7 @@ describe('web e2e: truncated composer statistics', () => { const cut = () => label.evaluate(element => element.scrollWidth > element.clientWidth) expect(await cut()).toBe(false) - await page.setViewportSize({ width: 390, height: 900 }) + await page.setViewportSize({ width: 334, height: 900 }) await expect.poll(cut).toBe(true) await usagePill.hover() await expect.poll(() => tooltip.textContent()).toBe(USAGE_READING) diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index 953f701fc9..ad741f474d 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -200,6 +200,23 @@ export function probeFreePort(): Promise { }) } +/** + * Open collapsed mobile navigation before using a sidebar action. + * @param page - browser page with the mounted sidebar. + * @param locale - current UI language. + */ +export async function revealNavigationDrawer(page: Page, locale: 'en' | 'zh' | 'ja'): Promise { + const drawer = await page.evaluate(() => window.innerWidth < 768 + && !document.documentElement.hasAttribute('data-windows-titlebar')) + await page.waitForFunction(expected => Boolean(document.querySelector('[data-sidebar-drawer]')) === expected, drawer) + const closed = page.locator('[data-sidebar-drawer][data-sidebar-collapsed]') + if (await closed.count() === 0) return + await page.locator('[data-shell-leading]').getByRole('button', { + name: { en: 'Open sidebar', zh: '打开侧边栏', ja: 'サイドバーを開く' }[locale], exact: true, + }).click() + await closed.waitFor({ state: 'detached' }) +} + /** * Drive the hero's workspace picker through the composed directory dialog * until the live composer unlocks. A fresh world has no Workspace, so the boot @@ -219,6 +236,7 @@ export function probeFreePort(): Promise { */ export async function connectFreshWorkspace(page: Page, root: string, name = 'workspace'): Promise { mkdirSync(join(root, name), { recursive: true }) + await revealNavigationDrawer(page, 'en') await page.getByRole('button', { name: 'Add workspace', exact: true }).click() const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' }) await dialog.waitFor({ timeout: 10_000 }) @@ -245,6 +263,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo */ export async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace', modelAvailable = true): Promise { mkdirSync(join(root, name), { recursive: true }) + await revealNavigationDrawer(page, 'zh') await page.getByRole('button', { name: '添加工作区', exact: true }).click() const dialog = page.getByRole('dialog', { name: '选择工作区目录' }) await dialog.waitFor({ timeout: 10_000 }) @@ -346,6 +365,14 @@ export function conversationContextKey(kind: string, id: string): string { */ export async function openSettings(page: Page, locale: 'en' | 'zh' | 'ja'): Promise { const labels = { en: ['Account menu', 'Settings'], zh: ['账号菜单', '设置'], ja: ['アカウントメニュー', '設定'] } as const - await page.getByRole('button', { name: labels[locale][0], exact: true }).click() + const trigger = page.getByRole('button', { + name: labels[locale][0], exact: true, includeHidden: true, + }) + await trigger.waitFor({ state: 'attached' }) + await revealNavigationDrawer(page, locale) + if (!await trigger.isVisible()) { + await page.getByRole('button', { name: { en: 'Open sidebar', zh: '打开侧边栏', ja: 'サイドバーを開く' }[locale], exact: true }).click() + } + await trigger.click() await page.getByRole('menuitem', { name: labels[locale][1], exact: true }).click() } diff --git a/apps/web/tests/window-drag-coverage.e2e.ts b/apps/web/tests/window-drag-coverage.e2e.ts index cdbb727736..70fd84f7a1 100644 --- a/apps/web/tests/window-drag-coverage.e2e.ts +++ b/apps/web/tests/window-drag-coverage.e2e.ts @@ -107,10 +107,10 @@ async function collectedRegions(page: Page, selector: string): Promise { const top = document.elementFromPoint(x, y) - // A pane's programmatic focus target also contains its window-drag strip. - // The strip owns those pixels; controls inside it still need their own subtraction. - const strip = top?.closest('[data-dockkit-strip][data-window-drag]') - if (element.matches('[data-dockkit-pane][tabindex="-1"]') + // Panes and the navigation dialog contain their own window-drag rows. + // Those rows own their pixels; controls inside still subtract themselves. + const strip = top?.closest('[data-window-drag]') + if (element.matches('[data-dockkit-pane][tabindex="-1"], [data-shortcut-modal="sidebar"][role="dialog"]') && strip != null && element.contains(strip)) return false return top !== null && (top === element || element.contains(top)) }) @@ -231,6 +231,41 @@ describe('web e2e: macOS window drag coverage', () => { return { page, tripwire } } + it('keeps the macOS sidebar toggle stationary as its expanded content starts fading', async () => { + const { page, tripwire } = await desktopPage() + try { + const open = page.getByRole('button', { name: 'Open sidebar', exact: true }) + if (await open.isVisible()) await open.click() + const toggle = page.getByRole('button', { name: 'Collapse sidebar', exact: true }) + await toggle.waitFor() + const positions = await toggle.evaluate(async (button) => { + if (!(button instanceof HTMLButtonElement)) throw new Error('Sidebar toggle is not a button') + const root = button.closest('[class*="topStrip"]')!.parentElement! + const position = () => ({ + left: button.getBoundingClientRect().left - root.getBoundingClientRect().left, + transform: getComputedStyle(button).transform, + }) + const before = position() + // Observe the class change before the 150ms fade can settle under host contention. + const fading = await new Promise>((resolve) => { + const observer = new MutationObserver(() => { + if (![...root.classList].some(name => name.includes('fading'))) return + observer.disconnect() + resolve(position()) + }) + observer.observe(root, { attributes: true, attributeFilter: ['class'] }) + button.click() + }) + return { before, fading } + }) + expect(positions.before.transform).toBe('matrix(1, 0, 0, 1, 4, 0)') + expect(positions.fading).toEqual(positions.before) + expect(tripwire.pageErrors).toEqual([]) + } finally { + await page.close() + } + }) + it.each(['darwin', 'win32'] as const)('%s keeps compact Plugins controls below native chrome in both sidebar states', async (platform) => { const { page, tripwire } = await desktopPage(platform) try { @@ -468,6 +503,32 @@ describe('web e2e: macOS window drag coverage', () => { } }, 120_000) + it('keeps the drawer titlebar draggable without swallowing its close button', async () => { + const { page, tripwire } = await desktopPage() + try { + await page.setViewportSize({ width: 571, height: 696 }) + await page.locator('[data-sidebar-drawer][data-sidebar-collapsed]').waitFor() + await page.locator('[data-shell-leading]').getByRole('button', { name: 'Open sidebar', exact: true }).click() + const drawer = page.getByRole('dialog', { name: 'Navigation', exact: true }) + await drawer.waitFor() + await settled(page) + const toggle = drawer.getByRole('button', { name: 'Collapse sidebar', exact: true }) + const button = (await toggle.boundingBox())! + const seat = await rectOf(page, '[data-shell-leading]') + const regions = await collectedRegions(page, INTERACTIVE_SELECTOR) + const y = button.y + button.height / 2 + expect(isDraggableAt(regions, seat.x + seat.width / 2, y), 'the inactive header controls behind the drawer').toBe(true) + expect(isDraggableAt(regions, (seat.x + seat.width + button.x) / 2, y), 'the gap before the close button').toBe(true) + expect(isDraggableAt(regions, button.x + button.width / 2, y), 'the close button').toBe(false) + expect(swallowedBoxes(regions)).toEqual([]) + await toggle.click() + await drawer.waitFor({ state: 'hidden' }) + expect(tripwire.pageErrors).toEqual([]) + } finally { + await page.close() + } + }) + it('keeps an entry page’s chrome row draggable', async () => { const { page, tripwire } = await desktopPage() try { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index f882829d98..c653301218 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -23,6 +23,7 @@ // this client-registered project must not also hold them (one program // cannot see both sides of the cordis Context merges). "exclude": [ + "tests/sidebar-drawer.e2e.ts", "tests/creation-recommendations.e2e.ts", "tests/plugin-recommendations.e2e.ts", "tests/japanese-plugin-metadata.e2e.ts", @@ -67,6 +68,7 @@ "tests/subagent-fork-schedule.snapshot.ts", "tests/live-interactions.e2e.ts", "tests/question-composer.e2e.ts", + "tests/question-composer-recommended.e2e.ts", "tests/question-ptc.e2e.ts", "tests/approval-composer.e2e.ts", "tests/ptc-escalation.e2e.ts", @@ -126,7 +128,7 @@ "tests/developer-tools-settings.e2e.ts", "tests/models-settings.e2e.ts", "tests/deepseek-messages-settings.e2e.ts", - "tests/deepseek-model-label-1017.e2e.ts", + "tests/model-badge.e2e.ts", "tests/deepseek-messages-chat.e2e.ts", "tests/models-settings-recovery.e2e.ts", "tests/default-model.e2e.ts", @@ -157,6 +159,7 @@ "tests/stats-paged-history.e2e.ts", "tests/stats-truncated-tooltip.e2e.ts", "tests/sidebar-scrollbar.e2e.ts", + "tests/sidebar-alignment.e2e.ts", "tests/sidebar-title-hover-scroll.e2e.ts", "tests/rail-search-expand.e2e.ts", "tests/conversation-column-overflow.e2e.ts", diff --git a/benchmarks/session-corpus/README.i18n.yaml b/benchmarks/session-corpus/README.i18n.yaml index 2162840b3e..5e02d08e37 100644 --- a/benchmarks/session-corpus/README.i18n.yaml +++ b/benchmarks/session-corpus/README.i18n.yaml @@ -12,8 +12,8 @@ en: 66a769ce60d49ff3 zh: 7e0bfa7c598157fa /session-corpus-benchmarks/run: - en: 2560e832041ae481 - zh: 2fbbff71135e52ab + en: c770082e58aa8194 + zh: 339d659595a1f97d /session-corpus-benchmarks/measurements: en: c357c231c3bfb588 zh: 3d901a2caccf2587 diff --git a/benchmarks/session-corpus/README.md b/benchmarks/session-corpus/README.md index d504417d77..f55fc563ec 100644 --- a/benchmarks/session-corpus/README.md +++ b/benchmarks/session-corpus/README.md @@ -18,7 +18,7 @@ Measure the Web Host's Session list, content search, and fork over synthetic cor From the repository root, build the libraries and workers with `pnpm run build:bench`, then run `pnpm exec vitest run --config vitest.bench.config.ts benchmarks/session-corpus/session-corpus.bench.ts`. Do not overlap timing runs with builds or other benchmarks. Seeding writes about 1.5 GB of Zstandard logs under a private temporary root, which the test removes after success or failure. -The test reports every fresh-process sample, the CPU model, available parallelism, platform, and Node version, and enforces median budgets. Its last case fails when the file, including seeding, exceeds five minutes. A failed worker reports its exit, signal, timeout, and stderr tail. The required benchmark lane discovers this file automatically. +The test reports every fresh-process sample, the CPU model, available parallelism, platform, and Node version, and enforces median budgets. Its last case fails when the file, including seeding, exceeds five minutes. Fork workers use a fixed V8 heap limit; peak RSS still measures the whole process. A failed worker reports its exit, signal, timeout, and stderr tail. The required benchmark lane discovers this file automatically. diff --git a/benchmarks/session-corpus/README.zh.md b/benchmarks/session-corpus/README.zh.md index 1077b5233e..e7b1f1586a 100644 --- a/benchmarks/session-corpus/README.zh.md +++ b/benchmarks/session-corpus/README.zh.md @@ -18,7 +18,7 @@ 在仓库根目录使用 `pnpm run build:bench` 构建库和 worker,然后运行 `pnpm exec vitest run --config vitest.bench.config.ts benchmarks/session-corpus/session-corpus.bench.ts`。不要让计时运行与构建或其他基准重叠。播种过程会在私有临时根目录下写入约 1.5 GB 的 Zstandard 日志,测试成功或失败后都会删除该目录。 -测试报告每个新进程样本、CPU 型号、可用并行度、平台和 Node 版本,并约束中位数预算。当整个文件(包括播种)超过五分钟时,最后一个用例失败。worker 失败时报告退出状态、信号、超时和 stderr 末尾。必需基准通道自动发现此文件。 +测试报告每个新进程样本、CPU 型号、可用并行度、平台和 Node 版本,并约束中位数预算。当整个文件(包括播种)超过五分钟时,最后一个用例失败。Fork worker 使用固定的 V8 堆上限;峰值 RSS 仍测量整个进程。worker 失败时报告退出状态、信号、超时和 stderr 末尾。必需基准通道自动发现此文件。 diff --git a/benchmarks/session-corpus/session-corpus.bench.ts b/benchmarks/session-corpus/session-corpus.bench.ts index 00bc2d2979..3bbb66809c 100644 --- a/benchmarks/session-corpus/session-corpus.bench.ts +++ b/benchmarks/session-corpus/session-corpus.bench.ts @@ -29,6 +29,8 @@ const CORPUS = { search: 1_000, fork: 1_000, list: 3_000 } as const const FILE_LIMIT_MS = 300_000 /** Per-child deadline: an endpoint slower than its budget still reports a measurement instead of a kill. */ const WORKER_TIMEOUT_MS = FILE_LIMIT_MS +/** Fixed old-space capacity makes fork RSS comparable across hosted V8 heap defaults. */ +const FORK_HEAP_LIMIT_MB = 512 /** Fresh processes per list and fork-strata scenario; the median enforces each budget. */ const ATTEMPTS = { list: 3, fork: 3 } as const /** Concurrent anchor-preparation processes; standard hosted runners have two CPUs. */ @@ -119,7 +121,10 @@ function serialDeadline(workers: number): number { } async function run(args: readonly string[]): Promise { - const outcome = await runBuiltBenchmarkWorker({ worker: WORKER, args, timeoutMs: WORKER_TIMEOUT_MS, exposeGc: true }) + const outcome = await runBuiltBenchmarkWorker({ + worker: WORKER, args, timeoutMs: WORKER_TIMEOUT_MS, exposeGc: true, + ...(args[1] === 'fork' ? { heapLimitMb: FORK_HEAP_LIMIT_MB } : {}), + }) if (outcome.report === undefined) { const stderr = outcome.stderr.trim().split('\n').slice(-20).join('\n') throw new Error(`session-corpus worker ${args[1] ?? ''} failed: exit=${String(outcome.exitCode)}, ` diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 39ebd5cd9c..d1766b9e80 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -74,4 +74,4 @@ Hunt these in any doc; [dsh-doc](../.agents/skills/dsh-doc/SKILL.md) runs this l ## Repository references -Use relative Markdown links for current files and tags or PR numbers for historical references. `verify-md-links` checks local targets. [Reference validation](../scripts/verify-repository-references.ts) rejects actual commit identifiers and disallowed organization URLs in maintained files. +Use relative Markdown links for current files and tags or PR numbers for historical references. `verify-md-links` checks local targets. [Reference validation](../scripts/verify-repository-references.ts) rejects commit identifiers containing a hexadecimal letter and disallowed organization URLs in maintained files. Decimal-only tokens are ignored to avoid collisions with numeric data, even when they identify commits. diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index c8543cdc36..72dc6b08a6 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -27,8 +27,8 @@ en: 1a79a70b62c04cdb zh: c86d765e21430405 /deepseek-harness-architecture/session-log: - en: 8a7f19b95d426c99 - zh: d5bb0538404e20ea + en: b3b63d8fe7cab9fe + zh: 3703fcf30b1639d0 /deepseek-harness-architecture/capability-seams: en: a5bc20c3f2a1b370 zh: 91773a281410b2f4 diff --git a/docs/architecture.md b/docs/architecture.md index e6bede8d0e..e3f5cd24a3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -127,7 +127,7 @@ Session consumers know only the current logical format. Header-only `stat` and ` **Model-visible means logged.** Every model request must be reconstructable from the log. New model-visible inputs require session events. Plugins changing existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly. Tool changes are capability-independent; [Session tool history](../packages/core/session/README.md) supplies provider declarations. -**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, and carriers batch cropped client views with `snapshot()`. Domain services await registration during activation, retain private handles, and expose business methods for other plugins. Releasing registration removes global participation; retained handles continue folding Session events. `AgentRegistry` owns turn and step state and exposes `turnBoundaryOf(session)` ([registration lifecycle](../packages/session/session-projection/README.md)). `AgentLoop` owns `modelSelection` and constructs each Agent’s `model` instance alongside its inbox. +**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events; carriers read selected client views through `snapshot()`. Domain services await registration, retain private handles, and expose business methods ([registration lifecycle](../packages/session/session-projection/README.md)). `AgentRegistry` exposes turn and step state through `turnBoundaryOf(session)`. `AgentLoop` owns `modelSelection` and inbox state ([loop lifecycle](../packages/core/agent-loop/README.md)). [Working directories](subsystems/working-directory.md) supply user context and execution paths without changing original project identity, sandbox write roots, or existing process directories. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 8863c8047c..4ab73a419d 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -131,7 +131,7 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` **模型可见即已记录。** 每个模型请求都必须能从日志重建。新增模型可见输入需要会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。 工具变更不依赖能力;[Session 工具历史](../packages/core/session/README.zh.md)提供提供方声明。 -**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。领域服务在激活时等待注册完成,在内部保留句柄,并向其他插件提供业务方法。释放注册会移除全局参与关系;已有句柄仍可继续折叠 Session 事件。`AgentRegistry` 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`([注册生命周期](../packages/session/session-projection/README.zh.md))。 `AgentLoop` 持有 `modelSelection`,并与 inbox 一起构建每个 Agent 的 `model` 实例。 +**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元折叠已提交事件,载体通过 `snapshot()` 读取选定的客户端视图。领域服务等待注册完成,在内部保留句柄,并提供业务方法([注册生命周期](../packages/session/session-projection/README.zh.md))。`AgentRegistry` 通过 `turnBoundaryOf(session)` 提供轮次和步骤状态。`AgentLoop` 持有 `modelSelection` 和 inbox 状态([Loop 生命周期](../packages/core/agent-loop/README.zh.md))。 [工作目录](subsystems/working-directory.zh.md) 提供用户上下文和执行路径,不改变原始项目标识、沙箱写入根目录或已有进程目录。 diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index cb8f632442..69805a5895 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: cbe99e1985f2deae - zh: 6e524e7a644ae2d6 + en: 6dfe441b758583b2 + zh: fb28de0533f07432 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 14a1a92890..e42fc5d08e 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -84,9 +84,10 @@ flowchart LR pkg_llm_replay["llm-replay"] pkg_agent_loop["agent-loop"] pkg_compaction_basic["compaction-basic"] + pkg_session_log_deepseek["session-log-deepseek"] + svc_sessionLogDeepSeek["ctx.sessionLogDeepSeek
Session-log upload policy"] pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"] svc_deepseekLlmApiExtensions["ctx.deepseekLlmApiExtensions
Official DeepSeek request extensions"] - pkg_session_log_deepseek["session-log-deepseek"] pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"] pkg_token_meter["token-meter"] svc_tokenMeter["ctx.tokenMeter
Replay token measurement"] @@ -312,6 +313,8 @@ flowchart LR pkg_cordis_host_runner["cordis-host-runner"] svc_dynamicCordisRunner["ctx.dynamicCordisRunner
Dynamic Cordis package host runner"] svc_cordisInspect["ctx.cordisInspect
Dynamic Cordis inspect registry"] + pkg_experimental_liveshare["experimental-liveshare"] + svc_liveshareShares["ctx.liveshareShares
Owner share management Remote"] pkg_action_cards --> svc_actionCards pkg_agent --> svc_agents pkg_agent_default_model --> svc_agentDefaultModel @@ -369,6 +372,7 @@ flowchart LR pkg_experimental_forge --> svc_forge pkg_experimental_forge_github --> svc_forge pkg_experimental_forge_session --> svc_forgeSession + pkg_experimental_liveshare --> svc_liveshareShares pkg_experimental_ptc_runtime_python --> svc_ptcRuntime pkg_experimental_speech_to_text --> svc_speechToText pkg_experimental_speech_to_text_sensevoice --> svc_speechToText @@ -419,6 +423,7 @@ flowchart LR pkg_schedule --> svc_schedule pkg_session --> svc_sessions pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions + pkg_session_log_deepseek --> svc_sessionLogDeepSeek pkg_session_persistence --> svc_sessionPersistence pkg_session_persistence_jsonl --> svc_sessionPersistence pkg_session_projection --> svc_sessionProjections @@ -669,6 +674,7 @@ flowchart LR | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. | | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | Owns streaming intake, durable storage, and staged receipt lifetime; the Session controller binds receipts to accepted submissions. | | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. | +| `ctx.sessionLogDeepSeek` | `core` | [`session-log-deepseek`](../packages/session/session-log-deepseek) | - | - | - | Plugins register caller-owned exclusions before the canonical Session-log request contribution is prepared; unmatched requests use normal upload eligibility. | | `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`session-log-deepseek`](../packages/session/session-log-deepseek), [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance. | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. | | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | @@ -751,5 +757,6 @@ flowchart LR | `ctx.lsp` | `seam` | [`lsp`](../packages/lsp/lsp) | [`lsp-stdio`](../packages/lsp/lsp-stdio) | [`tool-lsp`](../packages/lsp/tool-lsp) | - | Provider registration and selection plus normalized query execution over exactly four operations; the seam offers no protocol escape hatch, so a backend translates into the normalized request and result. | | `ctx.dynamicCordisRunner` | `core` | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | - | [`tool-cordis`](../packages/extensions/tool-cordis) | - | Owns the in-memory definition registry, the vm sandbox for host halves, and the request-run round trip; browser pages reach the same service over the wire through its remote namespace. | | `ctx.cordisInspect` | `core` | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | - | [`tool-cordis`](../packages/extensions/tool-cordis) | - | Registers host inspect providers, mirrors the client provider manifest, and routes client queries through the dynamic Cordis transport. | +| `ctx.liveshareShares` | `service` | [`experimental-liveshare`](../packages/experimental/liveshare) | - | - | - | Publishes the owner share-management operations as one Typert Remote namespace that the owner client face calls; every method delegates to the same operations the same-origin routes used. | Maintenance mode: hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard. diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 4e517bbab6..f434835133 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -86,9 +86,10 @@ flowchart LR pkg_llm_replay["llm-replay"] pkg_agent_loop["agent-loop"] pkg_compaction_basic["compaction-basic"] + pkg_session_log_deepseek["session-log-deepseek"] + svc_sessionLogDeepSeek["ctx.sessionLogDeepSeek
Session-log upload policy"] pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"] svc_deepseekLlmApiExtensions["ctx.deepseekLlmApiExtensions
Official DeepSeek request extensions"] - pkg_session_log_deepseek["session-log-deepseek"] pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"] pkg_token_meter["token-meter"] svc_tokenMeter["ctx.tokenMeter
Replay token measurement"] @@ -314,6 +315,8 @@ flowchart LR pkg_cordis_host_runner["cordis-host-runner"] svc_dynamicCordisRunner["ctx.dynamicCordisRunner
Dynamic Cordis package host runner"] svc_cordisInspect["ctx.cordisInspect
Dynamic Cordis inspect registry"] + pkg_experimental_liveshare["experimental-liveshare"] + svc_liveshareShares["ctx.liveshareShares
Owner share management Remote"] pkg_action_cards --> svc_actionCards pkg_agent --> svc_agents pkg_agent_default_model --> svc_agentDefaultModel @@ -371,6 +374,7 @@ flowchart LR pkg_experimental_forge --> svc_forge pkg_experimental_forge_github --> svc_forge pkg_experimental_forge_session --> svc_forgeSession + pkg_experimental_liveshare --> svc_liveshareShares pkg_experimental_ptc_runtime_python --> svc_ptcRuntime pkg_experimental_speech_to_text --> svc_speechToText pkg_experimental_speech_to_text_sensevoice --> svc_speechToText @@ -421,6 +425,7 @@ flowchart LR pkg_schedule --> svc_schedule pkg_session --> svc_sessions pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions + pkg_session_log_deepseek --> svc_sessionLogDeepSeek pkg_session_persistence --> svc_sessionPersistence pkg_session_persistence_jsonl --> svc_sessionPersistence pkg_session_projection --> svc_sessionProjections @@ -671,6 +676,7 @@ flowchart LR | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 | | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | 负责流式接收、持久存储和暂存回执生命周期;Session Controller 将回执绑定到已接受的提交。 | | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 | +| `ctx.sessionLogDeepSeek` | `core` | [`session-log-deepseek`](../packages/session/session-log-deepseek) | - | - | - | 插件在准备权威 Session 日志请求贡献前注册归调用方所有的排除条件;没有条件匹配的请求应用普通上传资格判断。 | | `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`session-log-deepseek`](../packages/session/session-log-deepseek), [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 插件准备彼此独立的顶层字段;官方适配器会合并这些字段,并在 HTTP 接受后提交其交付状态。 | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 | | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 | @@ -753,5 +759,6 @@ flowchart LR | `ctx.lsp` | `seam` | [`lsp`](../packages/lsp/lsp) | [`lsp-stdio`](../packages/lsp/lsp-stdio) | [`tool-lsp`](../packages/lsp/tool-lsp) | - | 提供方注册与选择,加上恰好四种操作的标准化查询执行;该 seam 不提供协议逃生口,后端必须转换为标准化请求和结果。 | | `ctx.dynamicCordisRunner` | `core` | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | - | [`tool-cordis`](../packages/extensions/tool-cordis) | - | 拥有内存定义注册表、Host 半的 vm 沙箱和 request-run 往返流程;浏览器页面通过其 Remote 命名空间在线访问同一服务。 | | `ctx.cordisInspect` | `core` | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | - | [`tool-cordis`](../packages/extensions/tool-cordis) | - | 注册 Host inspect 提供方、镜像 Client 提供方 manifest,并通过动态 Cordis 传输路由 Client 查询。 | +| `ctx.liveshareShares` | `service` | [`experimental-liveshare`](../packages/experimental/liveshare) | - | - | - | 以单个 Typert Remote 命名空间发布 owner 分享管理操作,供 owner 客户端面调用;每个方法都委托给同源路由原先使用的同一组操作。 | 维护模式:混合模式。服务从 Cordis 声明中发现;接口、实现和消费方角色在 `scripts/gen-doc-graphs.ts` 中分类,并设有完整性守卫。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 68c65673a9..ca0dc021f1 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -706,13 +706,30 @@ export type ThemePreference = typeof THEME_PREFERENCES[number] ``` + + + +## `@deepseek-ai/dsh-client-ui-trajectory` + +- `inject`: `sessions` · `typert` +- `source`: [`packages/client/ui-trajectory/src/index.ts:25`](../packages/client/ui-trajectory/src/index.ts) + +```ts config-catalog +/** Count limit for each live Session's transient tool results. */ +export interface Config { + /** Maximum retained results per Session, including values awaiting a result event. */ + maxResultsPerSession: number +} +``` + + ## `@deepseek-ai/dsh-compaction-basic` - `inject`: `llm` · `tokenMeter` · `sessions` -- `source`: [`packages/compaction/compaction-basic/src/types.ts:40`](../packages/compaction/compaction-basic/src/types.ts) +- `source`: [`packages/compaction/compaction-basic/src/types.ts:46`](../packages/compaction/compaction-basic/src/types.ts) ```ts config-catalog /** Basic compaction configuration with an optional exact-target policy table. */ @@ -1033,7 +1050,7 @@ export interface CotTranslationPreferences { ## `@deepseek-ai/dsh-experimental-client-ui-forge` - `inject`: `forge` · `forgeSession` · `sessionProjections` · `workingDirectory` · `fs` · `subprocess` · `sandbox` · `sandboxPolicy` · `agents` -- `source`: [`packages/experimental/client-ui-forge/src/types.ts:296`](../packages/experimental/client-ui-forge/src/types.ts) +- `source`: [`packages/experimental/client-ui-forge/src/types.ts:318`](../packages/experimental/client-ui-forge/src/types.ts) ```ts config-catalog /** Host settings of the forge panel Remote. */ @@ -1109,14 +1126,14 @@ export type Config = CuaConfig ## `@deepseek-ai/dsh-experimental-forge` -- `source`: [`packages/experimental/forge/src/types.ts:205`](../packages/experimental/forge/src/types.ts) +- `source`: [`packages/experimental/forge/src/types.ts:230`](../packages/experimental/forge/src/types.ts) ```ts config-catalog /** Poll cadence and fetch deadline for watched change requests. */ export interface Config { /** Poll interval in milliseconds while checks run or mergeability is unknown. */ activeIntervalMs?: number - /** Poll interval in milliseconds for an open change request whose checks have settled. */ + /** Poll interval in milliseconds for a closed request or an open request whose checks have settled. */ idleIntervalMs?: number /** Largest delay in milliseconds between polls after consecutive failures. */ maxBackoffMs?: number @@ -2217,8 +2234,8 @@ export type PiAiThinkingTokenBudgetField = NonNullable @@ -2978,7 +2997,7 @@ export interface JsonRpcConfig { - `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) +- `source`: [`packages/session/session-log-deepseek/src/index.ts:47`](../packages/session/session-log-deepseek/src/index.ts) ```ts config-catalog /** Session-log request contribution configuration. */ @@ -4816,7 +4835,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | | `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) | | `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) | -| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) | | `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) | | `@deepseek-ai/dsh-client-ui-directory-picker-native` | — | [`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts) | @@ -4856,7 +4874,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-client-ui-skill` | — | [`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts) | | `@deepseek-ai/dsh-client-ui-subagent` | — | [`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts) | | `@deepseek-ai/dsh-client-ui-tool` | — | [`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts) | -| `@deepseek-ai/dsh-client-ui-trajectory` | — | [`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts) | | `@deepseek-ai/dsh-client-ui-user-questions` | — | [`packages/client/ui-user-questions/src/index.ts`](../packages/client/ui-user-questions/src/index.ts) | | `@deepseek-ai/dsh-client-ui-workflow-run` | — | [`packages/client/ui-workflow-run/src/index.ts`](../packages/client/ui-workflow-run/src/index.ts) | | `@deepseek-ai/dsh-client-ui-workspace` | — | [`packages/client/ui-workspace/src/index.ts`](../packages/client/ui-workspace/src/index.ts) | @@ -4971,6 +4988,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. | `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-forge-bundle` | — | [`packages/experimental/forge-bundle/src/index.ts`](../packages/experimental/forge-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-inspector-profile` | — | [`packages/experimental/inspector-profile/src/index.ts`](../packages/experimental/inspector-profile/src/index.ts) | +| `@deepseek-ai/dsh-experimental-liveshare` | — | [`packages/experimental/liveshare/src/index.ts`](../packages/experimental/liveshare/src/index.ts) | | `@deepseek-ai/dsh-experimental-liveshare-protocol` | — | [`packages/experimental/liveshare-protocol/src/index.ts`](../packages/experimental/liveshare-protocol/src/index.ts) | | `@deepseek-ai/dsh-experimental-plugin-market-bundle` | — | [`packages/experimental/plugin-market-bundle/src/index.ts`](../packages/experimental/plugin-market-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-ralph-bundle` | — | [`packages/experimental/ralph-bundle/src/index.ts`](../packages/experimental/ralph-bundle/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index fb30c4bb5d..2b9d252a52 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -708,13 +708,30 @@ export type ThemePreference = typeof THEME_PREFERENCES[number] ``` + + + +## `@deepseek-ai/dsh-client-ui-trajectory` + +- `inject`: `sessions` · `typert` +- `source`: [`packages/client/ui-trajectory/src/index.ts:25`](../packages/client/ui-trajectory/src/index.ts) + +```ts config-catalog +/** Count limit for each live Session's transient tool results. */ +export interface Config { + /** Maximum retained results per Session, including values awaiting a result event. */ + maxResultsPerSession: number +} +``` + + ## `@deepseek-ai/dsh-compaction-basic` - `inject`: `llm` · `tokenMeter` · `sessions` -- `source`: [`packages/compaction/compaction-basic/src/types.ts:40`](../packages/compaction/compaction-basic/src/types.ts) +- `source`: [`packages/compaction/compaction-basic/src/types.ts:46`](../packages/compaction/compaction-basic/src/types.ts) ```ts config-catalog /** Basic compaction configuration with an optional exact-target policy table. */ @@ -1035,7 +1052,7 @@ export interface CotTranslationPreferences { ## `@deepseek-ai/dsh-experimental-client-ui-forge` - `inject`: `forge` · `forgeSession` · `sessionProjections` · `workingDirectory` · `fs` · `subprocess` · `sandbox` · `sandboxPolicy` · `agents` -- `source`: [`packages/experimental/client-ui-forge/src/types.ts:296`](../packages/experimental/client-ui-forge/src/types.ts) +- `source`: [`packages/experimental/client-ui-forge/src/types.ts:318`](../packages/experimental/client-ui-forge/src/types.ts) ```ts config-catalog /** Host settings of the forge panel Remote. */ @@ -1111,14 +1128,14 @@ export type Config = CuaConfig ## `@deepseek-ai/dsh-experimental-forge` -- `source`: [`packages/experimental/forge/src/types.ts:205`](../packages/experimental/forge/src/types.ts) +- `source`: [`packages/experimental/forge/src/types.ts:230`](../packages/experimental/forge/src/types.ts) ```ts config-catalog /** Poll cadence and fetch deadline for watched change requests. */ export interface Config { /** Poll interval in milliseconds while checks run or mergeability is unknown. */ activeIntervalMs?: number - /** Poll interval in milliseconds for an open change request whose checks have settled. */ + /** Poll interval in milliseconds for a closed request or an open request whose checks have settled. */ idleIntervalMs?: number /** Largest delay in milliseconds between polls after consecutive failures. */ maxBackoffMs?: number @@ -2219,8 +2236,8 @@ export type PiAiThinkingTokenBudgetField = NonNullable @@ -2980,7 +2999,7 @@ export interface JsonRpcConfig { - `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) +- `source`: [`packages/session/session-log-deepseek/src/index.ts:47`](../packages/session/session-log-deepseek/src/index.ts) ```ts config-catalog /** Session-log request contribution configuration. */ @@ -4818,7 +4837,6 @@ export interface Config { | `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | | `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) | | `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) | -| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) | | `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) | | `@deepseek-ai/dsh-client-ui-directory-picker-native` | — | [`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts) | @@ -4858,7 +4876,6 @@ export interface Config { | `@deepseek-ai/dsh-client-ui-skill` | — | [`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts) | | `@deepseek-ai/dsh-client-ui-subagent` | — | [`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts) | | `@deepseek-ai/dsh-client-ui-tool` | — | [`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts) | -| `@deepseek-ai/dsh-client-ui-trajectory` | — | [`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts) | | `@deepseek-ai/dsh-client-ui-user-questions` | — | [`packages/client/ui-user-questions/src/index.ts`](../packages/client/ui-user-questions/src/index.ts) | | `@deepseek-ai/dsh-client-ui-workflow-run` | — | [`packages/client/ui-workflow-run/src/index.ts`](../packages/client/ui-workflow-run/src/index.ts) | | `@deepseek-ai/dsh-client-ui-workspace` | — | [`packages/client/ui-workspace/src/index.ts`](../packages/client/ui-workspace/src/index.ts) | @@ -4973,6 +4990,7 @@ export interface Config { | `@deepseek-ai/dsh-experimental-cot-translation-bundle` | — | [`packages/experimental/cot-translation-bundle/src/index.ts`](../packages/experimental/cot-translation-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-forge-bundle` | — | [`packages/experimental/forge-bundle/src/index.ts`](../packages/experimental/forge-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-inspector-profile` | — | [`packages/experimental/inspector-profile/src/index.ts`](../packages/experimental/inspector-profile/src/index.ts) | +| `@deepseek-ai/dsh-experimental-liveshare` | — | [`packages/experimental/liveshare/src/index.ts`](../packages/experimental/liveshare/src/index.ts) | | `@deepseek-ai/dsh-experimental-liveshare-protocol` | — | [`packages/experimental/liveshare-protocol/src/index.ts`](../packages/experimental/liveshare-protocol/src/index.ts) | | `@deepseek-ai/dsh-experimental-plugin-market-bundle` | — | [`packages/experimental/plugin-market-bundle/src/index.ts`](../packages/experimental/plugin-market-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-ralph-bundle` | — | [`packages/experimental/ralph-bundle/src/index.ts`](../packages/experimental/ralph-bundle/src/index.ts) | diff --git a/docs/deepseek-llm-api-wire-extensions.i18n.yaml b/docs/deepseek-llm-api-wire-extensions.i18n.yaml index 79b48d6dc5..c5248fa3f7 100644 --- a/docs/deepseek-llm-api-wire-extensions.i18n.yaml +++ b/docs/deepseek-llm-api-wire-extensions.i18n.yaml @@ -9,8 +9,8 @@ en: 1da81051addc4b47 zh: 3e8d7cd3f8c1cdba /official-deepseek-llm-api-wire-extensions/request-headers: - en: 740e92e903ecb594 - zh: f934d3c9e5c56bfe + en: d3d6a2c8240b80e9 + zh: debf42ed01233728 /official-deepseek-llm-api-wire-extensions/body-extension-transaction: en: 48001cfd1281a23b zh: d410530ae8aa2823 @@ -18,8 +18,8 @@ en: f89059050613f3e8 zh: da5cf2a4eeb81b75 /official-deepseek-llm-api-wire-extensions/dsh-session-log: - en: de1a3f2c377d74e3 - zh: 7c803345f71117c5 + en: 0d033f72951e2920 + zh: 767efca0dc39ac03 /official-deepseek-llm-api-wire-extensions/dsh-session-log/wire-session-header: en: ccd8fcd773df21c4 zh: 7ea020068ad093ae diff --git a/docs/deepseek-llm-api-wire-extensions.md b/docs/deepseek-llm-api-wire-extensions.md index 1a30f7c463..e9a270d98e 100644 --- a/docs/deepseek-llm-api-wire-extensions.md +++ b/docs/deepseek-llm-api-wire-extensions.md @@ -28,7 +28,7 @@ The [`DeepSeekLlmApiExtensionRegistry`](../packages/llm/deepseek-llm-api-extensi | `x-deepseek-harness-session-id` | Model requests carrying a Session id | The exact request `sessionId` string | | `x-deepseek-harness-compact` | Model requests whose purpose is `compaction` | The literal string `1` | -Credential failure happens before anonymous-user-id resolution, so an unauthorized request neither sends these headers nor creates the identity file. A direct request without a Session omits `x-deepseek-harness-session-id`. Session-title requests have no additional purpose header; the ordinary Session-id rule still applies when one carries a `sessionId`. +Credential failure happens before anonymous-user-id resolution, so an unauthorized request neither sends these headers nor creates the identity file. A direct request without a Session omits `x-deepseek-harness-session-id`. Session-title and Auto review requests have no additional purpose header; the ordinary Session-id rule still applies when one carries a `sessionId`. ## Body-extension transaction @@ -73,7 +73,7 @@ An enabled inventory with no qualifying entries sends `packages: []`; disabling ## `dsh_session_log` -[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.md) contributes one contiguous suffix of the canonical Session log. The field is enabled by default. It applies to a request with a live Session and at least one event; a direct request, a stale Session id, or an empty log omits the field, as does a request whose first pending event alone exceeds `maxBytes` or cannot be serialized; a composition disables it with `enabled: false`. The examples below use logical Session format 2 only to illustrate the wire fields; they do not identify the [current writer format](session-format-status.md). +[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.md) contributes one contiguous suffix of the canonical Session log. The field is enabled by default. It applies to a request with a live Session and at least one event; a direct request, a stale Session id, an empty log, or a request matched by a registered upload exclusion omits the field, as does a request whose first pending event alone exceeds `maxBytes` or cannot be serialized; a composition disables it with `enabled: false`. The examples below use logical Session format 2 only to illustrate the wire fields; they do not identify the [current writer format](session-format-status.md). ```json { diff --git a/docs/deepseek-llm-api-wire-extensions.zh.md b/docs/deepseek-llm-api-wire-extensions.zh.md index dcccecc6a7..d498e8f951 100644 --- a/docs/deepseek-llm-api-wire-extensions.zh.md +++ b/docs/deepseek-llm-api-wire-extensions.zh.md @@ -28,7 +28,7 @@ | `x-deepseek-harness-session-id` | 携带会话 id 的模型请求 | 确切的请求 `sessionId` 字符串 | | `x-deepseek-harness-compact` | 用途为 `compaction` 的模型请求 | 字面字符串 `1` | -凭据失败发生在解析匿名用户 id 之前,因此未授权请求既不会发送这些标头,也不会创建身份文件。没有会话的直接请求会省略 `x-deepseek-harness-session-id`。会话标题请求没有额外的用途标头;请求携带 `sessionId` 时,仍然适用普通的会话 id 规则。 +凭据失败发生在解析匿名用户 id 之前,因此未授权请求既不会发送这些标头,也不会创建身份文件。没有会话的直接请求会省略 `x-deepseek-harness-session-id`。会话标题请求和 Auto 审查请求没有额外的用途标头;请求携带 `sessionId` 时,仍然适用普通的会话 id 规则。 ## 正文扩展事务 @@ -73,7 +73,7 @@ ## `dsh_session_log` -[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认启用。它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id 或空日志会省略该字段,第一条待发事件本身超过 `maxBytes` 或无法序列化的请求也会省略该字段;组合可用 `enabled: false` 禁用它。下方示例使用逻辑 Session 格式 2 仅为说明协议字段,并不标识[当前写入格式](session-format-status.zh.md)。 +[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认启用。它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id、空日志或命中已注册上传排除条件的请求会省略该字段,第一条待发事件本身超过 `maxBytes` 或无法序列化的请求也会省略该字段;组合可用 `enabled: false` 禁用它。下方示例使用逻辑 Session 格式 2 仅为说明协议字段,并不标识[当前写入格式](session-format-status.zh.md)。 ```json { diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 999b732bc7..406a86b628 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -21,8 +21,8 @@ en: 3221d1076554001f zh: 29f166be6a5c04d9 /development-guide/contributor-reference/typescript-project-layout: - en: 12810e1c6ca8b8e4 - zh: b505d34cdf27856d + en: eb79b6f8ea745a9f + zh: de676c78231c823a /development-guide/contributor-reference/environment-variables: en: dba2c72ee6ffa930 zh: b65c27fbb9129ec8 diff --git a/docs/development.md b/docs/development.md index 76e34201cb..406b6c1a7d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -66,7 +66,7 @@ The repository uses isolated Host and Client aggregates. An ordinary package is | `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No | | `tsconfig.base.client.json` | Browser compiler settings (`jsx`, DOM libs, `types: []`) extended by the Client aggregate and every `packages/client/*` package. | No | -Host aggregate type checks run with a 8 GiB Node old-space limit across build, release, and Windows diagnostic commands. This is a per-compiler limit; concurrent build steps need additional memory. +Host and Client aggregate type checks run with an 8 GiB Node old-space limit. This is a per-compiler limit; concurrent build steps need additional memory. `pnpm run build:official --serial` prevents build steps from overlapping, even with `DSH_GATE_CONCURRENCY` set; the DSH publication workflow uses this option. Host and Client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Three disciplines follow: diff --git a/docs/development.zh.md b/docs/development.zh.md index ac61221c52..8dcedc8938 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -70,7 +70,7 @@ pnpm run typecheck | `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 | | `tsconfig.base.client.json` | 浏览器编译设置(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` 包 extends。 | 否 | -构建、发布和 Windows 诊断命令中的 Host aggregate 类型检查为 Node 老生代堆设置 8 GiB 上限。这是单个编译进程的上限;并发构建步骤还需要额外内存。 +Host 和 Client aggregate 类型检查为 Node 老生代堆设置 8 GiB 上限。这是单个编译进程的上限;并发构建步骤还需要额外内存。`pnpm run build:official --serial` 阻止构建步骤重叠,即使设置了 `DSH_GATE_CONCURRENCY` 也不例外;DSH 发布 workflow 使用此选项。 Host 与 Client 保持两个 aggregate program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个 aggregate,一个 paths 门面也可以横跨两侧。由此推出三条纪律: diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 51396898e2..e4318ec102 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -13,21 +13,21 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:689`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:669`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:696`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:675`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:682`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:691`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:671`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:698`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:677`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:684`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | @@ -48,14 +48,15 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `workspace-controller` | | `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | | `forge/providers-changed` | `parallel` | [`packages/experimental/forge/src/index.ts:27`](../packages/experimental/forge/src/index.ts) | `forge` (`parallel`) | `forge-session` | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:77`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:77`](../packages/fs/fs/src/index.ts) | `liveshare` (`emit`), [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `goal/activation-changed` | `emit` | [`packages/goal/goal/src/types.ts:147`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | `remotes` | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | | `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:40`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | -| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` | +| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:24`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` | +| `llm/models-updated` | `emit` | [`packages/llm/llm/src/types.ts:30`](../packages/llm/llm/src/types.ts) | [`llm-deepseek`](../packages/llm/llm-deepseek) (`emit`) | `remotes` | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` | | `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:427`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | @@ -68,8 +69,8 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `forge-session`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `forge-session`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), `ui-trajectory`, [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | @@ -82,9 +83,9 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` | | `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | -| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `forge-session`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | +| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `forge-session`, `liveshare`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present), `ui-trajectory` | | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` | | `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:44`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 1ecaf2151f..cb38f9f44c 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -15,21 +15,21 @@ | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | | `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` | | `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `client-ui-forge`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:689`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:669`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:696`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:675`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:682`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:691`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:671`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:698`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:677`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:684`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | @@ -50,14 +50,15 @@ | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `workspace-controller` | | `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | | `forge/providers-changed` | `parallel` | [`packages/experimental/forge/src/index.ts:27`](../packages/experimental/forge/src/index.ts) | `forge` (`parallel`) | `forge-session` | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:77`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:77`](../packages/fs/fs/src/index.ts) | `liveshare` (`emit`), [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | `liveshare` (`waterfall`), [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `goal/activation-changed` | `emit` | [`packages/goal/goal/src/types.ts:147`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | `remotes` | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | | `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:40`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - | -| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` | +| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:24`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` | +| `llm/models-updated` | `emit` | [`packages/llm/llm/src/types.ts:30`](../packages/llm/llm/src/types.ts) | [`llm-deepseek`](../packages/llm/llm-deepseek) (`emit`) | `remotes` | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:75`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` | | `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:427`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | @@ -70,8 +71,8 @@ | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `forge-session`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `forge-session`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), `ui-trajectory`, [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, `claude-code-mods`, `client-ui-forge`, [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, `forge-session`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), `server`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`tool-jobs`](../packages/jobs/tool-jobs), `ui-trajectory`, [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | @@ -84,9 +85,9 @@ | `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` | | `tools/execute` | `waterfall` | [`packages/core/tools/src/events.ts:29`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/events.ts:41`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | -| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/events.ts:18`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), `liveshare`, [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tool-executor-ptc/src/runtime-types.ts:46`](../packages/core/tool-executor-ptc/src/runtime-types.ts) | [`tool-executor-ptc`](../packages/core/tool-executor-ptc) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) | -| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `forge-session`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) | +| `tools/result` | `emit` | [`packages/core/tools/src/events.ts:49`](../packages/core/tools/src/events.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, `forge-session`, `liveshare`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present), `ui-trajectory` | | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:174`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` | | `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:44`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) | diff --git a/docs/module-graph.md b/docs/module-graph.md index d4e25a9bec..025d6ea304 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -166,7 +166,6 @@ flowchart TD pkg_client_ui_chat["client-ui-chat"] pkg_client_ui_commands["client-ui-commands"] pkg_client_ui_conversation["client-ui-conversation"] - pkg_client_ui_deepseek_model_label_1017["client-ui-deepseek-model-label-1017"] pkg_client_ui_deliverables["client-ui-deliverables"] pkg_client_ui_directory_picker_browse["client-ui-directory-picker-browse"] pkg_client_ui_directory_picker_native["client-ui-directory-picker-native"] @@ -287,6 +286,7 @@ flowchart TD pkg_experimental_forge_session["experimental-forge-session"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_inspector_profile["experimental-inspector-profile"] + pkg_experimental_liveshare["experimental-liveshare"] pkg_experimental_liveshare_protocol["experimental-liveshare-protocol"] pkg_experimental_plugin_market_bundle["experimental-plugin-market-bundle"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] @@ -499,6 +499,7 @@ flowchart TD pkg_credentials_local --> pkg_credentials pkg_credentials_local --> pkg_home_paths pkg_credentials_local --> pkg_launch_environment + pkg_experimental_liveshare --> pkg_scope pkg_anonymous_user_id --> pkg_brand pkg_anonymous_user_id --> pkg_home_paths pkg_lsp --> pkg_brand @@ -1545,7 +1546,6 @@ flowchart TD | [`client-ui-chat`](../packages/client/ui-chat) | `client` | — | | [`client-ui-commands`](../packages/client/ui-commands) | `client` | — | | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | — | -| [`client-ui-deepseek-model-label-1017`](../packages/client/ui-deepseek-model-label-1017) | `client` | — | | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | — | | [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | — | | [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | — | @@ -1656,6 +1656,7 @@ flowchart TD | [`computer-use`](../packages/computer-use/computer-use) | `computer-use` | [`brand`](../packages/util/brand) | | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`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-liveshare`](../packages/experimental/liveshare) | `experimental` | [`scope`](../packages/core/scope) | | [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths) | | [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) | | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`storage`](../packages/storage/storage) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 289e407bb8..47141c6e0b 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -168,7 +168,6 @@ flowchart TD pkg_client_ui_chat["client-ui-chat"] pkg_client_ui_commands["client-ui-commands"] pkg_client_ui_conversation["client-ui-conversation"] - pkg_client_ui_deepseek_model_label_1017["client-ui-deepseek-model-label-1017"] pkg_client_ui_deliverables["client-ui-deliverables"] pkg_client_ui_directory_picker_browse["client-ui-directory-picker-browse"] pkg_client_ui_directory_picker_native["client-ui-directory-picker-native"] @@ -289,6 +288,7 @@ flowchart TD pkg_experimental_forge_session["experimental-forge-session"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_inspector_profile["experimental-inspector-profile"] + pkg_experimental_liveshare["experimental-liveshare"] pkg_experimental_liveshare_protocol["experimental-liveshare-protocol"] pkg_experimental_plugin_market_bundle["experimental-plugin-market-bundle"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] @@ -501,6 +501,7 @@ flowchart TD pkg_credentials_local --> pkg_credentials pkg_credentials_local --> pkg_home_paths pkg_credentials_local --> pkg_launch_environment + pkg_experimental_liveshare --> pkg_scope pkg_anonymous_user_id --> pkg_brand pkg_anonymous_user_id --> pkg_home_paths pkg_lsp --> pkg_brand @@ -1547,7 +1548,6 @@ flowchart TD | [`client-ui-chat`](../packages/client/ui-chat) | `client` | — | | [`client-ui-commands`](../packages/client/ui-commands) | `client` | — | | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | — | -| [`client-ui-deepseek-model-label-1017`](../packages/client/ui-deepseek-model-label-1017) | `client` | — | | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | — | | [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | — | | [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | — | @@ -1658,6 +1658,7 @@ flowchart TD | [`computer-use`](../packages/computer-use/computer-use) | `computer-use` | [`brand`](../packages/util/brand) | | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`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-liveshare`](../packages/experimental/liveshare) | `experimental` | [`scope`](../packages/core/scope) | | [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths) | | [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) | | [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`storage`](../packages/storage/storage) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 280eda500e..dce8443743 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 1387c4505dde2faf zh: 48570fa1c13d6910 /session-persistence-event-catalog/persistence-type-fingerprints: - en: 519d46d6078d3f68 - zh: ec9aec262312c573 + en: 31c6dab0878b05a8 + zh: 176d0f7cbdc3fce3 /session-persistence-event-catalog/event-envelope: en: 56414ffd00cdc32a zh: 6dc11eb013e95e41 @@ -186,14 +186,14 @@ en: 6c6a27578dff5828 zh: 9a5add2bc0df2d0a /session-persistence-event-catalog/events/session/session-title-llm-request-log-only: - en: 50d9e29d3d2bfe52 - zh: 3ff1d959cf7776b9 + en: 59b02baa2be32ca0 + zh: 3637c99652ca89cf /session-persistence-event-catalog/events/session-log-deepseek: en: 7b827672c80aecdd zh: 7b827672c80aecdd /session-persistence-event-catalog/events/session-log-deepseek/session-log-deepseek-delivery-accepted-log-only: - en: 4e9ce9182ec05808 - zh: 7f643a6756ec8f76 + en: a3e2059fcc540b64 + zh: cc9933fd677560f0 /session-persistence-event-catalog/events/step: en: bd0dcbbc352b771a zh: bd0dcbbc352b771a @@ -555,8 +555,8 @@ en: 028791baa24a6a0a zh: 028791baa24a6a0a /session-persistence-event-catalog/resolved-persistence-types/in-history: - en: 3e9695c61a9e30c8 - zh: cdc765b18de2d665 + en: ad2f023eedbc2a7e + zh: 2124b8dedfbf8383 /session-persistence-event-catalog/resolved-persistence-types/in-progress: en: 61de2b0c9de8c15b zh: 61de2b0c9de8c15b @@ -753,8 +753,11 @@ en: 1e5fb67af2382e55 zh: 1e5fb67af2382e55 /session-persistence-event-catalog/resolved-persistence-types/set: - en: b2a724cef8d97ef7 - zh: b2a724cef8d97ef7 + en: 8e70b63b25a29f58 + zh: 8e70b63b25a29f58 +/session-persistence-event-catalog/resolved-persistence-types/share-viewer: + en: 761fbf929bc6e6ed + zh: 761fbf929bc6e6ed /session-persistence-event-catalog/resolved-persistence-types/skill-catalog: en: 9483a26e4c21d639 zh: 9483a26e4c21d639 @@ -990,11 +993,11 @@ en: 87f2aaf2b23a5249 zh: 3d6c5ed6603bee31 /session-persistence-event-catalog/resolved-persistence-types/contentblock: - en: d3eb9a17ab27856f - zh: a71b16a1e36a36ac + en: 920125405e3f3d40 + zh: f2b241fc44cd63be /session-persistence-event-catalog/resolved-persistence-types/contentblocktype: - en: 27dc8b92bae26f1f - zh: e05ace867fb59ae1 + en: d51d3737a6f1208f + zh: 7109487fbfaee359 /session-persistence-event-catalog/resolved-persistence-types/contentblock~2: en: 9207aa0489ccf8db zh: 850dd92da6769998 @@ -1008,11 +1011,11 @@ en: a6a2e38fdd6d127a zh: 1fe82356d89f5769 /session-persistence-event-catalog/resolved-persistence-types/deepseeksearchllmrequest: - en: cf3979c698e2b2ac - zh: 6252c8b668faa706 + en: fd75d1f4b9bd643c + zh: dde892ea9752f298 /session-persistence-event-catalog/resolved-persistence-types/developermessage: - en: 16992572a6919e52 - zh: 853f4873fd520444 + en: c6171fd8144d74b1 + zh: 2f3dd34cc8d7ed92 /session-persistence-event-catalog/resolved-persistence-types/epochheader: en: 3bdc8ecd0bdf0c9c zh: 0fe5b75c174ebab4 @@ -1026,11 +1029,11 @@ en: 0bf0caf488d14011 zh: 734501852de79a21 /session-persistence-event-catalog/resolved-persistence-types/fileblock: - en: 4cab6c6ed2abedd8 - zh: a6cdd344e6c36c24 + en: cc6e6a199425dde9 + zh: 8a23127ba156d058 /session-persistence-event-catalog/resolved-persistence-types/finishreason: - en: 49fd9834247f3cc2 - zh: 4378ead90069d0d6 + en: 4a9e91f2673dfed0 + zh: 89d08b1d16191cfe /session-persistence-event-catalog/resolved-persistence-types/goalblockreason: en: b1589f9578a585dd zh: 3783dbbd8793bb37 @@ -1065,8 +1068,8 @@ en: 76ee1e266cd651da zh: 55c7c95a223ccbed /session-persistence-event-catalog/resolved-persistence-types/imageblock: - en: ccafbcae37570ce2 - zh: fbf43a8c7ff26154 + en: 31b19d23dfee0571 + zh: 3ffa6c6d18cf73d5 /session-persistence-event-catalog/resolved-persistence-types/imagemediatype: en: 32d86426192176d5 zh: c86291efc19bdc91 @@ -1116,17 +1119,17 @@ en: 8f669d7819805099 zh: 391d51d7835afe02 /session-persistence-event-catalog/resolved-persistence-types/llmfailure: - en: a18d2091f7e64726 - zh: 550fc25036bd096e + en: 1c1591ce9b3cc186 + zh: 74ebe1b8c58e87d7 /session-persistence-event-catalog/resolved-persistence-types/llmretryeventdata: en: 6dbe099c62f7c226 zh: 6fd788f63c038c91 /session-persistence-event-catalog/resolved-persistence-types/llmretrystartedeventdata: - en: 5d61649043e317aa - zh: bdf9b9d553d291cf + en: 06b510217b6056c4 + zh: 5b3a1e022f96de57 /session-persistence-event-catalog/resolved-persistence-types/message: - en: 56953b055da0dc2b - zh: dcaa193ddb2fece0 + en: 7a8ec632289bc7f9 + zh: 5c90cb4f3d5d0a67 /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackdelete: en: c582953270d4ca44 zh: 966ccfe2e7660a2a @@ -1137,14 +1140,14 @@ en: f58575a1fc96df42 zh: 9f0c6ed9033e5c9d /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackrating: - en: 70ade19513d0070f - zh: ff9acb50974a32ae + en: 65fb012fcc0fa7db + zh: b829c586714e45e2 /session-persistence-event-catalog/resolved-persistence-types/messagesource: - en: b1bd7c8bd2309d45 - zh: d434e41517c3620d + en: ddeed90cb6112496 + zh: cefb41765c45b22f /session-persistence-event-catalog/resolved-persistence-types/message~2: - en: dcd71b3190faff96 - zh: b2c07224705aa52e + en: 8e96551640079569 + zh: a06d587638d42237 /session-persistence-event-catalog/resolved-persistence-types/modelmessagesource: en: 5f9154ca34235ab9 zh: a833105910ebfa82 @@ -1164,11 +1167,8 @@ en: 3e36e2f29ff1b506 zh: 3dcc1a1dced59d9f /session-persistence-event-catalog/resolved-persistence-types/presentedfile~2: - en: bb11552744e477b8 - zh: afb881f3ebf97e68 -/session-persistence-event-catalog/resolved-persistence-types/principal: - en: 6d0d81e0e833403e - zh: fa47cd75ee75f948 + en: 764b3e83192daae3 + zh: aa3f87bffe8fb501 /session-persistence-event-catalog/resolved-persistence-types/ptcdispatcheventdata: en: c0f7b22b41cb0608 zh: de14a1b5cac17665 @@ -1176,11 +1176,11 @@ en: aca35dac4f9d2add zh: 6a4bdb95e3c7b0ce /session-persistence-event-catalog/resolved-persistence-types/reasoningblock: - en: 07e04c3e336f880f - zh: 6f8986ede9994fa1 + en: 8d2873c634bf6405 + zh: 91e92b5730462cf0 /session-persistence-event-catalog/resolved-persistence-types/replayenvelope: - en: 488050ba0b6438b4 - zh: 2bf607b5ad51e39d + en: 34cbedee7bc95921 + zh: f7deeddb8d60152a /session-persistence-event-catalog/resolved-persistence-types/requestcontext: en: 544039047a28b6bb zh: c691f0bb1e23db08 @@ -1206,11 +1206,11 @@ en: 9a54c4478ad3b620 zh: 02c4c1fd7e252770 /session-persistence-event-catalog/resolved-persistence-types/sessiontitleeventdata: - en: 46baac9dcdfb1b63 - zh: 30a357bb8405c147 + en: 1f9f610fa0e05abf + zh: 11560710a1b871de /session-persistence-event-catalog/resolved-persistence-types/sessiontitlellmrequesteventdata: - en: 035b656ba6500174 - zh: fc9d4d7cd154ca78 + en: 465fe144e60dd62f + zh: 29e1029426106c3b /session-persistence-event-catalog/resolved-persistence-types/sessiontitlemodelidentity: en: 9531468264499498 zh: 9da23c0ad2776563 @@ -1218,8 +1218,11 @@ en: 13de8e35b749db45 zh: 01e4f3bcb6784fc8 /session-persistence-event-catalog/resolved-persistence-types/sessiontitlesource: - en: d842f1883e1b97d1 - zh: 41356f94354511ce + en: 8e07874aa7841b0b + zh: d93527a0f8837075 +/session-persistence-event-catalog/resolved-persistence-types/shareviewerprincipal: + en: a7b8c3f842059eec + zh: 4b345faa13cc39f6 /session-persistence-event-catalog/resolved-persistence-types/skillcatalogsource: en: 2c3148e4a9f0c8f9 zh: dab42cb485067bcd @@ -1227,8 +1230,8 @@ en: ecc15ebadb77afdf zh: 97ac27d6a4175f6f /session-persistence-event-catalog/resolved-persistence-types/streamchunk: - en: 6a9cc39c2df1b476 - zh: 697ea4d14d77f59c + en: 76ef1d6de369c7b5 + zh: e033b6bbaa455e87 /session-persistence-event-catalog/resolved-persistence-types/subagentcatalogevent: en: 725539dcd5c335ed zh: 5ca08d3667f84ff9 @@ -1266,8 +1269,8 @@ en: 9bff57038f472770 zh: 4eec0e793ae16d53 /session-persistence-event-catalog/resolved-persistence-types/textblock: - en: c5fcd2cdd37ea387 - zh: 9c678fbdea0bf25c + en: 7ad0f4915397b511 + zh: bcaa8bd449927989 /session-persistence-event-catalog/resolved-persistence-types/todoitem: en: ee24f9407df530ac zh: 310bedf3ac730d77 @@ -1275,20 +1278,20 @@ en: 8bf023246cbd0887 zh: dbf7113574c1bf59 /session-persistence-event-catalog/resolved-persistence-types/tokenusage: - en: 0aa983637b619157 - zh: e0f68258606aae78 + en: 09833c3cf66cc620 + zh: b7e9b3c138dae85b /session-persistence-event-catalog/resolved-persistence-types/tooladditionblock: - en: 7ba7c600fc529015 - zh: 94b69e9d5bd1fcfd + en: bbce95b556fe670f + zh: a1f1cae553a356ab /session-persistence-event-catalog/resolved-persistence-types/toolcallblock: - en: 78482b09dc227306 - zh: fb5852ac93beb20e + en: d7478804801b52a4 + zh: 27287739a5874fdd /session-persistence-event-catalog/resolved-persistence-types/toolmessagesource: en: 036b2595215410bb zh: 41792808ebac1fa3 /session-persistence-event-catalog/resolved-persistence-types/toolremovalblock: - en: fdac0df45529e14c - zh: c5e38de83a0e0a42 + en: 0080c345c319a4b5 + zh: de1a8b3ed621ce21 /session-persistence-event-catalog/resolved-persistence-types/toolrestriction: en: d70be5d46cb1ef7d zh: d3d5a05d62e443fe @@ -1296,8 +1299,8 @@ en: 2258683c3cfa4ce3 zh: 5663aa76c911761b /session-persistence-event-catalog/resolved-persistence-types/toolschema: - en: 689c56f199c0035d - zh: 48bcb9d508d1892c + en: afa1e3fd9ccdd95b + zh: 7af667e3ebac25e8 /session-persistence-event-catalog/resolved-persistence-types/toolschema~2: en: 12c14727bb7834e2 zh: b0f3c877873c36db @@ -1317,14 +1320,14 @@ en: 778debd61e0284a2 zh: 2cc82ad9e80f18b1 /session-persistence-event-catalog/resolved-persistence-types/turnendreason: - en: 3cbc1b5ae251451f - zh: a95ff0555e368432 + en: 953127f7cf1e0866 + zh: 3613ef1aafd8191a /session-persistence-event-catalog/resolved-persistence-types/usermessage: - en: 1b30ae0450f156bd - zh: 00bb0efae7ac741e + en: a15a08beb2b824da + zh: 4fd8dc6b9680c4cf /session-persistence-event-catalog/resolved-persistence-types/usermessage~2: - en: 1f242f48a3c8c230 - zh: 2516be50ce573b0c + en: 85e537b82f1da7d8 + zh: 73a1b9d4b5d29ab0 /session-persistence-event-catalog/resolved-persistence-types/workflowagentoutcome: en: d0a3f9aab18c3de5 zh: 76ac69a835ca4e29 @@ -1350,8 +1353,8 @@ en: 2786fef560851bb4 zh: 5d787475356b2606 /session-persistence-event-catalog/resolved-persistence-types/string: - en: 1e12d380c4988d40 - zh: 14a781104e00f360 + en: 1e484bd12747db9f + zh: 1e6fc6aa9066a702 /session-persistence-event-catalog/resolved-persistence-types/string~2: en: 0089620bf4638263 zh: 1922bef1713094bb @@ -1371,9 +1374,12 @@ en: f692844b29923b2e zh: 72f12bca08d63267 /session-persistence-event-catalog/resolved-persistence-types/union-2-variants~2: - en: 69d360cf5a1ae6b4 - zh: f23faf39a2a0e6f1 + en: 4a1acf31ea15d403 + zh: d1ebe94ffd308763 /session-persistence-event-catalog/resolved-persistence-types/union-2-variants~3: + en: 4318d8c6211d4e95 + zh: d08f3f8c11c32506 +/session-persistence-event-catalog/resolved-persistence-types/union-2-variants~4: en: 5a1af9849b0967ea zh: 1085d793bbc47360 /session-persistence-event-catalog/resolved-persistence-types/union-3-variants: @@ -1482,8 +1488,11 @@ en: a07969828eed2e6f zh: e06893743af544b5 /session-persistence-event-catalog/resolved-persistence-types/dialect-handlerid-matcher-point: - en: eeba88520aacd126 - zh: 8e2ca4e2bd2c1e3a + en: 9748b4f0facfe9f8 + zh: 83ae2cd2cd02b804 +/session-persistence-event-catalog/resolved-persistence-types/displayname-providerid-subjectid-tenantid: + en: d2c98ed9b98325ae + zh: bf8c2f7967cf9cc8 /session-persistence-event-catalog/resolved-persistence-types/end-start: en: 4b7a8793663d3fef zh: 249459c825d330c3 @@ -1497,17 +1506,17 @@ en: d019b2909673d2a7 zh: cc16e9c055ea603d /session-persistence-event-catalog/resolved-persistence-types/failure-kind-aborted: - en: 349cd90f2598fd12 - zh: 1eda02d520ad5d76 + en: 7dddcbc923491531 + zh: bbdaea7d433b6fda /session-persistence-event-catalog/resolved-persistence-types/failure-kind-error: - en: 51af619ed8532a43 - zh: a5ad878c63d0e046 + en: 4cb1c48095df98dc + zh: 5a03e44761af559e /session-persistence-event-catalog/resolved-persistence-types/header-reason-startsseries: - en: 27f5b76003ac1ead - zh: 2a5a0490cc22512b + en: 7d1610d20e987988 + zh: 1297cba0f2a2c73d /session-persistence-event-catalog/resolved-persistence-types/headerseq-message-step-turn: - en: 598021cdfe2a8687 - zh: 02e63a8b453f2f4e + en: 4f9635604ed5b84d + zh: 74810a2d049907e6 /session-persistence-event-catalog/resolved-persistence-types/height-width: en: a5df50710083ac25 zh: aad3ccf6f6de70e4 @@ -1524,11 +1533,11 @@ en: 41f73c58a77dfa28 zh: af77f4aee3cb4a40 /session-persistence-event-catalog/resolved-persistence-types/inherited: - en: 7e64cae3bd1b71e9 - zh: 7451c0639044c59a + en: 8b5c65ce7b57514f + zh: 231494d71e5d635a /session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start: - en: df2dd98c35b34bd2 - zh: ee25992a3f29d561 + en: 5a3be677a8dc51ff + zh: c5fc8b80f73e72d9 /session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream: en: 8e86cd838e3e4a49 zh: f5fec2a8adf1f3cc @@ -1647,8 +1656,8 @@ en: 673110c18ee48238 zh: e8dd81d68e1a4ee0 /session-persistence-event-catalog/resolved-persistence-types/kind-max-tokens: - en: 337ebe760096729a - zh: bd35a401103a3071 + en: 7092c1681af1dacb + zh: a0c5505f839592ed /session-persistence-event-catalog/resolved-persistence-types/kind-model-selection: en: a5165486dca91116 zh: b7051ca8167806ef @@ -1668,8 +1677,11 @@ en: 3c986a3b9cbc4f95 zh: 875bc0c6d786f80e /session-persistence-event-catalog/resolved-persistence-types/kind-model-selection-form-snapshot: - en: 509f9a91aedb2da1 - zh: db1071678bfb98d1 + en: b0e849caa4bd0be7 + zh: e5d0dbcf8a326caf +/session-persistence-event-catalog/resolved-persistence-types/kind-owner: + en: 686b6465397b96c2 + zh: c21c1cc3c9df4fd8 /session-persistence-event-catalog/resolved-persistence-types/kind-parent: en: 5ab43c283829de53 zh: 5ed4e90922a3a88b @@ -1764,8 +1776,8 @@ en: c99d8a67438771ff zh: 070f6160fc419fa5 /session-persistence-event-catalog/resolved-persistence-types/kind-stop: - en: 09ce6f3196938ccf - zh: 1b8d5540f47fd3aa + en: a54b282e864ff035 + zh: 973e729590675d85 /session-persistence-event-catalog/resolved-persistence-types/kind-time-context: en: 5733addfc8b12a47 zh: aaf60d5396e30971 @@ -1809,8 +1821,8 @@ en: b05115b92a176f3a zh: 22ba93572bd0209a /session-persistence-event-catalog/resolved-persistence-types/kind-tool-calls: - en: 65fb50e399cddd05 - zh: 9e713bbcb1b7db35 + en: b9203133bb7e6d5a + zh: b3f32a61983bb2e4 /session-persistence-event-catalog/resolved-persistence-types/kind-tool-goal: en: e11ea3169004ae33 zh: 7d6a3a77cb9bf205 @@ -1854,11 +1866,11 @@ en: 08be4349548406a9 zh: 8e7cf9966198ef78 /session-persistence-event-catalog/resolved-persistence-types/kind-tool-registry: - en: 5a0bbf4c76266ac5 - zh: 0b20854f90f9d74d + en: 4e102391d685eff5 + zh: 9762243a13014f3d /session-persistence-event-catalog/resolved-persistence-types/kind-user: - en: e98b716228feb157 - zh: 2bb593ae9cd0ce0d + en: 984cac7296874653 + zh: 13b4ee707a8ce305 /session-persistence-event-catalog/resolved-persistence-types/kind-user-approval: en: a52d2dfbaf844127 zh: 4c5306ca40f20873 @@ -1920,8 +1932,8 @@ en: 04b34b8ffe7e7764 zh: a792d5371516fd9d /session-persistence-event-catalog/resolved-persistence-types/sessionformatversion-sessionid-throughseq: - en: 8aaa02b06896492d - zh: 4dd0e1ee40d8f22d + en: 8063597183ef767f + zh: b064698c54089f07 /session-persistence-event-catalog/resolved-persistence-types/shadowedrange-shadowedseqs-shadowedtokencount: en: 7a2ab7b62244f43b zh: 1084e3f8876feebc @@ -1944,11 +1956,11 @@ en: 374b8adee8d571fa zh: b21057efc58e6490 /session-persistence-event-catalog/resolved-persistence-types/type-agent-preset-selected: - en: a990181189cf77ac - zh: 70198727356d1ad3 + en: da6e084997b20cdc + zh: 1780f6fcf3496de1 /session-persistence-event-catalog/resolved-persistence-types/type-agent-inbox-spliced: - en: f3b491e65c79b3a0 - zh: 2d3e53ceb67a55a3 + en: 1a79ed1a97145a14 + zh: d6f658a85c1e65f7 /session-persistence-event-catalog/resolved-persistence-types/type-approval-asked: en: 9c77f18b0dad22f0 zh: 8894cf2878fe90db @@ -1965,11 +1977,11 @@ en: bb507524bcc39c53 zh: 8314898562437473 /session-persistence-event-catalog/resolved-persistence-types/type-block-end: - en: f23d45580ba6d17b - zh: 4bdf4fc300a5b287 + en: 7513d6a33f4ad113 + zh: 652201059a3de858 /session-persistence-event-catalog/resolved-persistence-types/type-block-start: - en: ec1f0e5653a604db - zh: f905b8ce4d3730be + en: 56230efa41e79869 + zh: 9ca4b8990a28d4d9 /session-persistence-event-catalog/resolved-persistence-types/type-chunk: en: 2186ede3e9917aea zh: 0cfef64045dce95b @@ -1992,11 +2004,11 @@ en: ba73fb10b7e2ffc9 zh: 8d5c97e63e5f16da /session-persistence-event-catalog/resolved-persistence-types/type-deliverables-presented: - en: 17956f440c284ea3 - zh: 5dbfd68d50483047 + en: b9c35ef9a9e1d585 + zh: 7b8ad008c8b7389c /session-persistence-event-catalog/resolved-persistence-types/type-developer-message: - en: ded2f3b02f776800 - zh: 4d6619c283a18e3c + en: b0a3c504326f42f9 + zh: f13a1c1ca28f7da3 /session-persistence-event-catalog/resolved-persistence-types/type-disabled: en: 585627a50ece4e5f zh: 157837aee3da0ac3 @@ -2010,8 +2022,8 @@ en: 14e4b48d856bcf57 zh: 72ba176535dc7f3f /session-persistence-event-catalog/resolved-persistence-types/type-finish: - en: 76f708f530d4fc9a - zh: b6f1100c734a5991 + en: 30f8f49d0755dc2d + zh: d8553c9590a0e9bf /session-persistence-event-catalog/resolved-persistence-types/type-goal-change: en: aad2516b62d3ee83 zh: e7204c4dc4e8e408 @@ -2049,8 +2061,8 @@ en: 34e5591d75322649 zh: a216839f305c9796 /session-persistence-event-catalog/resolved-persistence-types/type-reasoning-delta: - en: d1bf7fbf222b717a - zh: e88811c2e0d45fb6 + en: a3f08ffda3526201 + zh: e599517be130ff99 /session-persistence-event-catalog/resolved-persistence-types/type-request-context: en: f8addb1a01da9ca1 zh: 4ef11ebddd302c05 @@ -2070,11 +2082,11 @@ en: acf7e21e4d21ebff zh: fb83a853634d204a /session-persistence-event-catalog/resolved-persistence-types/type-session-title: - en: f0acf1c873cd9e2a - zh: b8e175e14306afd4 + en: 5a42c1482248e385 + zh: c7ba7d071e4756de /session-persistence-event-catalog/resolved-persistence-types/type-session-title-llm-request: - en: ab4f20d890eed5e5 - zh: 6e4035a166821157 + en: d88fd740ff167f04 + zh: fe6598a1bf4cd9bf /session-persistence-event-catalog/resolved-persistence-types/type-step-end: en: 7fc233c4a569dae2 zh: bc2b3fed4a397c00 @@ -2109,8 +2121,8 @@ en: 1484738718de5312 zh: 3e6fbe9c64068225 /session-persistence-event-catalog/resolved-persistence-types/type-text-delta: - en: 38a0279d2ba3a230 - zh: 42d7be8102d27995 + en: 5f30a603fab07135 + zh: 3b7e2076351a1473 /session-persistence-event-catalog/resolved-persistence-types/type-todo-write: en: 2fa7ba77598a95b4 zh: 50972b51d77e9155 @@ -2118,8 +2130,8 @@ en: 32f732b97739e4d2 zh: f288e48e356969c0 /session-persistence-event-catalog/resolved-persistence-types/type-tool-call-delta: - en: 5822c230b4880a6e - zh: e0b86bdee2834b24 + en: 0e907728f330a8c7 + zh: d9d1400d4c468ab2 /session-persistence-event-catalog/resolved-persistence-types/type-tool-workflow-agent-end: en: 8eb5e1eb95722bc7 zh: 8dd5980029600648 @@ -2151,11 +2163,11 @@ en: 1b045de96172be4b zh: 8a15c6f35b576b37 /session-persistence-event-catalog/resolved-persistence-types/type-usage: - en: 4ed36e4498f5f203 - zh: 7b3fcddec14c306b + en: a1d9300dd7a7b043 + zh: 3cb1e0e6834e9765 /session-persistence-event-catalog/resolved-persistence-types/type-user-message: - en: 5b8341a673284ec1 - zh: e3264a827babffeb + en: 44035d460a9d926b + zh: 8006b6dbfc58d5ae /session-persistence-event-catalog/resolved-persistence-types/type-web-deepseek-search-llm-request: en: 99c8837344178a47 zh: bf746f5678346596 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 97cfa4bd63..96128feabb 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -23,7 +23,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30) | +| `event:agent/inbox/spliced` | event | `cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -36,7 +36,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f` | [`{ type: "developer/message" }`](#persistence-type-sha256-95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f) | +| `event:developer/message` | event | `70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f` | [`{ type: "developer/message" }`](#persistence-type-sha256-70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -58,7 +58,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5) | +| `event:session/title-llm-request` | event | `2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -80,7 +80,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f` | [`{ type: "user/message" }`](#persistence-type-sha256-39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f) | +| `event:user/message` | event | `5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451` | [`{ type: "user/message" }`](#persistence-type-sha256-5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451) | | `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) | | `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | @@ -878,7 +878,7 @@ Source: [`packages/session/session-title/src/index.ts:77`](../packages/session/s Types: [SessionTitleLlmRequestEventData](subsystems/session-title.md) -Source: [`packages/session/session-title-llm/src/index.ts:57`](../packages/session/session-title-llm/src/index.ts) +Source: [`packages/session/session-title-llm/src/index.ts:63`](../packages/session/session-title-llm/src/index.ts) ### `session-log-deepseek/*` @@ -898,7 +898,7 @@ Source: [`packages/session/session-title-llm/src/index.ts:57`](../packages/sessi } ``` -Source: [`packages/session/session-log-deepseek/src/types.ts:81`](../packages/session/session-log-deepseek/src/types.ts) +Source: [`packages/session/session-log-deepseek/src/types.ts:96`](../packages/session/session-log-deepseek/src/types.ts) ### `step/*` @@ -2007,7 +2007,7 @@ SHA-256: `ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8` SHA-256: `f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5` -Sources: [`packages/llm/llm/src/types.ts:414`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:440`](../packages/llm/llm/src/types.ts) `"in-history"` @@ -2539,6 +2539,14 @@ SHA-256: `b765ee31033cea463863faf3fc6a0a1b85c47c6a2f13fe1ce833abe56105114b` `"set"` + + +### `"share-viewer"` + +SHA-256: `6ba7edb6447b6a3e7947dddf5e4116c1526000c4e62882a6d65edc67ba797c2c` + +`"share-viewer"` + ### `"skill-catalog"` @@ -3266,7 +3274,7 @@ Sources: [`packages/compaction/compaction/src/checkpoint.ts:22`](../packages/com SHA-256: `8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5` -Sources: [`packages/llm/llm/src/types.ts:150`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:157`](../packages/llm/llm/src/types.ts) One of: @@ -3288,7 +3296,7 @@ One of: SHA-256: `7d65acf0a3a43a8f445ce5d3320b42eca492739fc931b2a7516bfb8368dc2c68` -Sources: [`packages/llm/llm/src/types.ts:148`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:155`](../packages/llm/llm/src/types.ts) One of: @@ -3375,7 +3383,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web | `body` | required | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) | | `endpoint` | required | `string` | - + @@ -3383,7 +3391,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web ### `DeveloperMessage` -SHA-256: `cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0` +SHA-256: `64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428` Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3392,7 +3400,7 @@ Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"developer"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a) | Source compatibility: `source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. @@ -3481,7 +3489,7 @@ Sources: [`packages/attachment/attachment/src/types.ts:39`](../packages/attachme SHA-256: `f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef` -Sources: [`packages/llm/llm/src/types.ts:98`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:105`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -3498,7 +3506,7 @@ Sources: [`packages/llm/llm/src/types.ts:98`](../packages/llm/llm/src/types.ts) SHA-256: `5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80` -Sources: [`packages/llm/llm/src/types.ts:165`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:172`](../packages/llm/llm/src/types.ts) | kind | Form property | Other required fields | Full definition | |---|---|---|---| @@ -3736,7 +3744,7 @@ Sources: [`packages/attachment/attachment/src/types.ts:11`](../packages/attachme SHA-256: `12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7` -Sources: [`packages/llm/llm/src/types.ts:79`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:86`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4044,7 +4052,7 @@ Sources: [`packages/llm/llm/src/call-config.ts:54`](../packages/llm/llm/src/call SHA-256: `5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca` -Sources: [`packages/llm/llm/src/types.ts:41`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:48`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4091,7 +4099,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr | `step` | required | `number` | | `turn` | required | `number` | - + @@ -4099,15 +4107,15 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr ### `Message` -SHA-256: `4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d` +SHA-256: `c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36` Sources: [`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) One of: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0) -- [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) +- [`DeveloperMessage`](#persistence-type-sha256-64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428) +- [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) - [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -4184,7 +4192,7 @@ One of: - `"negative"` - `"positive"` - + @@ -4192,7 +4200,7 @@ One of: ### `MessageSource` -SHA-256: `b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591` +SHA-256: `7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a` Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) @@ -4304,7 +4312,7 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message | `tool-jobs` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | [`{ kind: "tool-jobs", form: "snapshot" }`](#persistence-type-sha256-c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49) | | `tool-jobs` | not declared | none | [`{ kind: "tool-jobs" }`](#persistence-type-sha256-24ce31aa528272e59f21cd26da9eca08968bcc97529756a749bfdab71746d9c8) | | `tool-registry` | not declared | none | [`{ kind: "tool-registry" }`](#persistence-type-sha256-8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543) | -| `user` | not declared | `rpcId`: `string` | [`{ kind: "user" }`](#persistence-type-sha256-6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def) | +| `user` | not declared | `rpcId`: `string` | [`{ kind: "user" }`](#persistence-type-sha256-a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302) | | `user` | not declared | none | [`CommandSource`](#persistence-type-sha256-ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05) | | `user-approval` | `"catalog"` | none | [`{ kind: "user-approval", form: "catalog" }`](#persistence-type-sha256-825ca23416b1ea9d81442afead5cf9229a6fa05842f042135cc3aa9710a6a2d8) | | `user-approval` | `"instructions"` | none | [`{ kind: "user-approval", form: "instructions" }`](#persistence-type-sha256-aa2bf9e82b9fdeb94a42af20031fffa30e4e23f01f4ac39757427d4c64c407f0) | @@ -4317,13 +4325,13 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | | `working-directory` | not declared | none | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc` +SHA-256: `ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72` -Array of [`Message`](#persistence-type-sha256-4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d). +Array of [`Message`](#persistence-type-sha256-c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36). @@ -4441,22 +4449,6 @@ SHA-256: `1d9cb3caa96100b18b1911fa3ff8c751ef6992971a03c5f4d9aefc1a0004a345` Array of [`PresentedFile`](#persistence-type-sha256-b8fc636a2121df9d8423c242e03c9d23b7ab7c6fdce00e45746932c60b730f97). - - - - - - -### `Principal` - -SHA-256: `deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5` - -Sources: [`packages/typert/protocol/src/types.ts:457`](../packages/typert/protocol/src/types.ts) · [`packages/typert/protocol/src/types.ts:461`](../packages/typert/protocol/src/types.ts) - -| Property | Presence | Type | -|---|---|---| -| `kind` | required | `"owner"` | - @@ -4511,7 +4503,7 @@ Sources: [`packages/core/tool-executor-ptc/src/types.ts:12`](../packages/core/to SHA-256: `814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e` -Sources: [`packages/llm/llm/src/types.ts:68`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:75`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4528,7 +4520,7 @@ Sources: [`packages/llm/llm/src/types.ts:68`](../packages/llm/llm/src/types.ts) SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555` -Sources: [`packages/llm/llm/src/types.ts:448`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:474`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4707,7 +4699,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ | `source` | required | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | required | `string` | - + @@ -4715,15 +4707,15 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ ### `SessionTitleLlmRequestEventData` -SHA-256: `9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f` +SHA-256: `7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54` -Sources: [`packages/session/session-title-llm/src/index.ts:37`](../packages/session/session-title-llm/src/index.ts) +Sources: [`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) | Property | Presence | Type | |---|---|---| | `maxTokens` | required | `number` | | `messageSeqs` | required | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | required | [`Message[]`](#persistence-type-sha256-d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc) | +| `messages` | required | [`Message[]`](#persistence-type-sha256-ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72) | | `reasoningEffort` | optional | `string` | | `route` | required | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | required | `string` | @@ -4776,6 +4768,24 @@ Sources: [`packages/session/session-title/src/types.ts:28`](../packages/session/ | `provider` | not declared | `provider`: `string` | [`{ kind: "provider" }`](#persistence-type-sha256-b81e9de81a481b24e1e1f9adf038bfc59e57b4f35ce62f8f1e45284186f4f812) | | `user` | not declared | none | [`CommandSource`](#persistence-type-sha256-ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05) | + + + + + + +### `ShareViewerPrincipal` + +SHA-256: `4d60dddaa65d3da8d4cd014d85ce3d2c2875664dd0433e331d8e9d395289686b` + +Sources: [`packages/experimental/liveshare/src/host/viewer-principal.ts:24`](../packages/experimental/liveshare/src/host/viewer-principal.ts) + +| Property | Presence | Type | +|---|---|---| +| `actor` | required | [`{ displayName?, providerId, subjectId, tenantId }`](#persistence-type-sha256-0a92c5bdfa06c2e2615b777728f3aa95f408d43b9012e7b754568311251db623) | +| `kind` | required | `"share-viewer"` | +| `shareId` | required | `string` | + @@ -4824,7 +4834,7 @@ Sources: [`packages/skill/skill/src/index.ts:146`](../packages/skill/skill/src/i SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` -Sources: [`packages/llm/llm/src/types.ts:470`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:496`](../packages/llm/llm/src/types.ts) One of: @@ -5077,7 +5087,7 @@ One of: SHA-256: `4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c` -Sources: [`packages/llm/llm/src/types.ts:62`](../packages/llm/llm/src/types.ts) · [`packages/web/web-search-deepseek/src/provider.ts:66`](../packages/web/web-search-deepseek/src/provider.ts) +Sources: [`packages/llm/llm/src/types.ts:69`](../packages/llm/llm/src/types.ts) · [`packages/web/web-search-deepseek/src/provider.ts:66`](../packages/web/web-search-deepseek/src/provider.ts) | Property | Presence | Type | |---|---|---| @@ -5119,7 +5129,7 @@ Array of [`TodoItem`](#persistence-type-sha256-a99f76fd149051c363f960bfeeb5b9509 SHA-256: `fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394` -Sources: [`packages/llm/llm/src/types.ts:175`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:182`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5140,7 +5150,7 @@ Sources: [`packages/llm/llm/src/types.ts:175`](../packages/llm/llm/src/types.ts) SHA-256: `8e611bf46821e5003a1605a73560a6f56388295149d449dfe2142d5ec335f29c` -Sources: [`packages/llm/llm/src/types.ts:115`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:122`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5158,7 +5168,7 @@ Sources: [`packages/llm/llm/src/types.ts:115`](../packages/llm/llm/src/types.ts) SHA-256: `c1f0f17b6493b85db6453bb30e01b208d703731b25bc1e68e5d4fc72eea9cdea` -Sources: [`packages/llm/llm/src/types.ts:105`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:112`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5194,7 +5204,7 @@ Sources: [`packages/llm/llm/src/message.ts:29`](../packages/llm/llm/src/message. SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8` -Sources: [`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:134`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5249,7 +5259,7 @@ Sources: [`packages/llm/llm/src/message.ts:173`](../packages/llm/llm/src/message SHA-256: `66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2` -Sources: [`packages/llm/llm/src/types.ts:491`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:517`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -5380,7 +5390,7 @@ Sources: [`packages/core/session/src/types.ts:238`](../packages/core/session/src | `interrupted` | not declared | none | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | not declared | none | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5388,7 +5398,7 @@ Sources: [`packages/core/session/src/types.ts:238`](../packages/core/session/src ### `UserMessage` -SHA-256: `8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14` +SHA-256: `4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce` Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5397,19 +5407,19 @@ Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"user"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a) | Source compatibility: `source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. Attribution-only additions: `action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292` +SHA-256: `7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3` -Array of [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14). +Array of [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce). @@ -5601,7 +5611,7 @@ Array of `number`. SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -Sources: [`packages/api/session-controller/src/types.ts:386`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +Sources: [`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -5673,6 +5683,17 @@ One of: - `"fork"` - `"fresh"` + + +### `union (2 variants)` + +SHA-256: `824a4aad44a54e245c67196817d67946734a2dc0eee0b0b6834a5b2a6ef352ce` + +| kind | Form property | Other required fields | Full definition | +|---|---|---|---| +| `owner` | not declared | none | [`{ kind: "owner" }`](#persistence-type-sha256-deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5) | +| `share-viewer` | not declared | `actor`: [`{ displayName?, providerId, subjectId, tenantId }`](#persistence-type-sha256-0a92c5bdfa06c2e2615b777728f3aa95f408d43b9012e7b754568311251db623); `shareId`: `string` | [`ShareViewerPrincipal`](#persistence-type-sha256-4d60dddaa65d3da8d4cd014d85ce3d2c2875664dd0433e331d8e9d395289686b) | + ### `union (2 variants)` @@ -6198,6 +6219,21 @@ Sources: [`packages/experimental/hook-protocol/src/types.ts:19`](../packages/exp | `point` | required | `string` | | `turn` | required | `number` | + + +### `{ displayName?, providerId, subjectId, tenantId }` + +SHA-256: `0a92c5bdfa06c2e2615b777728f3aa95f408d43b9012e7b754568311251db623` + +Sources: [`packages/experimental/liveshare/src/host/viewer-principal.ts:29`](../packages/experimental/liveshare/src/host/viewer-principal.ts) + +| Property | Presence | Type | +|---|---|---| +| `displayName` | optional | `string` | +| `providerId` | required | `string` | +| `subjectId` | required | `string` | +| `tenantId` | required | `string` | + ### `{ end, start }` @@ -6260,7 +6296,7 @@ Sources: [`packages/core/session/src/types.ts:383`](../packages/core/session/src SHA-256: `8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0` -Sources: [`packages/llm/llm/src/types.ts:160`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:167`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6273,7 +6309,7 @@ Sources: [`packages/llm/llm/src/types.ts:160`](../packages/llm/llm/src/types.ts) SHA-256: `693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057` -Sources: [`packages/llm/llm/src/types.ts:161`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:168`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6294,18 +6330,18 @@ Sources: [`packages/core/session/src/types.ts:398`](../packages/core/session/src | `reason` | required | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | optional | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2` +SHA-256: `53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a` Sources: [`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| | `headerSeq` | optional | `number` | -| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0) | +| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428) | | `step` | required | `number` | | `turn` | required | `number` | @@ -6389,17 +6425,17 @@ Sources: [`packages/core/session/src/types.ts:435`](../packages/core/session/src |---|---|---| | `inherited` | optional | `true` | - + ### `{ inserted, outcome?, removedCount?, start, … }` -SHA-256: `c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c` +SHA-256: `d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401` Sources: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) | Property | Presence | Type | |---|---|---| -| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292) | +| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3) | | `outcome` | optional | `"canceled"` | | `removedCount` | optional | `number` | | `start` | required | `number` | @@ -6869,7 +6905,7 @@ Sources: [`packages/core/session/src/types.ts:201`](../packages/core/session/src SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7` -Sources: [`packages/core/session/src/types.ts:220`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts) +Sources: [`packages/core/session/src/types.ts:220`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:166`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -6953,6 +6989,18 @@ SHA-256: `52c6113cb0e53f345324bf84f01928bea5d5df57ee80bc0dd65407b8bc5a3734` | `kind` | required | `"model-selection"` | | `sections` | required | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | + + +### `{ kind: "owner" }` + +SHA-256: `deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5` + +Sources: [`packages/typert/protocol/src/types.ts:457`](../packages/typert/protocol/src/types.ts) + +| Property | Presence | Type | +|---|---|---| +| `kind` | required | `"owner"` | + ### `{ kind: "parent" }` @@ -7309,7 +7357,7 @@ SHA-256: `28936c614bd4e7e2774806edcbb3cbfd3ee643ac1cbf0bbdff43d915505a8fe3` SHA-256: `1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f` -Sources: [`packages/llm/llm/src/types.ts:157`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:164`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7477,7 +7525,7 @@ SHA-256: `49dab019d00e8c26f27ad2ccff7b604d69e323ae4d17e0d2a7b6b8a830b1ade2` SHA-256: `287211846beea1eccb5f9acee00d20388a38fcb2e95858fa2332d6f58529379a` -Sources: [`packages/llm/llm/src/types.ts:158`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:165`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -7651,19 +7699,19 @@ Sources: [`packages/core/tools/src/index.ts:28`](../packages/core/tools/src/inde |---|---|---| | `kind` | required | `"tool-registry"` | - + ### `{ kind: "user" }` -SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def` +SHA-256: `a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302` -Sources: [`packages/api/session-controller/src/types.ts:395`](../packages/api/session-controller/src/types.ts) +Sources: [`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts) | Property | Presence | Type | |---|---|---| | `clientTimeZone` | optional | `string` | | `kind` | required | `"user"` | -| `principal` | optional | [`Principal`](#persistence-type-sha256-deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5) | +| `principal` | optional | [`union (2 variants)`](#persistence-type-sha256-824a4aad44a54e245c67196817d67946734a2dc0eee0b0b6834a5b2a6ef352ce) | | `rpcId` | required | `string` | @@ -7931,7 +7979,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:64`](../packages/web SHA-256: `8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52` -Sources: [`packages/session/session-log-deepseek/src/types.ts:81`](../packages/session/session-log-deepseek/src/types.ts) +Sources: [`packages/session/session-log-deepseek/src/types.ts:96`](../packages/session/session-log-deepseek/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -8046,17 +8094,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | required | `number` | | `type` | required | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30` +SHA-256: `cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c) | +| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -8149,7 +8197,7 @@ SHA-256: `1033093edd0db80ff410e00830b523405e00bb0c7684948e531ff65095799625` SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d` -Sources: [`packages/llm/llm/src/types.ts:475`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:501`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -8163,7 +8211,7 @@ Sources: [`packages/llm/llm/src/types.ts:475`](../packages/llm/llm/src/types.ts) SHA-256: `3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90` -Sources: [`packages/llm/llm/src/types.ts:471`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:497`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -8297,17 +8345,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | required | `number` | | `type` | required | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f` +SHA-256: `70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2) | +| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8381,7 +8429,7 @@ SHA-256: `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` SHA-256: `a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1` -Sources: [`packages/llm/llm/src/types.ts:477`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:503`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -8587,7 +8635,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:28`](../packages/llm/llm/src SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095` -Sources: [`packages/llm/llm/src/types.ts:473`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:499`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -8707,17 +8755,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | required | `number` | | `type` | required | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5` +SHA-256: `2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f) | +| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -8907,7 +8955,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:21`](../packages/llm/llm/src SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65` -Sources: [`packages/llm/llm/src/types.ts:472`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:498`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -8955,7 +9003,7 @@ Sources: [`packages/llm/llm/src/assistant-stream.ts:35`](../packages/llm/llm/src SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a` -Sources: [`packages/llm/llm/src/types.ts:474`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:500`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -9133,24 +9181,24 @@ SHA-256: `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` -Sources: [`packages/llm/llm/src/types.ts:476`](../packages/llm/llm/src/types.ts) +Sources: [`packages/llm/llm/src/types.ts:502`](../packages/llm/llm/src/types.ts) | Property | Presence | Type | |---|---|---| | `type` | required | `"usage"` | | `usage` | required | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f` +SHA-256: `5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451` | Property | Presence | Type | |---|---|---| -| `data` | required | [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) | +| `data` | required | [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index c1dc641ae7..c890b50507 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -25,7 +25,7 @@ | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30) | +| `event:agent/inbox/spliced` | event | `cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -38,7 +38,7 @@ | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f` | [`{ type: "developer/message" }`](#persistence-type-sha256-95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f) | +| `event:developer/message` | event | `70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f` | [`{ type: "developer/message" }`](#persistence-type-sha256-70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -60,7 +60,7 @@ | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5) | +| `event:session/title-llm-request` | event | `2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -82,7 +82,7 @@ | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f` | [`{ type: "user/message" }`](#persistence-type-sha256-39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f) | +| `event:user/message` | event | `5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451` | [`{ type: "user/message" }`](#persistence-type-sha256-5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451) | | `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) | | `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | @@ -880,7 +880,7 @@ export type SessionEvent = { 类型:[SessionTitleLlmRequestEventData](subsystems/session-title.zh.md) -来源:[`packages/session/session-title-llm/src/index.ts:57`](../packages/session/session-title-llm/src/index.ts) +来源:[`packages/session/session-title-llm/src/index.ts:63`](../packages/session/session-title-llm/src/index.ts) ### `session-log-deepseek/*` @@ -900,7 +900,7 @@ export type SessionEvent = { } ``` -来源:[`packages/session/session-log-deepseek/src/types.ts:81`](../packages/session/session-log-deepseek/src/types.ts) +来源:[`packages/session/session-log-deepseek/src/types.ts:96`](../packages/session/session-log-deepseek/src/types.ts) ### `step/*` @@ -2009,7 +2009,7 @@ SHA-256: `ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8` SHA-256: `f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5` -来源:[`packages/llm/llm/src/types.ts:414`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:440`](../packages/llm/llm/src/types.ts) `"in-history"` @@ -2541,6 +2541,14 @@ SHA-256: `b765ee31033cea463863faf3fc6a0a1b85c47c6a2f13fe1ce833abe56105114b` `"set"` + + +### `"share-viewer"` + +SHA-256: `6ba7edb6447b6a3e7947dddf5e4116c1526000c4e62882a6d65edc67ba797c2c` + +`"share-viewer"` + ### `"skill-catalog"` @@ -3268,7 +3276,7 @@ SHA-256: `31907de9563751822af6d9be02bafaf26faa098bbd1cb412524134d577cc1648` SHA-256: `8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5` -来源:[`packages/llm/llm/src/types.ts:150`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:157`](../packages/llm/llm/src/types.ts) 以下类型之一: @@ -3290,7 +3298,7 @@ SHA-256: `8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5` SHA-256: `7d65acf0a3a43a8f445ce5d3320b42eca492739fc931b2a7516bfb8368dc2c68` -来源:[`packages/llm/llm/src/types.ts:148`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:155`](../packages/llm/llm/src/types.ts) 以下类型之一: @@ -3377,7 +3385,7 @@ SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c` | `body` | 必需 | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) | | `endpoint` | 必需 | `string` | - + @@ -3385,7 +3393,7 @@ SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c` ### `DeveloperMessage` -SHA-256: `cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0` +SHA-256: `64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428` 来源:[`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3394,7 +3402,7 @@ SHA-256: `cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"developer"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. @@ -3483,7 +3491,7 @@ SHA-256: `f19bee84bf3d20904553cdcd57465232bc1c2a194f54c4d195292ba0487223a6` SHA-256: `f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef` -来源:[`packages/llm/llm/src/types.ts:98`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:105`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -3500,7 +3508,7 @@ SHA-256: `f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef` SHA-256: `5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80` -来源:[`packages/llm/llm/src/types.ts:165`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:172`](../packages/llm/llm/src/types.ts) | kind | form 属性 | 其他必需字段 | 完整定义 | |---|---|---|---| @@ -3738,7 +3746,7 @@ SHA-256: `b0b0eb1b16490f6bee135b2377e8f8c39227974c799ad7d49ff418af23ecbc35` SHA-256: `12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7` -来源:[`packages/llm/llm/src/types.ts:79`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:86`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4046,7 +4054,7 @@ SHA-256: `326a9bfda27dda5882fb3cbe6e715df41dae2298fdae3d66437457268a665693` SHA-256: `5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca` -来源:[`packages/llm/llm/src/types.ts:41`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:48`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4093,7 +4101,7 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` | `step` | 必需 | `number` | | `turn` | 必需 | `number` | - + @@ -4101,15 +4109,15 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` ### `Message` -SHA-256: `4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d` +SHA-256: `c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36` 来源:[`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) 以下类型之一: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0) -- [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) +- [`DeveloperMessage`](#persistence-type-sha256-64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428) +- [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) - [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -4186,7 +4194,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` - `"negative"` - `"positive"` - + @@ -4194,7 +4202,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` ### `MessageSource` -SHA-256: `b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591` +SHA-256: `7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a` 来源:[`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) @@ -4306,7 +4314,7 @@ SHA-256: `b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591` | `tool-jobs` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | [`{ kind: "tool-jobs", form: "snapshot" }`](#persistence-type-sha256-c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49) | | `tool-jobs` | 未声明 | 无 | [`{ kind: "tool-jobs" }`](#persistence-type-sha256-24ce31aa528272e59f21cd26da9eca08968bcc97529756a749bfdab71746d9c8) | | `tool-registry` | 未声明 | 无 | [`{ kind: "tool-registry" }`](#persistence-type-sha256-8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543) | -| `user` | 未声明 | `rpcId`: `string` | [`{ kind: "user" }`](#persistence-type-sha256-6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def) | +| `user` | 未声明 | `rpcId`: `string` | [`{ kind: "user" }`](#persistence-type-sha256-a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302) | | `user` | 未声明 | 无 | [`CommandSource`](#persistence-type-sha256-ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05) | | `user-approval` | `"catalog"` | 无 | [`{ kind: "user-approval", form: "catalog" }`](#persistence-type-sha256-825ca23416b1ea9d81442afead5cf9229a6fa05842f042135cc3aa9710a6a2d8) | | `user-approval` | `"instructions"` | 无 | [`{ kind: "user-approval", form: "instructions" }`](#persistence-type-sha256-aa2bf9e82b9fdeb94a42af20031fffa30e4e23f01f4ac39757427d4c64c407f0) | @@ -4319,13 +4327,13 @@ SHA-256: `b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591` | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | | `working-directory` | 未声明 | 无 | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc` +SHA-256: `ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72` -[`Message`](#persistence-type-sha256-4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d) 的数组。 +[`Message`](#persistence-type-sha256-c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36) 的数组。 @@ -4443,22 +4451,6 @@ SHA-256: `1d9cb3caa96100b18b1911fa3ff8c751ef6992971a03c5f4d9aefc1a0004a345` [`PresentedFile`](#persistence-type-sha256-b8fc636a2121df9d8423c242e03c9d23b7ab7c6fdce00e45746932c60b730f97) 的数组。 - - - - - - -### `Principal` - -SHA-256: `deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5` - -来源:[`packages/typert/protocol/src/types.ts:457`](../packages/typert/protocol/src/types.ts) · [`packages/typert/protocol/src/types.ts:461`](../packages/typert/protocol/src/types.ts) - -| 属性 | 存在性 | 类型 | -|---|---|---| -| `kind` | 必需 | `"owner"` | - @@ -4513,7 +4505,7 @@ SHA-256: `2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f` SHA-256: `814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e` -来源:[`packages/llm/llm/src/types.ts:68`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:75`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4530,7 +4522,7 @@ SHA-256: `814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e` SHA-256: `0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555` -来源:[`packages/llm/llm/src/types.ts:448`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:474`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4709,7 +4701,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` | `source` | 必需 | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | 必需 | `string` | - + @@ -4717,15 +4709,15 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` ### `SessionTitleLlmRequestEventData` -SHA-256: `9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f` +SHA-256: `7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54` -来源:[`packages/session/session-title-llm/src/index.ts:37`](../packages/session/session-title-llm/src/index.ts) +来源:[`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `maxTokens` | 必需 | `number` | | `messageSeqs` | 必需 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc) | +| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72) | | `reasoningEffort` | 可选 | `string` | | `route` | 必需 | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | 必需 | `string` | @@ -4778,6 +4770,24 @@ SHA-256: `30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f` | `provider` | 未声明 | `provider`: `string` | [`{ kind: "provider" }`](#persistence-type-sha256-b81e9de81a481b24e1e1f9adf038bfc59e57b4f35ce62f8f1e45284186f4f812) | | `user` | 未声明 | 无 | [`CommandSource`](#persistence-type-sha256-ada310bf0bdb8fed51f3b56ea63f6ea6b18bbd587f04fccb63a14ab0b2a24e05) | + + + + + + +### `ShareViewerPrincipal` + +SHA-256: `4d60dddaa65d3da8d4cd014d85ce3d2c2875664dd0433e331d8e9d395289686b` + +来源:[`packages/experimental/liveshare/src/host/viewer-principal.ts:24`](../packages/experimental/liveshare/src/host/viewer-principal.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `actor` | 必需 | [`{ displayName?, providerId, subjectId, tenantId }`](#persistence-type-sha256-0a92c5bdfa06c2e2615b777728f3aa95f408d43b9012e7b754568311251db623) | +| `kind` | 必需 | `"share-viewer"` | +| `shareId` | 必需 | `string` | + @@ -4826,7 +4836,7 @@ SHA-256: `672c4aa2a9b736e3d6e5da6346d2e2ed5e3573f2d0baafa18e7407c9f3fee3db` SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` -来源:[`packages/llm/llm/src/types.ts:470`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:496`](../packages/llm/llm/src/types.ts) 以下类型之一: @@ -5079,7 +5089,7 @@ SHA-256: `b2d2a01e51341a3bfec809198952c8c830a7899f4cb6ecd3c1dc0b5d9a2af943` SHA-256: `4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c` -来源:[`packages/llm/llm/src/types.ts:62`](../packages/llm/llm/src/types.ts) · [`packages/web/web-search-deepseek/src/provider.ts:66`](../packages/web/web-search-deepseek/src/provider.ts) +来源:[`packages/llm/llm/src/types.ts:69`](../packages/llm/llm/src/types.ts) · [`packages/web/web-search-deepseek/src/provider.ts:66`](../packages/web/web-search-deepseek/src/provider.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5121,7 +5131,7 @@ SHA-256: `4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea` SHA-256: `fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394` -来源:[`packages/llm/llm/src/types.ts:175`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:182`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5142,7 +5152,7 @@ SHA-256: `fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394` SHA-256: `8e611bf46821e5003a1605a73560a6f56388295149d449dfe2142d5ec335f29c` -来源:[`packages/llm/llm/src/types.ts:115`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:122`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5160,7 +5170,7 @@ SHA-256: `8e611bf46821e5003a1605a73560a6f56388295149d449dfe2142d5ec335f29c` SHA-256: `c1f0f17b6493b85db6453bb30e01b208d703731b25bc1e68e5d4fc72eea9cdea` -来源:[`packages/llm/llm/src/types.ts:105`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:112`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5196,7 +5206,7 @@ SHA-256: `d9c462b9f62fdd3efc76a44b9524dc8781f6b91cd9c7e584b17d971defafebab` SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8` -来源:[`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:134`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5251,7 +5261,7 @@ SHA-256: `4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731` SHA-256: `66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2` -来源:[`packages/llm/llm/src/types.ts:491`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:517`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -5382,7 +5392,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` | `interrupted` | 未声明 | 无 | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | 未声明 | 无 | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5390,7 +5400,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` ### `UserMessage` -SHA-256: `8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14` +SHA-256: `4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce` 来源:[`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5399,19 +5409,19 @@ SHA-256: `8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"user"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. 仅表示归属的新增 kind:`action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292` +SHA-256: `7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3` -[`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) 的数组。 +[`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) 的数组。 @@ -5603,7 +5613,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -来源:[`packages/api/session-controller/src/types.ts:386`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +来源:[`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -5675,6 +5685,17 @@ SHA-256: `50cf02d875f8a1f1411f4a6fbcffab9120ccef5f74a47331fbf29480e5f68f48` - `"fork"` - `"fresh"` + + +### `union (2 variants)` + +SHA-256: `824a4aad44a54e245c67196817d67946734a2dc0eee0b0b6834a5b2a6ef352ce` + +| kind | form 属性 | 其他必需字段 | 完整定义 | +|---|---|---|---| +| `owner` | 未声明 | 无 | [`{ kind: "owner" }`](#persistence-type-sha256-deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5) | +| `share-viewer` | 未声明 | `actor`: [`{ displayName?, providerId, subjectId, tenantId }`](#persistence-type-sha256-0a92c5bdfa06c2e2615b777728f3aa95f408d43b9012e7b754568311251db623); `shareId`: `string` | [`ShareViewerPrincipal`](#persistence-type-sha256-4d60dddaa65d3da8d4cd014d85ce3d2c2875664dd0433e331d8e9d395289686b) | + ### `union (2 variants)` @@ -6200,6 +6221,21 @@ SHA-256: `119abe2b9c0b2f46ecb31d82b9acba4d4b30e049f655e2475e8b4ae12d5a9e25` | `point` | 必需 | `string` | | `turn` | 必需 | `number` | + + +### `{ displayName?, providerId, subjectId, tenantId }` + +SHA-256: `0a92c5bdfa06c2e2615b777728f3aa95f408d43b9012e7b754568311251db623` + +来源:[`packages/experimental/liveshare/src/host/viewer-principal.ts:29`](../packages/experimental/liveshare/src/host/viewer-principal.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `displayName` | 可选 | `string` | +| `providerId` | 必需 | `string` | +| `subjectId` | 必需 | `string` | +| `tenantId` | 必需 | `string` | + ### `{ end, start }` @@ -6262,7 +6298,7 @@ SHA-256: `5302e763151277614b97e77f00e3b326694c6c1247b916a20503c344dc970777` SHA-256: `8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0` -来源:[`packages/llm/llm/src/types.ts:160`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:167`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6275,7 +6311,7 @@ SHA-256: `8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0` SHA-256: `693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057` -来源:[`packages/llm/llm/src/types.ts:161`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:168`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6296,18 +6332,18 @@ SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585` | `reason` | 必需 | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | 可选 | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2` +SHA-256: `53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a` 来源:[`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `headerSeq` | 可选 | `number` | -| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0) | +| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428) | | `step` | 必需 | `number` | | `turn` | 必需 | `number` | @@ -6391,17 +6427,17 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3` |---|---|---| | `inherited` | 可选 | `true` | - + ### `{ inserted, outcome?, removedCount?, start, … }` -SHA-256: `c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c` +SHA-256: `d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401` 来源:[`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| -| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292) | +| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3) | | `outcome` | 可选 | `"canceled"` | | `removedCount` | 可选 | `number` | | `start` | 必需 | `number` | @@ -6871,7 +6907,7 @@ SHA-256: `eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c` SHA-256: `ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7` -来源:[`packages/core/session/src/types.ts:220`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:159`](../packages/llm/llm/src/types.ts) +来源:[`packages/core/session/src/types.ts:220`](../packages/core/session/src/types.ts) · [`packages/llm/llm/src/types.ts:166`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6955,6 +6991,18 @@ SHA-256: `52c6113cb0e53f345324bf84f01928bea5d5df57ee80bc0dd65407b8bc5a3734` | `kind` | 必需 | `"model-selection"` | | `sections` | 必需 | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | + + +### `{ kind: "owner" }` + +SHA-256: `deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5` + +来源:[`packages/typert/protocol/src/types.ts:457`](../packages/typert/protocol/src/types.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `kind` | 必需 | `"owner"` | + ### `{ kind: "parent" }` @@ -7311,7 +7359,7 @@ SHA-256: `28936c614bd4e7e2774806edcbb3cbfd3ee643ac1cbf0bbdff43d915505a8fe3` SHA-256: `1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f` -来源:[`packages/llm/llm/src/types.ts:157`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:164`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7479,7 +7527,7 @@ SHA-256: `49dab019d00e8c26f27ad2ccff7b604d69e323ae4d17e0d2a7b6b8a830b1ade2` SHA-256: `287211846beea1eccb5f9acee00d20388a38fcb2e95858fa2332d6f58529379a` -来源:[`packages/llm/llm/src/types.ts:158`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:165`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7653,19 +7701,19 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543` |---|---|---| | `kind` | 必需 | `"tool-registry"` | - + ### `{ kind: "user" }` -SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def` +SHA-256: `a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302` -来源:[`packages/api/session-controller/src/types.ts:395`](../packages/api/session-controller/src/types.ts) +来源:[`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `clientTimeZone` | 可选 | `string` | | `kind` | 必需 | `"user"` | -| `principal` | 可选 | [`Principal`](#persistence-type-sha256-deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5) | +| `principal` | 可选 | [`union (2 variants)`](#persistence-type-sha256-824a4aad44a54e245c67196817d67946734a2dc0eee0b0b6834a5b2a6ef352ce) | | `rpcId` | 必需 | `string` | @@ -7933,7 +7981,7 @@ SHA-256: `9a2a9029f8d7ede05336980d8342737557f487b913bfd28853d0ab5214600ab5` SHA-256: `8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52` -来源:[`packages/session/session-log-deepseek/src/types.ts:81`](../packages/session/session-log-deepseek/src/types.ts) +来源:[`packages/session/session-log-deepseek/src/types.ts:96`](../packages/session/session-log-deepseek/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -8048,17 +8096,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | 必需 | `number` | | `type` | 必需 | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30` +SHA-256: `cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c) | +| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -8151,7 +8199,7 @@ SHA-256: `1033093edd0db80ff410e00830b523405e00bb0c7684948e531ff65095799625` SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d` -来源:[`packages/llm/llm/src/types.ts:475`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:501`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -8165,7 +8213,7 @@ SHA-256: `d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d` SHA-256: `3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90` -来源:[`packages/llm/llm/src/types.ts:471`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:497`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -8299,17 +8347,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | 必需 | `number` | | `type` | 必需 | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f` +SHA-256: `70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2) | +| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8383,7 +8431,7 @@ SHA-256: `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` SHA-256: `a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1` -来源:[`packages/llm/llm/src/types.ts:477`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:503`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -8589,7 +8637,7 @@ SHA-256: `71bb3254ec8f1e491ce63b762a7b5f18709de23aa4e8abcc8b886f4b6ce051dd` SHA-256: `2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095` -来源:[`packages/llm/llm/src/types.ts:473`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:499`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -8709,17 +8757,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | 必需 | `number` | | `type` | 必需 | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5` +SHA-256: `2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f) | +| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -8909,7 +8957,7 @@ SHA-256: `e72269f931559e901e5e8fe1cfcbd0669fb795f43cc038ff39fba62f60403c33` SHA-256: `95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65` -来源:[`packages/llm/llm/src/types.ts:472`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:498`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -8957,7 +9005,7 @@ SHA-256: `3860b4757d512a2869be833acb0faf834335b1b056bf3f83df5474dce95f8fa6` SHA-256: `88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a` -来源:[`packages/llm/llm/src/types.ts:474`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:500`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -9135,24 +9183,24 @@ SHA-256: `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` -来源:[`packages/llm/llm/src/types.ts:476`](../packages/llm/llm/src/types.ts) +来源:[`packages/llm/llm/src/types.ts:502`](../packages/llm/llm/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `type` | 必需 | `"usage"` | | `usage` | 必需 | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f` +SHA-256: `5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) | +| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | diff --git a/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.i18n.yaml b/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.i18n.yaml new file mode 100644 index 0000000000..4202182528 --- /dev/null +++ b/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-13-liveshare-share-viewer-principal.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 docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-13-liveshare-share-viewer-principal: + en: 9b8097ab5922733e + zh: e52f33004c75f18f +/2026-10-13-liveshare-share-viewer-principal/summary: + en: 32551ff4ee358694 + zh: f6cfc403bed4f780 +/2026-10-13-liveshare-share-viewer-principal/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-13-liveshare-share-viewer-principal/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-13-liveshare-share-viewer-principal/compatibility: + en: 3c4faef3cc5010bb + zh: 051485cd767ec6bc +/2026-10-13-liveshare-share-viewer-principal/verification: + en: 427079a2393d0ffe + zh: 17249b4e0fc0faeb +/2026-10-13-liveshare-share-viewer-principal/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.md b/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.md new file mode 100644 index 0000000000..a22251e96d --- /dev/null +++ b/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.md @@ -0,0 +1,60 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-13-liveshare-share-viewer-principal + +English | [中文](2026-10-13-liveshare-share-viewer-principal.zh.md) + +## Summary + +Adds the LiveShare share-viewer principal, including the Relay-asserted display name when available, to the user-rpc message source of prompts a share reader submits. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-13-liveshare-share-viewer-principal +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-10-25-skill-catalog-incomplete" + after: "cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e" + decision: same-version + - root: "event:developer/message" + previous: "2026-10-25-skill-catalog-incomplete" + after: "70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-10-25-skill-catalog-incomplete" + after: "2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c" + decision: same-version + - root: "event:user/message" + previous: "2026-10-25-skill-catalog-incomplete" + after: "5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451" + decision: same-version +``` + + +## Compatibility + +The share-viewer variant is written only for a prompt that a LiveShare share reader's Peer submits; owner prompts still omit principal, so their sources and every existing Session log are unchanged. The value carries the share id and the Relay-asserted provider, tenant, and subject ids. It also carries an optional display name when the Relay asserts one, for attribution only; credentials and avatars are never persisted, and authorization still uses the identity triple. Readers that do not know the optional field keep showing the durable subject id. The LiveShare host consumes it in two places: the `agent/pre-step` admission check re-reads the Relay's grant for every entering message whose principal is a share viewer, and rejects the step when that grant no longer holds; the same step records, in memory for that turn only, that the running turn was viewer-started, which confines that turn to the shared tool surface and leaves an owner-started turn on the Session's native tools. That marker is never appended to the Session log and is recomputed from the next admitted step. A build without LiveShare keeps the message an ordinary user message because the kind stays 'user', and a principal kind this package does not drive is left to whoever drives it. A current reader of an older log finds no share-viewer principal and admits those prompts as ordinary input, as before. The field is not part of any model request. SESSION_FORMAT_VERSION does not change. + + +## Verification + +pnpm exec vitest run packages/experimental/liveshare/tests/relay-target-admission.host.spec.ts packages/experimental/liveshare/tests/relay-pre-step-admission.host.spec.ts packages/experimental/liveshare/tests/share-host-listeners.spec.ts packages/experimental/liveshare packages/experimental/liveshare-protocol: a share reader's prompt persists source.principal in agent/inbox/spliced, recovered reader input is refused at the `agent/pre-step` admission check, an admitted one enters the model, and a principal this package does not drive is left alone; the optional displayName is persisted for attribution, accepted only as a non-empty string, and missing or malformed values fall back to the subject id; 35 tests passed plus the two-package LiveShare and protocol suites. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.schema.json b/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.schema.json new file mode 100644 index 0000000000..dcce773ee6 --- /dev/null +++ b/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.schema.json @@ -0,0 +1,253 @@ +{ + "formatVersion": 2, + "roots": [ + {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e"}, + {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f"}, + {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c"}, + {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451"} + ], + "types": [], + "nodes": { + "0203243b72fc867a": {"kind":"object","properties":[{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "022623b9ef98ec47": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "02b2c23f2d94b028": {"kind":"object","properties":[{"name":"kind","type":"c5687b1313206a6f","optional":false}],"indices":[]}, + "05205074b24d8340": {"kind":"literal","value":"action-cards"}, + "0657d3ca5ac4e7bf": {"kind":"primitive","type":"number"}, + "07e1a58c58b593f5": {"kind":"object","properties":[{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "09655b34d7f3cf1e": {"kind":"literal","value":"system-prompt"}, + "09f3f8dc417f2782": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"8af841fc9b265ade","optional":false},{"name":"source","type":"65a8400e451e667e","optional":false}],"indices":[]}, + "0a92c5bdfa06c2e2": {"kind":"object","properties":[{"name":"displayName","type":"4cd48f3e5108bd6e","optional":true},{"name":"providerId","type":"4cd48f3e5108bd6e","optional":false},{"name":"subjectId","type":"4cd48f3e5108bd6e","optional":false},{"name":"tenantId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "0ed8d1108fe7a5e1": {"kind":"object","properties":[{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "0eeb17e1df89b1eb": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "0f46153645c29784": {"kind":"object","properties":[{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "0fe52387f6505270": {"kind":"literal","value":"plan-mode"}, + "11638dc58a31496f": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "12d5808fac1996b9": {"kind":"object","properties":[{"name":"attachment","type":"b0b0eb1b16490f6b","optional":false},{"name":"offloaded","type":"dacf3d5f3e4ff619","optional":true},{"name":"type","type":"fb8b6a2e53cb9cf4","optional":false}],"indices":[]}, + "1379f0b72275994d": {"kind":"literal","value":"developer/message"}, + "16d0507cb94b4cfc": {"kind":"literal","value":"relay"}, + "1922cd79a70806c1": {"kind":"literal","value":"skill-invocation"}, + "1938c8c20f1cba7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "1e709346005826eb": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "1f0133217eeeffd8": {"kind":"literal","value":"tool-call"}, + "1f1385dbc3e875c0": {"kind":"literal","value":"repeat-tool-reminder"}, + "1fdad624334d3548": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "2152caf1a71e588e": {"kind":"object","properties":[{"name":"kind","type":"2e4debca12f2c449","optional":false},{"name":"messageId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderName","type":"4cd48f3e5108bd6e","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "2235d957bf4b7c6c": {"kind":"object","properties":[{"name":"kind","type":"7b1ce60193fa8b69","optional":false}],"indices":[]}, + "224aa11d4d5f88c4": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "23af3759678a30de": {"kind":"literal","value":"dsh-session-title-llm"}, + "24b10f7cdae64928": {"kind":"literal","value":"runtime-context"}, + "24ce31aa528272e5": {"kind":"object","properties":[{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "260523b412c19ad8": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "2765e9119edf5816": {"kind":"literal","value":"model"}, + "277b61f9c6f81704": {"kind":"literal","value":"file"}, + "28936c614bd4e7e2": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "290f32bfd81ff85a": {"kind":"object","properties":[{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "297fbd8067d7815a": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "2aad1179f9576314": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "2ab12a0e345658f9": {"kind":"array","element":"83da72c6857601c7"}, + "2e4debca12f2c449": {"kind":"literal","value":"team-message"}, + "2e505098f9209e52": {"kind":"object","properties":[{"name":"data","type":"7ff62f6599c083ce","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, + "2f1e6a7ebace43d7": {"kind":"union","types":["3f74ba74acd10644","58f94390b982f8a5","de30b1fa12c75f81","ebb768d85ec87fb6"]}, + "2f3f201c8846e19c": {"kind":"literal","value":"tool-goal"}, + "312aee2f0c42aabb": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "31907de956375182": {"kind":"object","properties":[{"name":"compactionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"6b23ed9ce1b397e6","optional":false},{"name":"sourceCommandId","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, + "3247e566f4340029": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "335e242de1fcc17b": {"kind":"union","types":["bcf0caf62d964b2f","e438a9d4fb883b3e"]}, + "383cf73ae4748e5b": {"kind":"literal","value":"recall"}, + "3b04790e08b35241": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "3b4f2c9ef2c1217e": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "3bd652ebfa8726b3": {"kind":"union","types":["0657d3ca5ac4e7bf","938f7c7357bc403e"]}, + "3bdb8dc6989655b0": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "3c2535ca0a2062f1": {"kind":"literal","value":"reasoning"}, + "3e2ece4967cfebe9": {"kind":"literal","value":"next-step"}, + "3f74ba74acd10644": {"kind":"literal","value":"image/jpeg"}, + "4035903993dfee62": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"7e9f1072c17f7aa8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "408927267422a7b2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "43e311235aa537df": {"kind":"object","properties":[{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"83d6916021eede8f","optional":false}],"indices":[]}, + "45f005e6bbb76452": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "4991dfc415c9efeb": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "49dab019d00e8c26": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "4a9bef9f27a94252": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"82470020b5bfcb1a","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4b259f7f8f0a44ad": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"isError","type":"aa93010d0e521ad7","optional":true},{"name":"role","type":"a13d8f379222ece8","optional":false},{"name":"source","type":"d9c462b9f62fdd3e","optional":false},{"name":"toolCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4cd48f3e5108bd6e": {"kind":"primitive","type":"string"}, + "4d60dddaa65d3da8": {"kind":"object","properties":[{"name":"actor","type":"0a92c5bdfa06c2e2","optional":false},{"name":"kind","type":"6ba7edb6447b6a3e","optional":false},{"name":"shareId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4e44c9118152f682": {"kind":"literal","value":"text"}, + "4e88776858652856": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"4e44c9118152f682","optional":false}],"indices":[]}, + "4e97c3d85c0fc817": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bf16d2e98a7f1389","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4edec1b770e8ec4c": {"kind":"literal","value":"ptc-mode"}, + "4f91f187baeabef5": {"kind":"object","properties":[{"name":"capturedFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"capturedThroughSeq","type":"3bd652ebfa8726b3","optional":false},{"name":"compacted","type":"aa93010d0e521ad7","optional":false},{"name":"inputIndex","type":"0657d3ca5ac4e7bf","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"omittedBytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"omittedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"originalMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retainedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"truncated","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, + "52c6113cb0e53f34": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "53cf1704ae0709c7": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "53d4f8256bc58109": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"64ef542b1bfc9883","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "5654685d7557ec74": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "58015da1c1950a66": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "587bb11da730e06a": {"kind":"literal","value":"session/title-llm-request"}, + "589d0d91239681f8": {"kind":"literal","value":"developer"}, + "58f94390b982f8a5": {"kind":"literal","value":"image/png"}, + "5bb231bc5afdb532": {"kind":"object","properties":[{"name":"data","type":"4035903993dfee62","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, + "5bb69b88517f6214": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "5bc66269cfaf614c": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "5cdbf342b7748d15": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "5d03ba38734809bc": {"kind":"array","element":"0657d3ca5ac4e7bf"}, + "5d7a64ed6aac5551": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "6085488d223aeffd": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "6292bcbedaa97fea": {"kind":"object","properties":[{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "62ecb62dc0fe1b5e": {"kind":"literal","value":"next-turn"}, + "64ef542b1bfc9883": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"7e9f1072c17f7aa8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "65a8400e451e667e": {"kind":"object","properties":[{"name":"kind","type":"09655b34d7f3cf1e","optional":false}],"indices":[]}, + "65d583e66518c523": {"kind":"object","properties":[{"name":"entries","type":"aba175fb20ffc711","optional":false},{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"incomplete","type":"dacf3d5f3e4ff619","optional":true},{"name":"kind","type":"6ebcc2dbec9f2dab","optional":false},{"name":"update","type":"dacf3d5f3e4ff619","optional":true}],"indices":[]}, + "672c4aa2a9b736e3": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1922cd79a70806c1","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "68f44f59e91c4139": {"kind":"literal","value":"user"}, + "6b23ed9ce1b397e6": {"kind":"literal","value":"compact-checkpoint"}, + "6ba7edb6447b6a3e": {"kind":"literal","value":"share-viewer"}, + "6ccb6bc423fb3952": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "6ea608bdc8a7a63d": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "6ebcc2dbec9f2dab": {"kind":"literal","value":"skill-catalog"}, + "6f22a9d02b9a1f11": {"kind":"object","properties":[{"name":"baseline","type":"dacf3d5f3e4ff619","optional":true},{"name":"baselineIdentity","type":"4cd48f3e5108bd6e","optional":true},{"name":"changes","type":"2ab12a0e345658f9","optional":false},{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"fbd2c74b346a1595","optional":false}],"indices":[]}, + "6f44f32be9bcd9a8": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "6f9e00383836275e": {"kind":"literal","value":"snapshot"}, + "70d2acdec1ebcfea": {"kind":"object","properties":[{"name":"data","type":"53d4f8256bc58109","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, + "719c51f67074f970": {"kind":"object","properties":[{"name":"kind","type":"2765e9119edf5816","optional":false},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"replayState","type":"bbc5696e3f13c758","optional":true}],"indices":[]}, + "7382c4fc95fe8c5d": {"kind":"literal","value":"remove"}, + "7554565be416b034": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "756b5e23a5cb2c7f": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "78a9be340090b735": {"kind":"literal","value":"tmux-context"}, + "7b1ce60193fa8b69": {"kind":"literal","value":"working-directory"}, + "7c203f3d4dfa6638": {"kind":"array","element":"4035903993dfee62"}, + "7df62978a2b7de85": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "7e9f1072c17f7aa8": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","a4afec59b75aad5d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, + "7eaef7a763ba5b94": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "7f13860ee89a2d35": {"kind":"literal","value":"session-reference"}, + "7f959241c24128b4": {"kind":"object","properties":[{"name":"goalId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"b815d7b91cdd9fc6","optional":false},{"name":"revision","type":"0657d3ca5ac4e7bf","optional":false},{"name":"round","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "7ff62f6599c083ce": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"ff2cbb53582d1827","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8064c03c87c357fd": {"kind":"union","types":["12d5808fac1996b9","43e311235aa537df","4e88776858652856","814d9434e1ddf907","8e611bf46821e500","c1f0f17b6493b85d","f2da0f27c1e6add5"]}, + "80677c8c8b804189": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "8067b0d232c350f1": {"kind":"literal","value":"time-context"}, + "814d9434e1ddf907": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"3c2535ca0a2062f1","optional":false}],"indices":[]}, + "82470020b5bfcb1a": {"kind":"literal","value":"agent-message"}, + "824a4aad44a54e24": {"kind":"union","types":["4d60dddaa65d3da8","deebfc7c62177ea8"]}, + "825ca23416b1ea9d": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "829a2fb3be6efd4b": {"kind":"literal","value":"agent/inbox/spliced"}, + "83079c8a3f733ac3": {"kind":"object","properties":[{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8338ee303e2bd448": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "83d6916021eede8f": {"kind":"literal","value":"tool-removal"}, + "83da72c6857601c7": {"kind":"object","properties":[{"name":"action","type":"904bf499d98be4a3","optional":false},{"name":"digest","type":"4cd48f3e5108bd6e","optional":true},{"name":"path","type":"4cd48f3e5108bd6e","optional":false},{"name":"scope","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "854db3efdfb0cd39": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "85d6d96ff8842974": {"kind":"literal","value":"answered"}, + "85f9b505b7bdff1f": {"kind":"literal","value":"tool-addition"}, + "85fe96bf15d5c755": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "8620189c8e335827": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "86e1e384495ee09a": {"kind":"object","properties":[{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "87b1a33564b9a4d6": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "88841202942c7683": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "8a046b52331959aa": {"kind":"object","properties":[{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "8af841fc9b265ade": {"kind":"literal","value":"system"}, + "8c1c4f72755a78ea": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "8c627d7a45fa406c": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8e611bf46821e500": {"kind":"object","properties":[{"name":"tool","type":"fcf427290c6213c2","optional":true},{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"85f9b505b7bdff1f","optional":false}],"indices":[]}, + "8f324cea3916c051": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "8fcb0b44a575e998": {"kind":"object","properties":[{"name":"kind","type":"a720757f704a2512","optional":false}],"indices":[]}, + "904bf499d98be4a3": {"kind":"union","types":["7382c4fc95fe8c5d","b765ee31033cea46","f6cb462f808309e3"]}, + "938f7c7357bc403e": {"kind":"primitive","type":"null"}, + "95a3ffccbf3c8afc": {"kind":"literal","value":"owner"}, + "95c0e88fcf0bafb2": {"kind":"literal","value":"assistant"}, + "9645cf93769a6dfb": {"kind":"literal","value":"user-question-reply"}, + "96bfb916a727cd44": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "96f4a9c81fc0f940": {"kind":"union","types":["3e2ece4967cfebe9","62ecb62dc0fe1b5e"]}, + "99135d8a39379779": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "99e7e7d7fed55ae6": {"kind":"literal","value":"tool-jobs"}, + "99f0e98184709ad1": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "9ac52f0f84fec2eb": {"kind":"literal","value":"model-selection"}, + "9edc162949abf93b": {"kind":"object","properties":[{"name":"deliveryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"db6c5246911d47a7","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"ruleId","type":"4cd48f3e5108bd6e","optional":false},{"name":"source","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "9f65c43e340bba31": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "a13d8f379222ece8": {"kind":"literal","value":"tool"}, + "a15a19ef63cf5d1b": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "a16e567c8cd2c55c": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "a2066bd52f61218b": {"kind":"object","properties":[{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "a3c6f1ea38e9a61d": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "a4afec59b75aad5d": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"68f44f59e91c4139","optional":false},{"name":"principal","type":"824a4aad44a54e24","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "a720757f704a2512": {"kind":"literal","value":"tool-registry"}, + "a9059a81430b3207": {"kind":"literal","value":"hooks-claude-code"}, + "aa2bf9e82b9fdeb9": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "aa93010d0e521ad7": {"kind":"primitive","type":"boolean"}, + "aae343bc3d663c83": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "aba175fb20ffc711": {"kind":"array","element":"83079c8a3f733ac3"}, + "ada310bf0bdb8fed": {"kind":"object","properties":[{"name":"kind","type":"68f44f59e91c4139","optional":false}],"indices":[]}, + "ade057f4fdc387ae": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "aec150797b655c5f": {"kind":"literal","value":"catalog"}, + "b062078e899958f9": {"kind":"object","properties":[{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "b0b0eb1b16490f6b": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mediaType","type":"2f1e6a7ebace43d7","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"originalDimensions","type":"0f46153645c29784","optional":true},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b2a5c8c008a31e3d": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "b3681c026a0b98e9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "b4ec0af39fd52ed9": {"kind":"object","properties":[{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "b52f215a6cd69105": {"kind":"array","element":"8064c03c87c357fd"}, + "b613f83572fe2fbf": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b6f94c17dd9bbd7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "b70f3e81fe11b7a5": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "b765ee31033cea46": {"kind":"literal","value":"set"}, + "b79641a9d25e7566": {"kind":"object","properties":[{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "b815d7b91cdd9fc6": {"kind":"literal","value":"goal"}, + "ba2a1a8f194b1bb2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "ba4c40b4381e0dcd": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "baccc97ec34be6c4": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "bb94f3217aa56c88": {"kind":"literal","value":"hooks-codex"}, + "bbc5696e3f13c758": {"kind":"opaque","reason":"unknown"}, + "bcf0caf62d964b2f": {"kind":"object","properties":[{"name":"endSeq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"op","type":"f6cb462f808309e3","optional":false},{"name":"startSeq","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "bf16d2e98a7f1389": {"kind":"literal","value":"subagent-settled"}, + "c1f0f17b6493b85d": {"kind":"object","properties":[{"name":"arguments","type":"4cd48f3e5108bd6e","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"1f0133217eeeffd8","optional":false}],"indices":[]}, + "c5228b7a55498d84": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "c55827fba7018198": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"9645cf93769a6dfb","optional":false},{"name":"outcome","type":"85d6d96ff8842974","optional":false}],"indices":[]}, + "c5687b1313206a6f": {"kind":"literal","value":"cordis-host-runner"}, + "c748fb42c80a1200": {"kind":"array","element":"86e1e384495ee09a"}, + "c78b9097bfae83cd": {"kind":"union","types":["09f3f8dc417f2782","4035903993dfee62","4b259f7f8f0a44ad","64ef542b1bfc9883","fa87537f4a9c1e25"]}, + "cb1c8959e1726f82": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "cda2345a9be9d139": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "cdf988172b589b23": {"kind":"object","properties":[{"name":"data","type":"d4d960185026266e","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, + "d4d960185026266e": {"kind":"object","properties":[{"name":"inserted","type":"7c203f3d4dfa6638","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, + "d4e4d575abeb4b72": {"kind":"literal","value":"canceled"}, + "d85590a5675c6e2f": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "d9c462b9f62fdd3e": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"a13d8f379222ece8","optional":false}],"indices":[]}, + "d9edf1862e4471d6": {"kind":"literal","value":"schedule"}, + "dacf3d5f3e4ff619": {"kind":"literal","value":true}, + "db06ef35b27a3842": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "db6c5246911d47a7": {"kind":"literal","value":"webhook"}, + "dba54c09c6e7aafc": {"kind":"object","properties":[{"name":"kind","type":"4edec1b770e8ec4c","optional":false}],"indices":[]}, + "dd00cdccd0ba487b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "dd5ee8762bd8a14b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "dd6cc2bb2a8f2824": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "dde0cdf9d426768b": {"kind":"object","properties":[{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "de25352d2dc41963": {"kind":"literal","value":"user/message"}, + "de30b1fa12c75f81": {"kind":"literal","value":"image/gif"}, + "deaae5cd30987d4f": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "deebfc7c62177ea8": {"kind":"object","properties":[{"name":"kind","type":"95a3ffccbf3c8afc","optional":false}],"indices":[]}, + "df843efd87c78cf8": {"kind":"array","element":"4f91f187baeabef5"}, + "e438a9d4fb883b3e": {"kind":"literal","value":"append"}, + "e7f4cbb3a57f4cbd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "e8de0f457fed5b9f": {"kind":"literal","value":"notice"}, + "ebb768d85ec87fb6": {"kind":"literal","value":"image/webp"}, + "edbbfff1128099fd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "ee577a0cc18664c9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "f19bee84bf3d2090": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "f230c0b583e23b25": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "f29d63336e8266d8": {"kind":"object","properties":[{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "f2da0f27c1e6add5": {"kind":"object","properties":[{"name":"attachment","type":"f19bee84bf3d2090","optional":false},{"name":"type","type":"277b61f9c6f81704","optional":false}],"indices":[]}, + "f4a66f654996c093": {"kind":"object","properties":[{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "f56c5b59ca016131": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"7f13860ee89a2d35","optional":false},{"name":"references","type":"df843efd87c78cf8","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, + "f6329fb30c7ea801": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "f6605d6c57933aea": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "f68c335306535a00": {"kind":"literal","value":"user-approval"}, + "f6afe41e3926f422": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "f6cb462f808309e3": {"kind":"literal","value":"replace"}, + "f7283223aacdc39b": {"kind":"object","properties":[{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "f88f1b7b6bb733f6": {"kind":"literal","value":1}, + "f97f47b0eb87905e": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "fa87537f4a9c1e25": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"95c0e88fcf0bafb2","optional":false},{"name":"source","type":"719c51f67074f970","optional":false}],"indices":[]}, + "fb8b6a2e53cb9cf4": {"kind":"literal","value":"image"}, + "fbd2c74b346a1595": {"kind":"literal","value":"agent-instructions"}, + "fcf427290c6213c2": {"kind":"primitive","type":"never"}, + "fcf81d3c58c13593": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "fe1a7acdeed4d0f5": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "fee1caf8ab6a65c1": {"kind":"literal","value":"instructions"}, + "ff2cbb53582d1827": {"kind":"array","element":"c78b9097bfae83cd"} + } +} diff --git a/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.zh.md b/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.zh.md new file mode 100644 index 0000000000..7250b05ac2 --- /dev/null +++ b/docs/persistence-changes/2026-10-13-liveshare-share-viewer-principal.zh.md @@ -0,0 +1,60 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-13-liveshare-share-viewer-principal + +[English](2026-10-13-liveshare-share-viewer-principal.md) | 中文 + +## 概述 + +为分享读者提交的提示词,在 user-rpc 消息来源中加入 LiveShare 的 share-viewer principal;Relay 断言了显示名时一并保存。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-13-liveshare-share-viewer-principal +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-10-25-skill-catalog-incomplete" + after: "cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e" + decision: same-version + - root: "event:developer/message" + previous: "2026-10-25-skill-catalog-incomplete" + after: "70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-10-25-skill-catalog-incomplete" + after: "2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c" + decision: same-version + - root: "event:user/message" + previous: "2026-10-25-skill-catalog-incomplete" + after: "5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451" + decision: same-version +``` + + +## 兼容性 + +只有 LiveShare 分享读者的 Peer 提交提示词时才写入 share-viewer 变体;所有者的提示词仍不写 principal,因此其来源与所有已有会话日志都不变。该值含分享 ID 和 Relay 断言的 provider、tenant、subject ID;Relay 断言了显示名时还包含可选的 displayName,仅供展示。凭据与头像永不持久化,授权仍只使用身份三元组;不认识该可选字段的读取方继续显示持久化的 subject id。LiveShare Host 在两个地方消费它:`agent/pre-step` 准入校验会为每条带分享查看者 principal 的进入消息重新读取 Relay 授权,授权不再成立时拒绝该步骤;同一步骤还会在当前轮次的内存状态中记录该轮次由查看者发起,使该轮次只运行共享工具面,而所有者自己发起的轮次保留会话原生工具。该标记不写入会话日志,并从下一个准入步骤重新计算。未装 LiveShare 的构建因 kind 仍为 'user' 而照常把它当作普通用户消息;本包不驱动的 principal 种类交由驱动它的插件处理。当前读取方读取旧日志时找不到 share-viewer principal,与以往一样把这些提示词当作普通输入放行。该字段不进入任何模型请求。SESSION_FORMAT_VERSION 不变。 + + +## 验证 + +pnpm exec vitest run packages/experimental/liveshare/tests/relay-target-admission.host.spec.ts packages/experimental/liveshare/tests/relay-pre-step-admission.host.spec.ts packages/experimental/liveshare/tests/share-host-listeners.spec.ts packages/experimental/liveshare packages/experimental/liveshare-protocol:分享读者的提示词在 agent/inbox/spliced 中持久化 source.principal,恢复的读者输入在 `agent/pre-step` 准入校验中被拒绝,已准入的输入进入模型,本包不驱动的 principal 种类不受影响;可选 displayName 仅用于展示,只接受非空字符串,缺失或非法值回退 subject id;35 个测试通过,另有 LiveShare 与协议两个包的全量测试。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/historical-formats/README.i18n.yaml b/docs/persistence-changes/historical-formats/README.i18n.yaml index 40d842e063..e9e8287e17 100644 --- a/docs/persistence-changes/historical-formats/README.i18n.yaml +++ b/docs/persistence-changes/historical-formats/README.i18n.yaml @@ -15,8 +15,8 @@ en: 14c57f30e676f6e3 zh: b1a0ccf26fab3727 /session-persistence-formats/format-references: - en: e3511a4ac3bc4457 - zh: 828f3c39f80d5370 + en: 5912b107ada0bfc4 + zh: b997083d6fed3f07 /session-persistence-formats/scope-and-evidence: en: b220062139ddac1e zh: 7c4c0efe05dafa9c diff --git a/docs/persistence-changes/historical-formats/README.md b/docs/persistence-changes/historical-formats/README.md index 74c575b300..e0a594594d 100644 --- a/docs/persistence-changes/historical-formats/README.md +++ b/docs/persistence-changes/historical-formats/README.md @@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant. | 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 622 | +| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 626 | diff --git a/docs/persistence-changes/historical-formats/README.zh.md b/docs/persistence-changes/historical-formats/README.zh.md index 12eb600771..4202dfcc4e 100644 --- a/docs/persistence-changes/historical-formats/README.zh.md +++ b/docs/persistence-changes/historical-formats/README.zh.md @@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式 | 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 622 | +| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 626 | diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index c6c15a7c90..aa46bc962e 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -5,7 +5,7 @@ {"key":"JsonlHeaderLine","kind":"header","digest":"18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f"}, {"key":"SessionEventEnvelope","kind":"envelope","digest":"5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc"}, {"key":"event:agent-preset/selected","kind":"event","event":"agent-preset/selected","surface":false,"digest":"a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b"}, - {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30"}, + {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e"}, {"key":"event:approval/asked","kind":"event","event":"approval/asked","surface":false,"digest":"3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1"}, {"key":"event:approval/decided","kind":"event","event":"approval/decided","surface":false,"digest":"bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3"}, {"key":"event:approval/policy","kind":"event","event":"approval/policy","surface":false,"digest":"26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78"}, @@ -18,7 +18,7 @@ {"key":"event:compaction/start","kind":"event","event":"compaction/start","surface":false,"digest":"db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965"}, {"key":"event:compaction/summary","kind":"event","event":"compaction/summary","surface":false,"digest":"e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602"}, {"key":"event:deliverables/presented","kind":"event","event":"deliverables/presented","surface":false,"digest":"13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176"}, - {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f"}, + {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f"}, {"key":"event:feedback/message-delete","kind":"event","event":"feedback/message-delete","surface":false,"digest":"3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796"}, {"key":"event:feedback/message-put","kind":"event","event":"feedback/message-put","surface":false,"digest":"b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf"}, {"key":"event:feedback/record","kind":"event","event":"feedback/record","surface":false,"digest":"b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8"}, @@ -40,7 +40,7 @@ {"key":"event:session-log-deepseek/delivery-accepted","kind":"event","event":"session-log-deepseek/delivery-accepted","surface":false,"digest":"d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9"}, {"key":"event:session/end-seed","kind":"event","event":"session/end-seed","surface":false,"digest":"5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e"}, {"key":"event:session/title","kind":"event","event":"session/title","surface":false,"digest":"1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5"}, - {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5"}, + {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c"}, {"key":"event:step/end","kind":"event","event":"step/end","surface":false,"digest":"e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05"}, {"key":"event:step/start","kind":"event","event":"step/start","surface":false,"digest":"4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986"}, {"key":"event:subagent/catalog","kind":"event","event":"subagent/catalog","surface":false,"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac"}, @@ -62,7 +62,7 @@ {"key":"event:tool/result","kind":"event","event":"tool/result","surface":true,"digest":"7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397"}, {"key":"event:turn/end","kind":"event","event":"turn/end","surface":false,"digest":"0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce"}, {"key":"event:turn/start","kind":"event","event":"turn/start","surface":false,"digest":"aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8"}, - {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f"}, + {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451"}, {"key":"event:web/deepseek-search-llm-request","kind":"event","event":"web/deepseek-search-llm-request","surface":false,"digest":"0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52"}, {"key":"event:working-directory/change","kind":"event","event":"working-directory/change","surface":false,"digest":"124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3"}, {"key":"event:workspace/changes","kind":"event","event":"workspace/changes","surface":false,"digest":"e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72"} @@ -84,8 +84,9 @@ {"digest":"09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363","names":["packages/llm/llm/src/message.ts#SystemMessage"],"sources":["packages/llm/llm/src/message.ts:151"]}, {"digest":"0a30c3be336cdd4f8f34e66543a3d46d611c43986edd9b3ce6cbb9b721d3b5d3","names":[],"sources":["packages/core/session/src/types.ts:209"]}, {"digest":"0a8a062bc9c3c0d97fd97742262f2ea53dd32297f834e81114870d38ac3dc6da","names":[],"sources":[]}, + {"digest":"0a92c5bdfa06c2e2615b777728f3aa95f408d43b9012e7b754568311251db623","names":[],"sources":["packages/experimental/liveshare/src/host/viewer-principal.ts:29"]}, {"digest":"0aa62ab51b36c0e64b134bd03ba8725d758744180a1a447da49beaad194f219f","names":[],"sources":[]}, - {"digest":"0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555","names":["packages/llm/llm/src/types.ts#ReplayEnvelope"],"sources":["packages/llm/llm/src/types.ts:448"]}, + {"digest":"0acd94fe8794574c9ae72c7855ee16d29c546096d4af760d16d1225385764555","names":["packages/llm/llm/src/types.ts#ReplayEnvelope"],"sources":["packages/llm/llm/src/types.ts:474"]}, {"digest":"0af3ecfb8c60d416fc40a7cc4fb2c4851a8e46608b2b38ab9e510670c971aa1f","names":[],"sources":[]}, {"digest":"0b6d718c693fdf7f06dcccb0bcbbabe78cc63514eafc62d6b2a3899735279d6f","names":["packages/goal/goal/src/types.ts#GoalRef"],"sources":["packages/goal/goal/src/types.ts:20"]}, {"digest":"0b9b8facbe2c19773b3fb0ecc824ccbcc603d41b8f18069faa3807d449572fce","names":[],"sources":[]}, @@ -108,8 +109,7 @@ {"digest":"11f70b595aa76897fc4e25a352bfa718c659f44b799e5aa14e84e5a38fe72912","names":[],"sources":["packages/interaction/user-approval/src/index.ts:41"]}, {"digest":"124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3","names":[],"sources":[]}, {"digest":"12a0cfa619ac4a5fc6c3e8e359c8dae15769edc61497a575935c78d5842f8e42","names":[],"sources":[]}, - {"digest":"12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7","names":["packages/llm/llm/src/types.ts#ImageBlock"],"sources":["packages/llm/llm/src/types.ts:79"]}, - {"digest":"12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2","names":[],"sources":["packages/core/session/src/types.ts:317"]}, + {"digest":"12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7","names":["packages/llm/llm/src/types.ts#ImageBlock"],"sources":["packages/llm/llm/src/types.ts:86"]}, {"digest":"1379f0b72275994d20f1ddca2d60a96ab16ec80397ca9f1af21bdc67ba5c269c","names":[],"sources":[]}, {"digest":"13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176","names":[],"sources":[]}, {"digest":"14230f9b8b5797bb7e8c621fa636531cfbde2ffba1ef5f1fd058216ff21952a0","names":[],"sources":[]}, @@ -139,7 +139,7 @@ {"digest":"1ccb56061942f5ccb0d30c9e4f2883b9d115ff906560029de637b61d3cde896d","names":[],"sources":["packages/core/session/src/types.ts:349"]}, {"digest":"1cde1bedde79c5e8a56aee9c05c8e0a579cf5bc611deb85b93fa331a16ca978f","names":["packages/compaction/compaction-image-offload/src/projection.ts#ImageOffloadTarget"],"sources":["packages/compaction/compaction-image-offload/src/projection.ts:9"]}, {"digest":"1d5f04a03ea75fc8eeece544129eba0f49f0c82046e9c404461920390135c7b2","names":[],"sources":[]}, - {"digest":"1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f","names":[],"sources":["packages/llm/llm/src/types.ts:157"]}, + {"digest":"1d76406cbb2ddadb0dbbb51e9d8bc6cf678fed455cf8eb512f36f7c86d37a62f","names":[],"sources":["packages/llm/llm/src/types.ts:164"]}, {"digest":"1d8798b1a5e8c545ee4dcd2b4e75c0b4c9ffd985e4744c6d1a1aab992f859721","names":[],"sources":[]}, {"digest":"1d9cb3caa96100b18b1911fa3ff8c751ef6992971a03c5f4d9aefc1a0004a345","names":[],"sources":[]}, {"digest":"1df34a6c086866ffdc2dd9e8e5c581c3751a010cdf6b8538a7327f7a3237f0b4","names":["packages/goal/goal/src/domain.ts#GoalSnapshotChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:24"]}, @@ -169,13 +169,13 @@ {"digest":"26c879bdd130e8b8683a6ce588414fad2244783d6573ca399fa0de184a2b0bb1","names":[],"sources":[]}, {"digest":"2765e9119edf5816ac1ab4c95c5ee32c90403f3742962eff11544f7752d59d2e","names":[],"sources":[]}, {"digest":"277b61f9c6f8170425fa6ffe06cfa14376757a896a4749cb82a687d7364c4fc9","names":[],"sources":[]}, - {"digest":"287211846beea1eccb5f9acee00d20388a38fcb2e95858fa2332d6f58529379a","names":[],"sources":["packages/llm/llm/src/types.ts:158"]}, + {"digest":"287211846beea1eccb5f9acee00d20388a38fcb2e95858fa2332d6f58529379a","names":[],"sources":["packages/llm/llm/src/types.ts:165"]}, {"digest":"28936c614bd4e7e2774806edcbb3cbfd3ee643ac1cbf0bbdff43d915505a8fe3","names":[],"sources":[]}, {"digest":"290f32bfd81ff85a8095886a8066fae06e885942b63a8bcd6a8d66d328445808","names":[],"sources":[]}, {"digest":"297fbd8067d7815adae02b89790ec6ff5fb7c2cb0d0eb026e1746c24ec857761","names":[],"sources":[]}, {"digest":"2aad1179f957631477b582e57cd8f538cb82618fc3fb816bde1171154235b065","names":[],"sources":[]}, {"digest":"2ab12a0e345658f9342816d773fe610f049982bcad82631f43b9205abe43e5ee","names":[],"sources":[]}, - {"digest":"2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095","names":[],"sources":["packages/llm/llm/src/types.ts:473"]}, + {"digest":"2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095","names":[],"sources":["packages/llm/llm/src/types.ts:499"]}, {"digest":"2aed07493b1c65b6d00de5586da1e743cf501684b9ad43a1765fb4007d4ea456","names":[],"sources":[]}, {"digest":"2c4c21f19f426a58f2dac914e34313aa43f8408972de857a486d1f086d5adb6c","names":[],"sources":[]}, {"digest":"2c64d4bc7264fd3f8bed47e96ab8a593bce05a1e4e1c436dcd244a3b7e0c2a9d","names":[],"sources":[]}, @@ -183,6 +183,7 @@ {"digest":"2da725e554cd89301d829ed9dc45a572c3cd1b93b5e91d0d71f007976d8a0863","names":[],"sources":[]}, {"digest":"2e3523541e942ef6cc9fbc5b91f242e0ca4c6ada479b79b96025b797f6e41f3f","names":["packages/schedule/schedule/src/types.ts#ScheduleChange"],"sources":["packages/schedule/schedule/src/types.ts:256"]}, {"digest":"2e4debca12f2c449f96dc6574f3fbf2fb346193443114a241d01a236362ec48c","names":[],"sources":[]}, + {"digest":"2e505098f9209e52eebd577f1a6f9d4e773ec4d78987aff28a7c786442b9f54c","names":[],"sources":[]}, {"digest":"2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e","names":["packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot"],"sources":["packages/experimental/agent-team/src/types.ts:129"]}, {"digest":"2f1e6a7ebace43d727ae138ddcd352eb3813960009e5805b8c5ac84300c97716","names":["packages/attachment/attachment/src/types.ts#ImageMediaType"],"sources":["packages/attachment/attachment/src/types.ts:8"]}, {"digest":"2f3f201c8846e19c20bb75c02280e3d4eb3c57509e4715f0c0a4d1d4f91a7a06","names":[],"sources":[]}, @@ -207,7 +208,6 @@ {"digest":"383cf73ae4748e5beb281ae74857ba1ffb92ae01075b9fbf51af78b666007a8f","names":[],"sources":[]}, {"digest":"3860b4757d512a2869be833acb0faf834335b1b056bf3f83df5474dce95f8fa6","names":[],"sources":["packages/llm/llm/src/assistant-stream.ts:35"]}, {"digest":"390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackRating"],"sources":["packages/feedback/message-feedback/src/types.ts:17"]}, - {"digest":"39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f","names":[],"sources":[]}, {"digest":"39cf2d3ff8bac81c62d49ac2ba38a095b18e80f3ad700819fa1f650a4e60197f","names":["packages/jobs/jobs/src/types.ts#JobToolCall"],"sources":["packages/jobs/jobs/src/types.ts:16"]}, {"digest":"3b04790e08b35241ffa0d38f6d7e4f8788aa921341f6c0d7574a329397c59408","names":[],"sources":[]}, {"digest":"3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48","names":[],"sources":[]}, @@ -215,7 +215,7 @@ {"digest":"3bc287e765d58dc3a22672a0495a6cfd561220dd38fbaecef80aa95b96caed79","names":[],"sources":[]}, {"digest":"3bd652ebfa8726b3ce3937a4f1bd2759e02f86b3a08a20f9e41a8513656fbc5f","names":["packages/core/session/src/types.ts#OptionalSessionSeq"],"sources":["packages/core/session/src/types.ts:66"]}, {"digest":"3bdb8dc6989655b0d4ec361edafb8e2629bc263e1ea591819414fdafc1a413b3","names":[],"sources":[]}, - {"digest":"3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90","names":[],"sources":["packages/llm/llm/src/types.ts:471"]}, + {"digest":"3bec87f9e6ab3da03346f49923d3c51621debc0159df68605e543c3b2a026f90","names":[],"sources":["packages/llm/llm/src/types.ts:497"]}, {"digest":"3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1","names":[],"sources":[]}, {"digest":"3c2535ca0a2062f172f15344f8040755002b5ac506cf94fc4ebaa822b15feb8c","names":[],"sources":[]}, {"digest":"3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f","names":["packages/core/tools/src/contracts.ts#ToolRestriction"],"sources":["packages/core/tools/src/contracts.ts:376"]}, @@ -226,11 +226,12 @@ {"digest":"3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796","names":[],"sources":[]}, {"digest":"3f74ba74acd10644c1ed6947a411d8088bd2cfac74f61bb8540f96d68e31f004","names":[],"sources":[]}, {"digest":"3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f","names":["packages/schedule/schedule/src/types.ts#LegacyAfterScheduleRecord"],"sources":["packages/schedule/schedule/src/types.ts:161"]}, + {"digest":"4035903993dfee62cca2142c0ca51ab57075909ddee2817d0620daa97ac4d4ce","names":["packages/llm/llm/src/message.ts#UserMessage"],"sources":["packages/llm/llm/src/message.ts:162"]}, {"digest":"408927267422a7b2beb6df354ff7c1406d929a5646554f48adf85eb8372f811c","names":[],"sources":[]}, {"digest":"4208123b50df5006b181481ab45fcf1cde807b88d3fd4d340090bc2e202fac41","names":[],"sources":[]}, {"digest":"42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f","names":[],"sources":[]}, {"digest":"4312134fc6f9cad507c31390bd3d147535492dd77621ec517c5e85f853d53a03","names":[],"sources":[]}, - {"digest":"43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8","names":["packages/llm/llm/src/types.ts#ToolRemovalBlock"],"sources":["packages/llm/llm/src/types.ts:127"]}, + {"digest":"43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8","names":["packages/llm/llm/src/types.ts#ToolRemovalBlock"],"sources":["packages/llm/llm/src/types.ts:134"]}, {"digest":"44818000e56acb97afdb5045ce235eb55e3fb1ccabc9fc7d0750bb38f36a7f8e","names":[],"sources":[]}, {"digest":"44c8c77aa9aa100b0d74d2deee85474b29faa6eee0ac0f662f7abe9931ddd7f2","names":[],"sources":[]}, {"digest":"4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986","names":[],"sources":[]}, @@ -238,7 +239,7 @@ {"digest":"45f2a07ea94573123b6ae14db787a40062dec3834f36d8be92c523d40ba28448","names":[],"sources":["packages/interaction/user-approval/src/types.ts:44"]}, {"digest":"477614bca595a9e7adb5a1870be446b9a8684f3ba8950259d6289f2d3b74f028","names":[],"sources":["packages/compaction/compaction-image-offload/src/projection.ts:25"]}, {"digest":"47e5d84f1a93450a78c7f3954b43990d0f629af7c034a56313e5deb63e772c1b","names":[],"sources":[]}, - {"digest":"47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180","names":["packages/llm/llm/src/types.ts#StreamChunk"],"sources":["packages/llm/llm/src/types.ts:470"]}, + {"digest":"47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180","names":["packages/llm/llm/src/types.ts#StreamChunk"],"sources":["packages/llm/llm/src/types.ts:496"]}, {"digest":"487b65c37bfedb8b72088c4107a0a90a6249701f6b26745f6d98f9b8a22b07c7","names":[],"sources":["packages/llm/llm/src/assistant-stream.ts:44"]}, {"digest":"48e5c9861f16ac07e78cb7b5ae9dabdf7bb85c58baed5a51b4ad275050ea58e3","names":[],"sources":[]}, {"digest":"48f9c19417a1abbedfa59f4667bba36b93ac2db407adf5e84cb3ba0de30942cb","names":[],"sources":[]}, @@ -251,14 +252,14 @@ {"digest":"4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731","names":["packages/llm/llm/src/message.ts#ToolResultMessage"],"sources":["packages/llm/llm/src/message.ts:173"]}, {"digest":"4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea","names":[],"sources":[]}, {"digest":"4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db","names":[],"sources":["packages/core/session/src/types.ts:198"]}, - {"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:386","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]}, + {"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:388","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]}, + {"digest":"4d60dddaa65d3da8d4cd014d85ce3d2c2875664dd0433e331d8e9d395289686b","names":["packages/experimental/liveshare/src/host/viewer-principal.ts#ShareViewerPrincipal"],"sources":["packages/experimental/liveshare/src/host/viewer-principal.ts:24"]}, {"digest":"4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159","names":[],"sources":[]}, {"digest":"4e44c9118152f6829634bfd508e47669ab0497caffa380c94b4be91f9390d35d","names":[],"sources":[]}, - {"digest":"4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c","names":["packages/llm/llm/src/types.ts#TextBlock"],"sources":["packages/llm/llm/src/types.ts:62","packages/web/web-search-deepseek/src/provider.ts:66"]}, + {"digest":"4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c","names":["packages/llm/llm/src/types.ts#TextBlock"],"sources":["packages/llm/llm/src/types.ts:69","packages/web/web-search-deepseek/src/provider.ts:66"]}, {"digest":"4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a","names":["packages/subagent/subagent/src/continuation-messages.ts#SubagentSettledMessageSource"],"sources":["packages/subagent/subagent/src/continuation-messages.ts:29"]}, {"digest":"4edec1b770e8ec4ce1220bf42edc5ac46713a1b48d4caa778dcfc98bad1f28a3","names":[],"sources":[]}, {"digest":"4f507598cf476214aa7646ce7841a7064a9dcb05d0a1eaaefaf79c2b738f160f","names":[],"sources":[]}, - {"digest":"4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d","names":["packages/llm/llm/src/message.ts#Message"],"sources":["packages/llm/llm/src/message.ts:196"]}, {"digest":"4f91f187baeabef5c7950c4780d31bbe39f3345dff591a6cf26ac7e135ac1cf4","names":[],"sources":["packages/context/session-reference/src/types.ts:18"]}, {"digest":"4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc","names":[],"sources":[]}, {"digest":"50cf02d875f8a1f1411f4a6fbcffab9120ccef5f74a47331fbf29480e5f68f48","names":[],"sources":[]}, @@ -272,6 +273,7 @@ {"digest":"530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6","names":["packages/core/session/src/types.ts#RequestHeaderReason"],"sources":["packages/core/session/src/types.ts:279"]}, {"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac","names":[],"sources":[]}, {"digest":"53cf1704ae0709c7678bcf070c83c619fc48ca1b5c173b74cfa8702f22a8e492","names":[],"sources":[]}, + {"digest":"53d4f8256bc58109468ea358ac54d437760cd765012360cafe72a401e6a41d4a","names":[],"sources":["packages/core/session/src/types.ts:317"]}, {"digest":"54f9276a1617d7c12594722c89cfe864bcbe4517e9a6f9c98bab14f199a135e0","names":[],"sources":["packages/core/session/src/types.ts:363"]}, {"digest":"557e148d30b79ee48645850832b30c717d7c3b64641deb5d6133b8b4e91ee0b8","names":[],"sources":[]}, {"digest":"5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c","names":[],"sources":[]}, @@ -281,13 +283,14 @@ {"digest":"5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc","names":[],"sources":[]}, {"digest":"57f3f606ff8b6c84d9c443faace9d1dd10cca0354f37a4f515ef075f7c351b90","names":[],"sources":[]}, {"digest":"58015da1c1950a66daf66ae24913d6a1fc6d1d9ac441b155f8847f7f8584ba8e","names":[],"sources":[]}, - {"digest":"5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80","names":["packages/llm/llm/src/types.ts#FinishReason"],"sources":["packages/llm/llm/src/types.ts:165"]}, + {"digest":"5808e36c95355f0e745201b5db1b6bcec4ebf5adb75d3e82d7d59cc89635ab80","names":["packages/llm/llm/src/types.ts#FinishReason"],"sources":["packages/llm/llm/src/types.ts:172"]}, {"digest":"587bb11da730e06aa52b62c087d217d6f8f22459b25f31cb9e1e1baf17944e5e","names":[],"sources":[]}, {"digest":"589d0d91239681f89ee485459dc8c2f73ea47183b5534289d966637024a2c35d","names":[],"sources":[]}, {"digest":"58f94390b982f8a5ac1f4f235ba612557f4a1dc4a995d8b1cec63ce7639decb0","names":[],"sources":[]}, {"digest":"5abab3a27fd2736660ecddd94b28d6bbf114986dd894880153bd1be3ccacb563","names":[],"sources":[]}, {"digest":"5b232a9b9bfcc0fc6794dab7865bcf203a65785c5de5aa647a51228bff628413","names":[],"sources":[]}, {"digest":"5b6fb1f226ff56402db08a6aae82af76100b14056271875f69ec526a1ae05d51","names":[],"sources":["packages/interaction/commands/src/types.ts:112"]}, + {"digest":"5bb231bc5afdb532a6898a20604e76bf0c1db438bfcdc17d4847dceaddc02451","names":[],"sources":[]}, {"digest":"5bb69b88517f6214f2af4d1a6d2b771e271ad3ad695cc9c2c272069ae698c9bc","names":[],"sources":[]}, {"digest":"5bc66269cfaf614c0ecda8ecb469f23101a7609793fc2b693ac23c4aecdb575e","names":[],"sources":[]}, {"digest":"5bd310104cfe448e530beb11422d2f791e2e32e8e95fdc528a15f9ca693dae75","names":[],"sources":[]}, @@ -301,7 +304,7 @@ {"digest":"5dd766ccd1702a1142ad8b4bd44f2242f8e8a6752669ad3de0a5d224329b1438","names":[],"sources":["packages/compaction/compaction/src/types.ts:82"]}, {"digest":"5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e","names":[],"sources":[]}, {"digest":"5ebce1317eacd2c148456627a09668e927ee15c204f0643f5ea76b46b54a2647","names":[],"sources":[]}, - {"digest":"5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca","names":["packages/llm/llm/src/types.ts#LlmFailure"],"sources":["packages/llm/llm/src/types.ts:41"]}, + {"digest":"5f2117d1856a5ab43c28371faa978ebfaa48f8c7d734ea3fc94842210df83dca","names":["packages/llm/llm/src/types.ts#LlmFailure"],"sources":["packages/llm/llm/src/types.ts:48"]}, {"digest":"5f26a6c20b37632f8f57729d171c671def4683d994ac6257a8dffcd855101627","names":[],"sources":[]}, {"digest":"5f758f3de5f00c18968a7c6d347083aac5c9ae13f6a79c78f6611b23c4973564","names":["packages/subagent/subagent/src/descriptor.ts#OneShotSubagentDescriptorData"],"sources":["packages/subagent/subagent/src/descriptor.ts:61"]}, {"digest":"6085488d223aeffdef51d7ae17655fc26ab22ee13cbed82a50e62e2b6fb5b918","names":[],"sources":[]}, @@ -312,20 +315,22 @@ {"digest":"62e6a429ff3f390a46ac39fb468692032f42936ca0db19b53454184189401379","names":[],"sources":[]}, {"digest":"62ecb62dc0fe1b5e70217f8a80c39256a40ee3a1917be14e5ca899d20c35f7fd","names":[],"sources":[]}, {"digest":"6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f","names":["packages/subagent/subagent/src/descriptor.ts#ContinuableSubagentDescriptorData"],"sources":["packages/subagent/subagent/src/descriptor.ts:72"]}, + {"digest":"64ef542b1bfc988382d337f2ea85aab8ad76ea55341cf082163919d315688428","names":["packages/llm/llm/src/message.ts#DeveloperMessage"],"sources":["packages/llm/llm/src/message.ts:157"]}, {"digest":"6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25","names":["packages/experimental/agent-team/src/types.ts#TeamMemberSnapshot"],"sources":["packages/experimental/agent-team/src/types.ts:48"]}, {"digest":"65a8400e451e667e5b6e34df72ceb81fca8c12330330fa72083ad40279b189ad","names":["packages/llm/llm/src/message.ts#SystemPromptMessageSource"],"sources":["packages/llm/llm/src/message.ts:35"]}, {"digest":"65d583e66518c5236015e0846470fb32efcf92375ed306aef14e50a9d2cd2099","names":["packages/skill/tool-skill/src/index.ts#SkillCatalogSource"],"sources":["packages/skill/tool-skill/src/index.ts:35"]}, - {"digest":"66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2","names":["packages/llm/llm/src/types.ts#ToolSchema"],"sources":["packages/llm/llm/src/types.ts:491"]}, + {"digest":"66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2","names":["packages/llm/llm/src/types.ts#ToolSchema"],"sources":["packages/llm/llm/src/types.ts:517"]}, {"digest":"672c4aa2a9b736e3d6e5da6346d2e2ed5e3573f2d0baafa18e7407c9f3fee3db","names":["packages/skill/skill/src/index.ts#SkillInvocationSource"],"sources":["packages/skill/skill/src/index.ts:146"]}, {"digest":"67d5f73ceae82f5265258142ad6cf172de2c1fef93ba551b7f0454e0a93bffe0","names":["packages/llm/llm/src/call-config.ts#LlmCallConfig"],"sources":["packages/llm/llm/src/call-config.ts:31"]}, {"digest":"68141cbe3994a5ffcd064c07c75996538437fd9dbf77ce8a2927e89ec577fabe","names":[],"sources":[]}, {"digest":"688e853dc149f0d30b31765c12ec7e3be74169bdf1a4c6463a10b945d50cc1b8","names":[],"sources":[]}, {"digest":"68f44f59e91c41395076cb77265981a601680ec8a381bbbeb6ab754ffdc94573","names":[],"sources":[]}, {"digest":"69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1","names":[],"sources":[]}, - {"digest":"693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057","names":[],"sources":["packages/llm/llm/src/types.ts:161"]}, + {"digest":"693f4952fb5565da96c06bace8924b098ac2fbc915fde0ff8019701183abc057","names":[],"sources":["packages/llm/llm/src/types.ts:168"]}, {"digest":"69d238a6e9b08d67f601b1825962963d8d3523cb69ebf6208c697dc5d058c199","names":[],"sources":[]}, {"digest":"6ab45ac76839722df912f3ba50f1c6b41f2d614bde11f1aabfae04117ccdc80d","names":[],"sources":[]}, {"digest":"6b23ed9ce1b397e602788dbe033693bf97642a69e23fcab1cc45bf6f13b6d619","names":[],"sources":[]}, + {"digest":"6ba7edb6447b6a3e7947dddf5e4116c1526000c4e62882a6d65edc67ba797c2c","names":[],"sources":[]}, {"digest":"6bbe793f6a93dc8d50a4d4d3fbb6f8ed15650bcb7e151d28669628f7b7c3fc25","names":["packages/goal/goal/src/types.ts#GoalSnapshot"],"sources":["packages/goal/goal/src/types.ts:60"]}, {"digest":"6bc8e51124d0874bc89e1d0ceff381a9c3cd0fc240667d69f841ce4f60f65fe5","names":[],"sources":["packages/core/session/src/types.ts:338"]}, {"digest":"6c03546143cdf6ae88f1745ac98c5139efa1588d1415249bbc5f3104d32ab800","names":[],"sources":["packages/core/session/src/types.ts:196"]}, @@ -334,7 +339,6 @@ {"digest":"6ccb6bc423fb3952458abf9c1b1c3284208b7e660344b612cc55da773d61afe4","names":[],"sources":[]}, {"digest":"6cda0d6d3b4336d83998fe580628426df81c2afbab5d768b8813b56d569120e8","names":[],"sources":[]}, {"digest":"6cf082d0fb6034dea61636787ff0ddf43b6e59aad71e644435a479bc9b83ad65","names":[],"sources":[]}, - {"digest":"6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def","names":[],"sources":["packages/api/session-controller/src/types.ts:395"]}, {"digest":"6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406","names":[],"sources":["packages/preset/agent-preset-registry/src/session.ts:31"]}, {"digest":"6ea608bdc8a7a63d3d23b84c49721325094db88d136482e43a820945295eee3e","names":[],"sources":[]}, {"digest":"6ebcc2dbec9f2dabca9f668ae6d39e877899b180740570f453409db339221df3","names":[],"sources":[]}, @@ -345,6 +349,7 @@ {"digest":"6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20","names":["packages/llm/llm-retry/src/types.ts#LlmRetryStartedEventData"],"sources":["packages/llm/llm-retry/src/types.ts:43"]}, {"digest":"6f9e00383836275e46c0a9915767f9bb561be5fc30aae490174908a93e99a912","names":[],"sources":[]}, {"digest":"70b082fe78991edf13cf66e1b4d06cd2dcff9cacfd76f8cfb996bfb61939e7b2","names":[],"sources":[]}, + {"digest":"70d2acdec1ebcfea56d02f7233ebb9ec0f7f494447fb952884c22c3afdc01d6f","names":[],"sources":[]}, {"digest":"7180844e1066230c5e891f1b9ccd942423b0fe48aac6ded7eef2a285b288a732","names":[],"sources":[]}, {"digest":"719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91","names":["packages/llm/llm/src/message.ts#ModelMessageSource"],"sources":["packages/llm/llm/src/message.ts:24"]}, {"digest":"71bb3254ec8f1e491ce63b762a7b5f18709de23aa4e8abcc8b886f4b6ce051dd","names":[],"sources":["packages/llm/llm/src/assistant-stream.ts:28"]}, @@ -362,23 +367,27 @@ {"digest":"7a651f3b672c52fb615f22c0ce9cb3cc5b22aad92e6bbeefb0808fd7b9233a50","names":[],"sources":[]}, {"digest":"7af85bf70d4eafce63e739adae38ea85501e68df3a0e22f97f3a0be90fd9e4cb","names":[],"sources":[]}, {"digest":"7b1ce60193fa8b69292e5843858d9cee7dc565dcb40d33c136e5d7e8b8d4296a","names":[],"sources":[]}, + {"digest":"7c203f3d4dfa6638ff2601247cc7ca5d46731d2c614aa61217475b9eb1fbd0b3","names":[],"sources":[]}, {"digest":"7c6129b6961cf4b2a030f36c78d2b0a5be38617651a9b81322e054ee512bee06","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackDelete"],"sources":["packages/feedback/message-feedback/src/types.ts:46"]}, {"digest":"7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397","names":[],"sources":[]}, {"digest":"7d226b9fd95b4ba213d4237c22e930a91c3c769c95ca7f92977b9d625ba13438","names":["packages/goal/goal/src/domain.ts#GoalChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:44"]}, {"digest":"7d54a5a5e629e2e7783bfac72683bcc5e969e9f43426b9289826aaf294ff6344","names":["packages/feedback/command-feedback/src/types.ts#FeedbackRecord"],"sources":["packages/feedback/command-feedback/src/types.ts:27"]}, - {"digest":"7d65acf0a3a43a8f445ce5d3320b42eca492739fc931b2a7516bfb8368dc2c68","names":["packages/llm/llm/src/types.ts#ContentBlockType"],"sources":["packages/llm/llm/src/types.ts:148"]}, + {"digest":"7d65acf0a3a43a8f445ce5d3320b42eca492739fc931b2a7516bfb8368dc2c68","names":["packages/llm/llm/src/types.ts#ContentBlockType"],"sources":["packages/llm/llm/src/types.ts:155"]}, {"digest":"7df62978a2b7de8540e714152297743e0a622209a46a5ac9513fc0fad73ca23b","names":[],"sources":[]}, + {"digest":"7e9f1072c17f7aa86547c9c8b02766c1006d866a1859a3d677a336e932f70d1a","names":["packages/llm/llm/src/message.ts#MessageSource"],"sources":["packages/llm/llm/src/message.ts:136"]}, {"digest":"7eaef7a763ba5b949b8464bc8add8980a3a5ab566014b69787aeba71e5dfe07b","names":[],"sources":[]}, {"digest":"7f13860ee89a2d35ce09f958e85a04566cd59cb1de1d73a9619a9b631961dc71","names":[],"sources":[]}, {"digest":"7f7fd5a6b0064f597534b29ff62ef26e786dffccf5e14f654a7d4fcea2c35f04","names":[],"sources":[]}, {"digest":"7f9365bae9fcb73e1e34061a1acfe0422c1a5fd351848cace9bebbe806f28462","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunEndData"],"sources":["packages/workflow/tool-workflow/src/types.ts:36"]}, {"digest":"7f959241c24128b4983b559984811861dc59774b85905c8fa57c6cb0148a3dbc","names":["packages/goal/goal/src/domain.ts#GoalMessageSource"],"sources":["packages/goal/goal/src/domain.ts:47"]}, - {"digest":"8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5","names":["packages/llm/llm/src/types.ts#ContentBlock"],"sources":["packages/llm/llm/src/types.ts:150"]}, + {"digest":"7ff62f6599c083ceb5a0d7a8940ff85ba80dc1d7ddd6ba9339903e93f74cba54","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:43"]}, + {"digest":"8064c03c87c357fd1575cb9395ce86e6dcbe2d275bf7717864a02cddf01827d5","names":["packages/llm/llm/src/types.ts#ContentBlock"],"sources":["packages/llm/llm/src/types.ts:157"]}, {"digest":"80677c8c8b804189a0fde8aa70dd4ad7af8bb12dfd03027e8e023c8592c838bc","names":[],"sources":[]}, {"digest":"8067b0d232c350f189738e60ee3c9939d46ca3b32b45db5525237b077893f649","names":[],"sources":[]}, {"digest":"811229eedcec57979dbadca7dbea832a59f8580cff30cb217fca5e2da16314bd","names":[],"sources":[]}, - {"digest":"814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e","names":["packages/llm/llm/src/types.ts#ReasoningBlock"],"sources":["packages/llm/llm/src/types.ts:68"]}, + {"digest":"814d9434e1ddf9078612b3b34322f26e9363b469213335007f7e9ba7fee9464e","names":["packages/llm/llm/src/types.ts#ReasoningBlock"],"sources":["packages/llm/llm/src/types.ts:75"]}, {"digest":"82470020b5bfcb1a843e2aa05602af7d37df99e82d01f015c7c589ba054fd89d","names":[],"sources":[]}, + {"digest":"824a4aad44a54e245c67196817d67946734a2dc0eee0b0b6834a5b2a6ef352ce","names":[],"sources":[]}, {"digest":"825ca23416b1ea9d81442afead5cf9229a6fa05842f042135cc3aa9710a6a2d8","names":[],"sources":[]}, {"digest":"829a2fb3be6efd4ba36aef8ed2d381a4c6fb2a0aaf50c823a6f3f8290786b809","names":[],"sources":[]}, {"digest":"83079c8a3f733ac3fa603eefa0fbb4737ca4d8b69e7e2e7298b60f9ac4098693","names":[],"sources":["packages/skill/tool-skill/src/index.ts:43"]}, @@ -398,19 +407,18 @@ {"digest":"871bfd6f7bb91061246768d3377f3fd58d6b8ea7c31f44c714ae7ab215650d62","names":[],"sources":[]}, {"digest":"87b1a33564b9a4d63b00988a524c12259243953aebb9c9a05127d87b27464df9","names":[],"sources":[]}, {"digest":"88841202942c7683b3b7e5ea6a796047ec326b6701ac23e58cc236d85613b9fa","names":[],"sources":[]}, - {"digest":"88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a","names":[],"sources":["packages/llm/llm/src/types.ts:474"]}, + {"digest":"88a610b831be6d92612a464be5c7d0b88ec24d97bd299837bf74bf7672183c9a","names":[],"sources":["packages/llm/llm/src/types.ts:500"]}, {"digest":"8a046b52331959aa3394680d6acbe09b8a5447c6b61375f263445bfcef19b7f1","names":[],"sources":[]}, {"digest":"8a6e1ec9e8db346b0e02f027db73c07a94f067a26d40c1aef1abd09c47ce7ba0","names":[],"sources":[]}, {"digest":"8a9c990773f798eac10ca8878e6bcc531ee080bc589cac956730e462cc6a09ad","names":[],"sources":["packages/interaction/permission-presets/src/index.ts:53"]}, {"digest":"8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141","names":[],"sources":["packages/experimental/hook-protocol/src/types.ts:31"]}, {"digest":"8af841fc9b265ade3441caf9fe40dd24db24226befba48150e6d0eeccc1389c3","names":[],"sources":[]}, - {"digest":"8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52","names":[],"sources":["packages/session/session-log-deepseek/src/types.ts:81"]}, - {"digest":"8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14","names":["packages/llm/llm/src/message.ts#UserMessage"],"sources":["packages/llm/llm/src/message.ts:162"]}, + {"digest":"8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52","names":[],"sources":["packages/session/session-log-deepseek/src/types.ts:96"]}, {"digest":"8c1c4f72755a78ea163523a46565316e36e9b644382eb48e6635c71a6f412dd2","names":[],"sources":[]}, {"digest":"8c627d7a45fa406cea879a3922105e27ed4c3c7966e264afaf11ea7a330f2dd1","names":[],"sources":[]}, {"digest":"8cdd3fdd295cee53035de16a1b23411716f75e9f994fe9510cac5d7fef605c76","names":[],"sources":[]}, - {"digest":"8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0","names":[],"sources":["packages/llm/llm/src/types.ts:160"]}, - {"digest":"8e611bf46821e5003a1605a73560a6f56388295149d449dfe2142d5ec335f29c","names":["packages/llm/llm/src/types.ts#ToolAdditionBlock"],"sources":["packages/llm/llm/src/types.ts:115"]}, + {"digest":"8e31bb958e9b015e6f8407eaade32217068159e816f3cc99c3ad76eb889732e0","names":[],"sources":["packages/llm/llm/src/types.ts:167"]}, + {"digest":"8e611bf46821e5003a1605a73560a6f56388295149d449dfe2142d5ec335f29c","names":["packages/llm/llm/src/types.ts#ToolAdditionBlock"],"sources":["packages/llm/llm/src/types.ts:122"]}, {"digest":"8e8155594e07812356b67d732196dda894b0f9c91a5969d51612cf490b0a016d","names":[],"sources":[]}, {"digest":"8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af","names":["packages/schedule/schedule/src/types.ts#OneShotScheduleDispatchChange"],"sources":["packages/schedule/schedule/src/types.ts:237"]}, {"digest":"8f324cea3916c0517261e88167def8efb3a9994c1135e23d40ed9fd48f8c7bd4","names":[],"sources":[]}, @@ -424,14 +432,12 @@ {"digest":"93cbbc5a7fd0b1aa1ff1b9a6bb5ec52b073f48e5a5200365c45c0cd54059e1b9","names":[],"sources":["packages/interaction/user-approval/src/types.ts:55"]}, {"digest":"93d38436347f67c9022aacbc4472e283d722bcab8eb73fc376aaf7b6a6b9fb35","names":[],"sources":[]}, {"digest":"94c56aeb1564ad735eb773d88636b40761d063efa1610f1308004d32923f5378","names":[],"sources":[]}, - {"digest":"95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65","names":[],"sources":["packages/llm/llm/src/types.ts:472"]}, + {"digest":"95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65","names":[],"sources":["packages/llm/llm/src/types.ts:498"]}, {"digest":"95a3ffccbf3c8afcf9a2f3b355f724da8e07e98dc47ee741166dde7f9ea058bd","names":[],"sources":[]}, - {"digest":"95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f","names":[],"sources":[]}, {"digest":"95c0e88fcf0bafb2087f912ad9e2369de6ae6530cc0c4c2772c96205b57f7f60","names":[],"sources":[]}, {"digest":"9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114","names":[],"sources":[]}, {"digest":"96bfb916a727cd4445a594df264c523fc039749a31bfed924b8cc7bb308a5bb0","names":[],"sources":[]}, {"digest":"96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c","names":["packages/core/agent/src/types.ts#InboxTarget"],"sources":["packages/core/agent/src/types.ts:40"]}, - {"digest":"9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:37"]}, {"digest":"9890113e97dd3541787bfe6704f2b223d3a890b5325ca0acae2b0398909b413e","names":[],"sources":["packages/sandbox/sandbox-policy/src/session-mode.ts:33"]}, {"digest":"99135d8a3937977955eaac5f8f04f930dcd1562f8450a54a51cde9b921dd59a6","names":[],"sources":[]}, {"digest":"9947aa5938fe19a895298da412917acbf9ffb5093a73ab8baa257e6ae9e63a07","names":[],"sources":[]}, @@ -449,7 +455,6 @@ {"digest":"9e07693fd7cd9babe0688c22c986e4d519ae1c0e40eeea13b2dc583bed2abc86","names":[],"sources":[]}, {"digest":"9e2c06d8653585b33d72d5c1a80da79c8a24a88c284409bca15cc0f0ead5ebfb","names":[],"sources":[]}, {"digest":"9e41386b3a0c9572b0d63078492ebb3997da7d3a830d44e0259418bd02f4bcb2","names":[],"sources":["packages/compaction/compaction/src/types.ts:38","packages/compaction/compaction/src/types.ts:84"]}, - {"digest":"9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30","names":[],"sources":[]}, {"digest":"9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94","names":[],"sources":["packages/experimental/webhook/src/types.ts:74"]}, {"digest":"9f65c43e340bba31cdc0fc37bd93ccb68d5e9b36f47a68c207e95a8e68620a5d","names":[],"sources":[]}, {"digest":"a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb","names":[],"sources":[]}, @@ -461,12 +466,13 @@ {"digest":"a15a19ef63cf5d1b711db1d249821dab0cedcd3a9ad09137b47352c93841fd20","names":[],"sources":[]}, {"digest":"a16e567c8cd2c55cf1e911aed3c200c4e1f6febb1459ff2a9e7adf159afb68cb","names":[],"sources":[]}, {"digest":"a2066bd52f61218ba15b758def2641cc6a8bd3d4a594a488ee4c65be356d7e39","names":[],"sources":[]}, - {"digest":"a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1","names":[],"sources":["packages/llm/llm/src/types.ts:477"]}, + {"digest":"a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1","names":[],"sources":["packages/llm/llm/src/types.ts:503"]}, {"digest":"a2be0f0cb59f0d4a06557d45847d1938aced238baa77a6da416e590b8b958553","names":[],"sources":[]}, {"digest":"a3a259a1aa5401af786ce08c4a8419681d3000d73467991b5fc563389bc4ebd2","names":[],"sources":[]}, {"digest":"a3c6f1ea38e9a61d7a9706057ba65aabe02dfe6d9dd90d4736aa62325d874531","names":[],"sources":[]}, {"digest":"a3edd1efd1fef21b72c55e213c8a8b24196b091abde2b89db52be0b2aaae3cfa","names":[],"sources":[]}, {"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/experimental/agent-team/src/types.ts:232"]}, + {"digest":"a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302","names":[],"sources":["packages/api/session-controller/src/types.ts:397"]}, {"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc","names":[],"sources":[]}, {"digest":"a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb","names":["packages/experimental/hook-protocol/src/types.ts#HookDialect"],"sources":["packages/experimental/hook-protocol/src/types.ts:48"]}, {"digest":"a6da9ad5d42ae83a6833a8288b076688e8e0801633ea36cbdd8a30a6d9f3160c","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunStartData"],"sources":["packages/workflow/tool-workflow/src/types.ts:14"]}, @@ -484,9 +490,8 @@ {"digest":"aa7bc2817014ca6c45719b1c193c5c131be718a0921f34345098ff250f444626","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackPut"],"sources":["packages/feedback/message-feedback/src/types.ts:38"]}, {"digest":"aa93010d0e521ad7c42273f58d5c9b8bf1dfc94edf1b805a3971b364a35c1877","names":[],"sources":[]}, {"digest":"aacda84a69739238aa782eea902d3073017d9d74b9808de2bf5a83c9738cbdc6","names":[],"sources":[]}, - {"digest":"aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292","names":[],"sources":[]}, {"digest":"aae343bc3d663c83f2e8b5e1c7eea5d509dc7a5361bc8bd8b5ac3bb8d3b1ed56","names":[],"sources":[]}, - {"digest":"ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7","names":[],"sources":["packages/core/session/src/types.ts:220","packages/llm/llm/src/types.ts:159"]}, + {"digest":"ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7","names":[],"sources":["packages/core/session/src/types.ts:220","packages/llm/llm/src/types.ts:166"]}, {"digest":"aba175fb20ffc71112c55efe70458ff5d7dbfb83501892b4ba8d077f0ea5d7ba","names":[],"sources":[]}, {"digest":"abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c","names":["packages/web/web-search-deepseek/src/provider.ts#DeepSeekSearchLlmRequest"],"sources":["packages/web/web-search-deepseek/src/provider.ts:55"]}, {"digest":"ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585","names":[],"sources":["packages/core/session/src/types.ts:398"]}, @@ -503,7 +508,6 @@ {"digest":"b0127044ab31a702bddfd785d345f5abd7a70876746e895ce443afa3e60ddf2d","names":[],"sources":[]}, {"digest":"b062078e899958f99ae464514578b8c093924dfe34bd9a835932b420ff2150b1","names":[],"sources":[]}, {"digest":"b0b0eb1b16490f6bee135b2377e8f8c39227974c799ad7d49ff418af23ecbc35","names":["packages/attachment/attachment/src/types.ts#ImageAttachmentRef"],"sources":["packages/attachment/attachment/src/types.ts:11"]}, - {"digest":"b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591","names":["packages/llm/llm/src/message.ts#MessageSource"],"sources":["packages/llm/llm/src/message.ts:136"]}, {"digest":"b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081","names":["packages/core/session/src/types.ts#RequestContext"],"sources":["packages/core/session/src/types.ts:260"]}, {"digest":"b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7","names":[],"sources":[]}, {"digest":"b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1","names":[],"sources":["packages/experimental/agent-team/src/types.ts:230"]}, @@ -551,12 +555,11 @@ {"digest":"bf16d2e98a7f13894ded2cffe5ce857bf473954a0e3c0ed9bc7739e2e1c7b78a","names":[],"sources":[]}, {"digest":"c0ea97bc89dfc46fa94eab956f5c3ee6af2f964aa7db6327cb18b451f207bef7","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowAgentEndData"],"sources":["packages/workflow/tool-workflow/src/types.ts:29"]}, {"digest":"c170da8ab7c37635b10867b2571811d9e317ac3ec394e8ff8efe53f902fc41c1","names":["packages/llm/llm-retry/src/types.ts#LlmRetryEventData"],"sources":["packages/llm/llm-retry/src/types.ts:16"]}, - {"digest":"c1f0f17b6493b85db6453bb30e01b208d703731b25bc1e68e5d4fc72eea9cdea","names":["packages/llm/llm/src/types.ts#ToolCallBlock"],"sources":["packages/llm/llm/src/types.ts:105"]}, + {"digest":"c1f0f17b6493b85db6453bb30e01b208d703731b25bc1e68e5d4fc72eea9cdea","names":["packages/llm/llm/src/types.ts#ToolCallBlock"],"sources":["packages/llm/llm/src/types.ts:112"]}, {"digest":"c1f9e0405de6d18cabb9ee70782a027f9bbdc57e5abec9dcccdd56119e2e9058","names":[],"sources":[]}, {"digest":"c219efbdfd53b836786202ab4532913d555798f0a8615a4e9629736e3799fb70","names":[],"sources":[]}, {"digest":"c2deb7c9183735d4e0fffddf5c623f4e34daa1c8e96b111982e957dfcbbcf896","names":["packages/workflow/workflow/src/types.ts#WorkflowAgentOutcome"],"sources":["packages/workflow/workflow/src/types.ts:110"]}, {"digest":"c2f49004d9ae38a964e80850c87bc6849d6a872091a1d43e99eedf00c641e2a3","names":[],"sources":[]}, - {"digest":"c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c","names":[],"sources":["packages/core/agent/src/types.ts:110"]}, {"digest":"c43b119ce579b3e9983a0d3adc4ba402ebb9ba773d89738a2dab01ecf5813943","names":[],"sources":[]}, {"digest":"c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49","names":[],"sources":[]}, {"digest":"c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005","names":[],"sources":["packages/interaction/user-questions/src/types.ts:134"]}, @@ -564,6 +567,7 @@ {"digest":"c589e938cc0e4989df8b82abef6339de621f0dc4fe62c230d145b94996af7f92","names":[],"sources":[]}, {"digest":"c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6","names":["packages/util/values/src/index.ts#JsonValue"],"sources":["packages/util/values/src/index.ts:4"]}, {"digest":"c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262","names":[],"sources":[]}, + {"digest":"c78b9097bfae83cd08f0a98a92730236ba80ac61d712046959e4fc7f28aa1f36","names":["packages/llm/llm/src/message.ts#Message"],"sources":["packages/llm/llm/src/message.ts:196"]}, {"digest":"c8e6f48878cc2a2f0dc59682280d81f407943fb503f32c64bf9ff3d9393c3eff","names":[],"sources":[]}, {"digest":"c99e20441bf15341e9db54385e558e5c03d21dff2eb1ac08ccbf47c2d8747954","names":[],"sources":[]}, {"digest":"ca085dba2c877feceebf3c62f9d39c6c2a4d2314d97b94b3927a780fe8fe2bb2","names":[],"sources":[]}, @@ -575,14 +579,14 @@ {"digest":"cda2345a9be9d139df49ccbd16dcba716f3440178bf20ff8b801fda1f5eb77dc","names":[],"sources":[]}, {"digest":"cdca80b8dad24d4e7b1d5a288a8475ba8d8d46be385bd2da85f122ee836f1670","names":[],"sources":[]}, {"digest":"cdee10e87dbed8440d8b6935888624884878efc7af95402e94171b29f173dced","names":[],"sources":["packages/llm/llm-retry/src/types.ts:17"]}, - {"digest":"cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0","names":["packages/llm/llm/src/message.ts#DeveloperMessage"],"sources":["packages/llm/llm/src/message.ts:157"]}, + {"digest":"cdf988172b589b230f2bce868489def1bb575eb0501076ac757a8f369ac5c64e","names":[],"sources":[]}, {"digest":"cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6","names":["packages/core/tool-executor-ptc/src/types.ts#PtcDispatchEventData"],"sources":["packages/core/tool-executor-ptc/src/types.ts:21"]}, {"digest":"cffcdc19881ab61ec0a9ce18743996f7ac44ac8889f34dfac36ae745574aa22f","names":["packages/goal/goal/src/domain.ts#GoalClearChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:35"]}, {"digest":"d0fbb0ca1bbba059830826f33f801da1aced35a2504376d61e020bdfec8d50c0","names":[],"sources":[]}, {"digest":"d28765f87d6b6ab408e0b2f3e7e297791b004b806d8e302c49903ebae815feb2","names":[],"sources":[]}, - {"digest":"d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc","names":[],"sources":[]}, - {"digest":"d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d","names":[],"sources":["packages/llm/llm/src/types.ts:475"]}, + {"digest":"d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d","names":[],"sources":["packages/llm/llm/src/types.ts:501"]}, {"digest":"d4c799e3d51cae1040362ba4968efc484739e14416c9c1df20c2f108b4ef2eb5","names":[],"sources":[]}, + {"digest":"d4d960185026266ed7bd127be1e252234689e8b4890c7c2677a644ba58f35401","names":[],"sources":["packages/core/agent/src/types.ts:110"]}, {"digest":"d4e4d575abeb4b72d616dc65c17ac0eaa943cc135609395044c38d5afcb10e31","names":[],"sources":[]}, {"digest":"d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a","names":[],"sources":[]}, {"digest":"d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9","names":[],"sources":[]}, @@ -607,7 +611,7 @@ {"digest":"de25352d2dc4196309bd84b5bac6319f6d5c898ea0e82bb5ae66c5738d0d426f","names":[],"sources":[]}, {"digest":"de30b1fa12c75f81df6aa4096e1595296d2d04e2be14a7c29849cf8028b79048","names":[],"sources":[]}, {"digest":"deaae5cd30987d4f49398e8f9d994c125907e7f928300745fb1dbf15f145d0e7","names":[],"sources":[]}, - {"digest":"deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5","names":["packages/typert/protocol/src/types.ts#Principal"],"sources":["packages/typert/protocol/src/types.ts:457","packages/typert/protocol/src/types.ts:461"]}, + {"digest":"deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5","names":[],"sources":["packages/typert/protocol/src/types.ts:457"]}, {"digest":"df5d9709b7db2a72510dc52052a23f0c38f39730af1504bab8bccabc14fae528","names":[],"sources":[]}, {"digest":"df843efd87c78cf8de98f8e0d0216956cd568d0d0e56a22c853663e274aa1a18","names":[],"sources":[]}, {"digest":"dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd","names":["packages/experimental/agent-team/src/types.ts#TeamMemberPhase"],"sources":["packages/experimental/agent-team/src/types.ts:45"]}, @@ -638,7 +642,6 @@ {"digest":"e95f957154b2b1b58692af53d4131180b6dd19e2264869f876dbaa959991494e","names":[],"sources":[]}, {"digest":"ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403","names":[],"sources":["packages/experimental/agent-team/src/types.ts:234"]}, {"digest":"eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c","names":[],"sources":["packages/core/session/src/types.ts:201"]}, - {"digest":"eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5","names":[],"sources":[]}, {"digest":"ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8","names":[],"sources":[]}, {"digest":"ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93","names":[],"sources":[]}, {"digest":"ec8b5ff7c77ca6893ac7ba5a0c5b2aefeb52f12c64450b7359660c2a7e8a53d7","names":[],"sources":["packages/core/session/src/types.ts:234"]}, @@ -655,11 +658,11 @@ {"digest":"f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402","names":["packages/core/session/src/types.ts#TurnEndReason"],"sources":["packages/core/session/src/types.ts:238"]}, {"digest":"f1996af2602e6f62b5ab179ca5c4af00d8753938b74cc6ef102de09e99e74a7e","names":[],"sources":[]}, {"digest":"f19bee84bf3d20904553cdcd57465232bc1c2a194f54c4d195292ba0487223a6","names":["packages/attachment/attachment/src/types.ts#FileAttachmentRef"],"sources":["packages/attachment/attachment/src/types.ts:39"]}, - {"digest":"f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515","names":[],"sources":["packages/llm/llm/src/types.ts:476"]}, + {"digest":"f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515","names":[],"sources":["packages/llm/llm/src/types.ts:502"]}, {"digest":"f230c0b583e23b25ee0a61dbe4c05e95c1e2ae44b27939501eb9e03696f0c278","names":[],"sources":[]}, {"digest":"f29d63336e8266d80dd7baeab219436db86341304f6be1cd0edb36f5134c4a47","names":[],"sources":[]}, - {"digest":"f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef","names":["packages/llm/llm/src/types.ts#FileBlock"],"sources":["packages/llm/llm/src/types.ts:98"]}, - {"digest":"f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5","names":["packages/llm/llm/src/types.ts#SystemPromptUpdate"],"sources":["packages/llm/llm/src/types.ts:414"]}, + {"digest":"f2da0f27c1e6add59982c37b15e4a4912c5b33466859cce3b13d503600e24aef","names":["packages/llm/llm/src/types.ts#FileBlock"],"sources":["packages/llm/llm/src/types.ts:105"]}, + {"digest":"f311671e07bc7fa645a0b6597de9966b97147956565296ba76a36543801b66c5","names":["packages/llm/llm/src/types.ts#SystemPromptUpdate"],"sources":["packages/llm/llm/src/types.ts:440"]}, {"digest":"f4a66f654996c093aae700ea4c434ff2ee3569fea9c0e5813ccf6afdccabd163","names":[],"sources":[]}, {"digest":"f5520c1d261ca86ea03735799c311a0a9ede8babb588cf0a65422576d2ee0ad1","names":[],"sources":[]}, {"digest":"f56c5b59ca0161310c338d31dd16cf14287ccd602ff4181058df178c1c286d7f","names":["packages/context/session-reference/src/types.ts#SessionReferenceSource"],"sources":["packages/context/session-reference/src/types.ts:13"]}, @@ -688,8 +691,9 @@ {"digest":"fd0d91a1f9dd2efb8959bdf5e07f8f0a98b1cdfa38ce8d4732ebde0b44c3abd4","names":[],"sources":[]}, {"digest":"fe1a7acdeed4d0f5ab483396eaa5b3dc97263b7c10eb4da2f0028cb01728bfc9","names":[],"sources":[]}, {"digest":"fe792a6cd430494503837e21e79f91677704ccdd9e870c679aabd7d43ed47ddd","names":[],"sources":[]}, - {"digest":"fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394","names":["packages/llm/llm/src/types.ts#TokenUsage"],"sources":["packages/llm/llm/src/types.ts:175"]}, - {"digest":"fee1caf8ab6a65c1bc981f5a1730164490f22a2a89ffa3d0b76f1f432c44c8b5","names":[],"sources":[]} + {"digest":"fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394","names":["packages/llm/llm/src/types.ts#TokenUsage"],"sources":["packages/llm/llm/src/types.ts:182"]}, + {"digest":"fee1caf8ab6a65c1bc981f5a1730164490f22a2a89ffa3d0b76f1f432c44c8b5","names":[],"sources":[]}, + {"digest":"ff2cbb53582d1827b02622482246979942f59e07e0393b76a161251423549e72","names":[],"sources":[]} ], "nodes": { "0116b1f89c05221d": {"kind":"literal","value":"web_search_20250305"}, @@ -708,6 +712,7 @@ "09f3f8dc417f2782": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"8af841fc9b265ade","optional":false},{"name":"source","type":"65a8400e451e667e","optional":false}],"indices":[]}, "0a30c3be336cdd4f": {"kind":"object","properties":[{"name":"kind","type":"1e0b0b66b6f0c53f","optional":false},{"name":"reason","type":"1f8c126cfab3df81","optional":false}],"indices":[]}, "0a8a062bc9c3c0d9": {"kind":"literal","value":"subagent/model-selection-policy"}, + "0a92c5bdfa06c2e2": {"kind":"object","properties":[{"name":"displayName","type":"4cd48f3e5108bd6e","optional":true},{"name":"providerId","type":"4cd48f3e5108bd6e","optional":false},{"name":"subjectId","type":"4cd48f3e5108bd6e","optional":false},{"name":"tenantId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "0aa62ab51b36c0e6": {"kind":"object","properties":[],"indices":[{"key":"4cd48f3e5108bd6e","value":"bbc5696e3f13c758"}]}, "0acd94fe8794574c": {"kind":"object","properties":[{"name":"blocks","type":"b1664fa89f8eab49","optional":true},{"name":"response","type":"bbc5696e3f13c758","optional":false}],"indices":[]}, "0af3ecfb8c60d416": {"kind":"literal","value":"turn/start"}, @@ -733,7 +738,6 @@ "124aa62078ca3311": {"kind":"object","properties":[{"name":"data","type":"da60ed58a8fa23d7","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"d4c799e3d51cae10","optional":false}],"indices":[]}, "12a0cfa619ac4a5f": {"kind":"literal","value":"read-only"}, "12d5808fac1996b9": {"kind":"object","properties":[{"name":"attachment","type":"b0b0eb1b16490f6b","optional":false},{"name":"offloaded","type":"dacf3d5f3e4ff619","optional":true},{"name":"type","type":"fb8b6a2e53cb9cf4","optional":false}],"indices":[]}, - "12f9a5a7506bca46": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"cebb7e9f3f663e66","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "1379f0b72275994d": {"kind":"literal","value":"developer/message"}, "13d3d180f977bf78": {"kind":"object","properties":[{"name":"data","type":"1528539c63db8b23","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1a2718b00937b236","optional":false}],"indices":[]}, "14230f9b8b5797bb": {"kind":"literal","value":"chunk"}, @@ -807,6 +811,7 @@ "2da725e554cd8930": {"kind":"literal","value":"fork"}, "2e3523541e942ef6": {"kind":"union","types":["18dc21f12bbac8ad","8e8e91646a5cae3f","e6063d0451dc0202","eea687d437880bd5"]}, "2e4debca12f2c449": {"kind":"literal","value":"team-message"}, + "2e505098f9209e52": {"kind":"object","properties":[{"name":"data","type":"7ff62f6599c083ce","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, "2eafefdc6f79a492": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderName","type":"4cd48f3e5108bd6e","optional":false},{"name":"targetId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "2f1e6a7ebace43d7": {"kind":"union","types":["3f74ba74acd10644","58f94390b982f8a5","de30b1fa12c75f81","ebb768d85ec87fb6"]}, "2f3f201c8846e19c": {"kind":"literal","value":"tool-goal"}, @@ -831,7 +836,6 @@ "383cf73ae4748e5b": {"kind":"literal","value":"recall"}, "3860b4757d512a28": {"kind":"object","properties":[{"name":"args","type":"93c33d9687613293","optional":false},{"name":"dt","type":"5d03ba38734809bc","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"time0","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"616f93a5047de668","optional":false}],"indices":[]}, "390eed1028aba027": {"kind":"union","types":["b1f62380f6746fed","df5d9709b7db2a72"]}, - "39adb7fce8475af6": {"kind":"object","properties":[{"name":"data","type":"8bca2b4f53ec7745","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, "39cf2d3ff8bac81c": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"rootCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "3b04790e08b35241": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, "3b1be838223869fe": {"kind":"object","properties":[{"name":"data","type":"e4c18e294232c3ba","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"c589e938cc0e4989","optional":false}],"indices":[]}, @@ -850,6 +854,7 @@ "3ee93b06f3a12585": {"kind":"object","properties":[{"name":"data","type":"7c6129b6961cf4b2","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"44818000e56acb97","optional":false}],"indices":[]}, "3f74ba74acd10644": {"kind":"literal","value":"image/jpeg"}, "3fdf9582c3b5597d": {"kind":"object","properties":[{"name":"afterSeconds","type":"0657d3ca5ac4e7bf","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"5b232a9b9bfcc0fc","optional":false},{"name":"prompt","type":"4cd48f3e5108bd6e","optional":false},{"name":"scheduledAt","type":"4cd48f3e5108bd6e","optional":false},{"name":"title","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, + "4035903993dfee62": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"7e9f1072c17f7aa8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "408927267422a7b2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, "4208123b50df5006": {"kind":"object","properties":[{"name":"data","type":"ad0c38a0adb82a94","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"3e0eff5946255ff0","optional":false}],"indices":[]}, "42e0916e0dda5f6d": {"kind":"object","properties":[{"name":"data","type":"7f9365bae9fcb73e","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"fe792a6cd4304945","optional":false}],"indices":[]}, @@ -876,13 +881,13 @@ "4b7ddaf3710b88ff": {"kind":"array","element":"a99f76fd149051c3"}, "4bb451330ff699f3": {"kind":"object","properties":[{"name":"kind","type":"b38f206d59bad891","optional":false}],"indices":[]}, "4cd48f3e5108bd6e": {"kind":"primitive","type":"string"}, + "4d60dddaa65d3da8": {"kind":"object","properties":[{"name":"actor","type":"0a92c5bdfa06c2e2","optional":false},{"name":"kind","type":"6ba7edb6447b6a3e","optional":false},{"name":"shareId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "4e011753f8263f43": {"kind":"literal","value":"team/message/queued"}, "4e44c9118152f682": {"kind":"literal","value":"text"}, "4e88776858652856": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"4e44c9118152f682","optional":false}],"indices":[]}, "4e97c3d85c0fc817": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bf16d2e98a7f1389","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "4edec1b770e8ec4c": {"kind":"literal","value":"ptc-mode"}, "4f507598cf476214": {"kind":"literal","value":"series"}, - "4f9125fa028a5628": {"kind":"union","types":["09f3f8dc417f2782","4b259f7f8f0a44ad","8bca2b4f53ec7745","cebb7e9f3f663e66","fa87537f4a9c1e25"]}, "4f91f187baeabef5": {"kind":"object","properties":[{"name":"capturedFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"capturedThroughSeq","type":"3bd652ebfa8726b3","optional":false},{"name":"compacted","type":"aa93010d0e521ad7","optional":false},{"name":"inputIndex","type":"0657d3ca5ac4e7bf","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"omittedBytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"omittedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"originalMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retainedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"truncated","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, "4fb59762612c3e3a": {"kind":"object","properties":[{"name":"data","type":"b1ebd5cda47d90d9","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"18fcdd273a2f2f9d","optional":false}],"indices":[]}, "50cf02d875f8a1f1": {"kind":"union","types":["2da725e554cd8930","dd2780e7e0768433"]}, @@ -896,6 +901,7 @@ "530da6c0267813bb": {"kind":"union","types":["34d1ab5c5df37818","4f507598cf476214","e7ce49f12953583f","f789af5ab844827e"]}, "53750b9abf61efb7": {"kind":"object","properties":[{"name":"data","type":"f731ffe80810b028","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"44c8c77aa9aa100b","optional":false}],"indices":[]}, "53cf1704ae0709c7": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "53d4f8256bc58109": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"64ef542b1bfc9883","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "54f9276a1617d7c1": {"kind":"object","properties":[{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"stream","type":"84bbfdce7d2eab5b","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "557e148d30b79ee4": {"kind":"literal","value":"agent-preset/selected"}, "5654685d7557ec74": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, @@ -912,6 +918,7 @@ "5abab3a27fd27366": {"kind":"literal","value":"text-delta"}, "5b232a9b9bfcc0fc": {"kind":"literal","value":"after"}, "5b6fb1f226ff5640": {"kind":"object","properties":[{"name":"commandId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"1ab2ef1296f157ba","optional":false},{"name":"sourceEventSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"text","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, + "5bb231bc5afdb532": {"kind":"object","properties":[{"name":"data","type":"4035903993dfee62","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, "5bb69b88517f6214": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "5bc66269cfaf614c": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, "5bd310104cfe448e": {"kind":"literal","value":"resource-cost"}, @@ -936,6 +943,7 @@ "62e6a429ff3f390a": {"kind":"union","types":["1d8798b1a5e8c545","1e346446e183ee76","34d1ab5c5df37818","7624ca28c61ca0ea","ad22a5ca5bbaab2e","d9196086e41e4dd5"]}, "62ecb62dc0fe1b5e": {"kind":"literal","value":"next-turn"}, "6487b10ba7542f3c": {"kind":"object","properties":[{"name":"agentModel","type":"4cd48f3e5108bd6e","optional":true},{"name":"agentProvider","type":"4cd48f3e5108bd6e","optional":true},{"name":"agentReasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"persona","type":"4cd48f3e5108bd6e","optional":true},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"toolFilter","type":"3cfc3a56502da1f8","optional":true},{"name":"version","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "64ef542b1bfc9883": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"7e9f1072c17f7aa8","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "6598cd6da253948d": {"kind":"object","properties":[{"name":"context","type":"50cf02d875f8a1f1","optional":false},{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"error","type":"4cd48f3e5108bd6e","optional":true},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"phase","type":"dfce5db4aa42b7eb","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "65a8400e451e667e": {"kind":"object","properties":[{"name":"kind","type":"09655b34d7f3cf1e","optional":false}],"indices":[]}, "65d583e66518c523": {"kind":"object","properties":[{"name":"entries","type":"aba175fb20ffc711","optional":false},{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"incomplete","type":"dacf3d5f3e4ff619","optional":true},{"name":"kind","type":"6ebcc2dbec9f2dab","optional":false},{"name":"update","type":"dacf3d5f3e4ff619","optional":true}],"indices":[]}, @@ -950,6 +958,7 @@ "69d238a6e9b08d67": {"kind":"literal","value":"interrupted"}, "6ab45ac76839722d": {"kind":"literal","value":"assistant/message"}, "6b23ed9ce1b397e6": {"kind":"literal","value":"compact-checkpoint"}, + "6ba7edb6447b6a3e": {"kind":"literal","value":"share-viewer"}, "6bbe793f6a93dc8d": {"kind":"object","properties":[{"name":"blockedReason","type":"84ac8c1a66ffc697","optional":true},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"maxGoalRounds","type":"0657d3ca5ac4e7bf","optional":false},{"name":"objective","type":"4cd48f3e5108bd6e","optional":false},{"name":"phase","type":"b8327af7ee576795","optional":false},{"name":"revision","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "6bc8e51124d0874b": {"kind":"object","properties":[{"name":"message","type":"09f3f8dc417f2782","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "6c03546143cdf6ae": {"kind":"object","properties":[{"name":"kind","type":"e6411b09c1d982f7","optional":false}],"indices":[]}, @@ -958,7 +967,6 @@ "6ccb6bc423fb3952": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "6cda0d6d3b4336d8": {"kind":"literal","value":"feedback/message-put"}, "6cf082d0fb6034de": {"kind":"literal","value":"producer"}, - "6dacb1584f2bfbe2": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"68f44f59e91c4139","optional":false},{"name":"principal","type":"deebfc7c62177ea8","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "6e52dd55a1d48954": {"kind":"object","properties":[{"name":"agentPreset","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "6ea608bdc8a7a63d": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, "6ebcc2dbec9f2dab": {"kind":"literal","value":"skill-catalog"}, @@ -969,6 +977,7 @@ "6f72ece8f317cd08": {"kind":"object","properties":[{"name":"retry","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "6f9e00383836275e": {"kind":"literal","value":"snapshot"}, "70b082fe78991edf": {"kind":"literal","value":"request/context"}, + "70d2acdec1ebcfea": {"kind":"object","properties":[{"name":"data","type":"53d4f8256bc58109","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, "7180844e1066230c": {"kind":"literal","value":"service-stability"}, "719c51f67074f970": {"kind":"object","properties":[{"name":"kind","type":"2765e9119edf5816","optional":false},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"replayState","type":"bbc5696e3f13c758","optional":true}],"indices":[]}, "71bb3254ec8f1e49": {"kind":"object","properties":[{"name":"dt","type":"5d03ba38734809bc","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"texts","type":"93c33d9687613293","optional":false},{"name":"time0","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"688e853dc149f0d3","optional":false}],"indices":[]}, @@ -986,23 +995,27 @@ "7a651f3b672c52fb": {"kind":"literal","value":"session/title"}, "7af85bf70d4eafce": {"kind":"object","properties":[{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "7b1ce60193fa8b69": {"kind":"literal","value":"working-directory"}, + "7c203f3d4dfa6638": {"kind":"array","element":"4035903993dfee62"}, "7c6129b6961cf4b2": {"kind":"object","properties":[{"name":"messageId","type":"4cd48f3e5108bd6e","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "7c9f44e90a0058f4": {"kind":"object","properties":[{"name":"data","type":"5302e76315127761","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"570e850224346133","optional":false}],"indices":[]}, "7d226b9fd95b4ba2": {"kind":"union","types":["1df34a6c086866ff","cffcdc19881ab61e"]}, "7d54a5a5e629e2e7": {"kind":"object","properties":[{"name":"category","type":"3305a4bb6b6c29b4","optional":true},{"name":"text","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, "7d65acf0a3a43a8f": {"kind":"union","types":["1f0133217eeeffd8","277b61f9c6f81704","3c2535ca0a2062f1","4e44c9118152f682","83d6916021eede8f","85f9b505b7bdff1f","fb8b6a2e53cb9cf4"]}, "7df62978a2b7de85": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "7e9f1072c17f7aa8": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","a4afec59b75aad5d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, "7eaef7a763ba5b94": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, "7f13860ee89a2d35": {"kind":"literal","value":"session-reference"}, "7f7fd5a6b0064f59": {"kind":"object","properties":[{"name":"data","type":"5dd766ccd1702a11","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"10e386801903f9d4","optional":false}],"indices":[]}, "7f9365bae9fcb73e": {"kind":"object","properties":[{"name":"runId","type":"4cd48f3e5108bd6e","optional":false},{"name":"stopReason","type":"8465ece5e9d9007c","optional":false}],"indices":[]}, "7f959241c24128b4": {"kind":"object","properties":[{"name":"goalId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"b815d7b91cdd9fc6","optional":false},{"name":"revision","type":"0657d3ca5ac4e7bf","optional":false},{"name":"round","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "7ff62f6599c083ce": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"ff2cbb53582d1827","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "8064c03c87c357fd": {"kind":"union","types":["12d5808fac1996b9","43e311235aa537df","4e88776858652856","814d9434e1ddf907","8e611bf46821e500","c1f0f17b6493b85d","f2da0f27c1e6add5"]}, "80677c8c8b804189": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, "8067b0d232c350f1": {"kind":"literal","value":"time-context"}, "811229eedcec5797": {"kind":"literal","value":"allowed-once"}, "814d9434e1ddf907": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"3c2535ca0a2062f1","optional":false}],"indices":[]}, "82470020b5bfcb1a": {"kind":"literal","value":"agent-message"}, + "824a4aad44a54e24": {"kind":"union","types":["4d60dddaa65d3da8","deebfc7c62177ea8"]}, "825ca23416b1ea9d": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, "829a2fb3be6efd4b": {"kind":"literal","value":"agent/inbox/spliced"}, "83079c8a3f733ac3": {"kind":"object","properties":[{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, @@ -1029,7 +1042,6 @@ "8ad45ad053b4d89f": {"kind":"object","properties":[{"name":"decision","type":"4cd48f3e5108bd6e","optional":false},{"name":"durationMs","type":"0657d3ca5ac4e7bf","optional":false},{"name":"exitCode","type":"0657d3ca5ac4e7bf","optional":true},{"name":"handlerId","type":"4cd48f3e5108bd6e","optional":false},{"name":"point","type":"4cd48f3e5108bd6e","optional":false},{"name":"stderrSummary","type":"4cd48f3e5108bd6e","optional":true},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "8af841fc9b265ade": {"kind":"literal","value":"system"}, "8bb8c3751a6ce64b": {"kind":"object","properties":[{"name":"sessionFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"throughSeq","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "8bca2b4f53ec7745": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"b112459dcb068609","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "8c1c4f72755a78ea": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "8c627d7a45fa406c": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "8cdd3fdd295cee53": {"kind":"literal","value":"command/done"}, @@ -1050,12 +1062,10 @@ "94c56aeb1564ad73": {"kind":"literal","value":"in_progress"}, "95080295115fbd83": {"kind":"object","properties":[{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"5abab3a27fd27366","optional":false}],"indices":[]}, "95a3ffccbf3c8afc": {"kind":"literal","value":"owner"}, - "95acdbd066cf445d": {"kind":"object","properties":[{"name":"data","type":"12f9a5a7506bca46","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, "95c0e88fcf0bafb2": {"kind":"literal","value":"assistant"}, "9645cf93769a6dfb": {"kind":"literal","value":"user-question-reply"}, "96bfb916a727cd44": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, "96f4a9c81fc0f940": {"kind":"union","types":["3e2ece4967cfebe9","62ecb62dc0fe1b5e"]}, - "9808c3d0f0a1be8f": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"d2b617d9dfcc4e0a","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "9890113e97dd3541": {"kind":"object","properties":[{"name":"mode","type":"fab2bb09bc5e3b9d","optional":false},{"name":"source","type":"76632533f67879dc","optional":true}],"indices":[]}, "99135d8a39379779": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, "9947aa5938fe19a8": {"kind":"union","types":["576a2de8355eb098","a99ec0993c62c058"]}, @@ -1073,7 +1083,6 @@ "9e07693fd7cd9bab": {"kind":"literal","value":"ask"}, "9e2c06d8653585b3": {"kind":"array","element":"1cde1bedde79c5e8"}, "9e41386b3a0c9572": {"kind":"object","properties":[{"name":"end","type":"0657d3ca5ac4e7bf","optional":false},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "9e8043098fb89db6": {"kind":"object","properties":[{"name":"data","type":"c433ef580a5b229b","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, "9edc162949abf93b": {"kind":"object","properties":[{"name":"deliveryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"db6c5246911d47a7","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"ruleId","type":"4cd48f3e5108bd6e","optional":false},{"name":"source","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "9f65c43e340bba31": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, "a0a2e5c42e1c9294": {"kind":"object","properties":[{"name":"data","type":"2e3523541e942ef6","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"0e7b2a97f54f1afd","optional":false}],"indices":[]}, @@ -1091,6 +1100,7 @@ "a3c6f1ea38e9a61d": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "a3edd1efd1fef21b": {"kind":"literal","value":"workspace-write"}, "a40d12070f6f4a12": {"kind":"object","properties":[{"name":"task","type":"51edec100659da3a","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, + "a4afec59b75aad5d": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"68f44f59e91c4139","optional":false},{"name":"principal","type":"824a4aad44a54e24","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "a6567ccb2e530606": {"kind":"object","properties":[{"name":"data","type":"0d47a8f3d847243c","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"0a8a062bc9c3c0d9","optional":false}],"indices":[]}, "a6b41f5bede8094d": {"kind":"union","types":["15495614ccfa9cbd","aff60ef947c18b01"]}, "a6da9ad5d42ae83a": {"kind":"object","properties":[{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"runId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, @@ -1108,7 +1118,6 @@ "aa7bc2817014ca6c": {"kind":"object","properties":[{"name":"item","type":"3d7d07060a7be779","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "aa93010d0e521ad7": {"kind":"primitive","type":"boolean"}, "aacda84a69739238": {"kind":"literal","value":"subagent"}, - "aada2771a250136c": {"kind":"array","element":"8bca2b4f53ec7745"}, "aae343bc3d663c83": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, "ab7ec5f9eb2c6dfa": {"kind":"object","properties":[{"name":"kind","type":"68141cbe3994a5ff","optional":false}],"indices":[]}, "aba175fb20ffc711": {"kind":"array","element":"83079c8a3f733ac3"}, @@ -1127,7 +1136,6 @@ "b0127044ab31a702": {"kind":"object","properties":[{"name":"data","type":"ca59530d1a4f3e19","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1986c2664009c991","optional":false}],"indices":[]}, "b062078e899958f9": {"kind":"object","properties":[{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, "b0b0eb1b16490f6b": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mediaType","type":"2f1e6a7ebace43d7","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"originalDimensions","type":"0f46153645c29784","optional":true},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "b112459dcb068609": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6dacb1584f2bfbe2","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, "b158f3ff69237148": {"kind":"object","properties":[{"name":"contextWindow","type":"0657d3ca5ac4e7bf","optional":true},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"systemPromptUpdate","type":"f311671e07bc7fa6","optional":true}],"indices":[]}, "b1664fa89f8eab49": {"kind":"array","element":"bbc5696e3f13c758"}, "b1ebd5cda47d90d9": {"kind":"object","properties":[{"name":"member","type":"6598cd6da253948d","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, @@ -1180,7 +1188,6 @@ "c219efbdfd53b836": {"kind":"literal","value":"step/end"}, "c2deb7c9183735d4": {"kind":"union","types":["1ebc6f9ff3aa9e3a","2142e391b9ccb60d","b57a2889408de36e"]}, "c2f49004d9ae38a9": {"kind":"union","types":["1ebc6f9ff3aa9e3a","b57a2889408de36e","ef226a90ddac6cd8"]}, - "c433ef580a5b229b": {"kind":"object","properties":[{"name":"inserted","type":"aada2771a250136c","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, "c43b119ce579b3e9": {"kind":"literal","value":"paused"}, "c5228b7a55498d84": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "c55827fba7018198": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"9645cf93769a6dfb","optional":false},{"name":"outcome","type":"85d6d96ff8842974","optional":false}],"indices":[]}, @@ -1188,6 +1195,7 @@ "c589e938cc0e4989": {"kind":"literal","value":"tool/call"}, "c592ce75aab73fca": {"kind":"union","types":["0657d3ca5ac4e7bf","4cd48f3e5108bd6e","529e0ccd0e34079a","72ec79127a9c0d02","938f7c7357bc403e","aa93010d0e521ad7"]}, "c748fb42c80a1200": {"kind":"array","element":"86e1e384495ee09a"}, + "c78b9097bfae83cd": {"kind":"union","types":["09f3f8dc417f2782","4035903993dfee62","4b259f7f8f0a44ad","64ef542b1bfc9883","fa87537f4a9c1e25"]}, "c8e6f48878cc2a2f": {"kind":"literal","value":"active"}, "c99e20441bf15341": {"kind":"literal","value":"feedback/record"}, "ca085dba2c877fec": {"kind":"literal","value":"sandbox/mode"}, @@ -1199,14 +1207,14 @@ "cda2345a9be9d139": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "cdca80b8dad24d4e": {"kind":"literal","value":"tool-workflow/run-start"}, "cdee10e87dbed844": {"kind":"object","properties":[{"name":"delayMs","type":"0657d3ca5ac4e7bf","optional":false},{"name":"failure","type":"5f2117d1856a5ab4","optional":false},{"name":"maxRetries","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mode","type":"3bc287e765d58dc3","optional":false},{"name":"policyKey","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"retry","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "cebb7e9f3f663e66": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"b112459dcb068609","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "cdf988172b589b23": {"kind":"object","properties":[{"name":"data","type":"d4d960185026266e","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, "cef171adf021a186": {"kind":"object","properties":[{"name":"arguments","type":"bbc5696e3f13c758","optional":false},{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"error","type":"a81059ca06999da1","optional":true},{"name":"isError","type":"aa93010d0e521ad7","optional":false},{"name":"meta","type":"c592ce75aab73fca","optional":true},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"parentCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"rootCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"subCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "cffcdc19881ab61e": {"kind":"object","properties":[{"name":"cleared","type":"0b6d718c693fdf7f","optional":false},{"name":"clearedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"kind","type":"237c004c73b6b288","optional":false},{"name":"operation","type":"e7e154d47d5fbc53","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "d0fbb0ca1bbba059": {"kind":"literal","value":"delete"}, "d28765f87d6b6ab4": {"kind":"literal","value":"one-shot"}, - "d2b617d9dfcc4e0a": {"kind":"array","element":"4f9125fa028a5628"}, "d4b739491689cc6b": {"kind":"object","properties":[{"name":"block","type":"8064c03c87c357fd","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"eefbea819caeeea9","optional":false}],"indices":[]}, "d4c799e3d51cae10": {"kind":"literal","value":"working-directory/change"}, + "d4d960185026266e": {"kind":"object","properties":[{"name":"inserted","type":"7c203f3d4dfa6638","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, "d4e4d575abeb4b72": {"kind":"literal","value":"canceled"}, "d595ec73b32b016a": {"kind":"object","properties":[{"name":"data","type":"a40d12070f6f4a12","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"b57ae1b37e52bee0","optional":false}],"indices":[]}, "d63b8b8ffad9c02f": {"kind":"object","properties":[{"name":"data","type":"8bb8c3751a6ce64b","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"47e5d84f1a93450a","optional":false}],"indices":[]}, @@ -1262,7 +1270,6 @@ "e95f957154b2b1b5": {"kind":"literal","value":"assistant/attempt"}, "ea08ec1df0c33108": {"kind":"object","properties":[{"name":"message","type":"2eafefdc6f79a492","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, "eaa71be0fbf0f702": {"kind":"object","properties":[{"name":"kind","type":"ad3b56aa2fc1ad42","optional":false}],"indices":[]}, - "eaa9125aec0fb284": {"kind":"object","properties":[{"name":"data","type":"9808c3d0f0a1be8f","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, "ebb768d85ec87fb6": {"kind":"literal","value":"image/webp"}, "ec38b5949af8eaca": {"kind":"object","properties":[{"name":"data","type":"2fe5026daf14bd4f","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"2aed07493b1c65b6","optional":false}],"indices":[]}, "ec8b5ff7c77ca689": {"kind":"object","properties":[{"name":"kind","type":"b2245a906e13aa94","optional":false}],"indices":[]}, @@ -1313,6 +1320,7 @@ "fe1a7acdeed4d0f5": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "fe792a6cd4304945": {"kind":"literal","value":"tool-workflow/run-end"}, "fec5442b9ee4122e": {"kind":"object","properties":[{"name":"cacheReadTokens","type":"0657d3ca5ac4e7bf","optional":true},{"name":"cacheWriteTokens","type":"0657d3ca5ac4e7bf","optional":true},{"name":"inputTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"outputTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"reasoningTokens","type":"0657d3ca5ac4e7bf","optional":true},{"name":"totalTokens","type":"0657d3ca5ac4e7bf","optional":true}],"indices":[]}, - "fee1caf8ab6a65c1": {"kind":"literal","value":"instructions"} + "fee1caf8ab6a65c1": {"kind":"literal","value":"instructions"}, + "ff2cbb53582d1827": {"kind":"array","element":"c78b9097bfae83cd"} } } diff --git a/docs/subsystems/README.i18n.yaml b/docs/subsystems/README.i18n.yaml index 7f6b7de64c..069f78cc33 100644 --- a/docs/subsystems/README.i18n.yaml +++ b/docs/subsystems/README.i18n.yaml @@ -3,5 +3,5 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/README.md /subsystems: - en: dcbb2f834453e166 - zh: d73f32a7baeec42d + en: 81b2c48d922de5a9 + zh: cf0db50b34d2feb3 diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md index e14535c2f6..86e30a5519 100644 --- a/docs/subsystems/README.md +++ b/docs/subsystems/README.md @@ -52,6 +52,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures | [voice-input.md](voice-input.md) | experimental named recognizers, transient audio and revision-guarded draft insertion | | [translation.md](translation.md) | experimental anonymous text translation and explicit provider requests | | [agent-team.md](agent-team.md) | Agent Teams: implicit Lead identity, named continuable teammates, direct peer inbox delivery, and shared task DAG | +| [liveshare.md](liveshare.md) | the opt-in LiveShare bundle: nested share modes, a fork's ceiling and durability, enforcement installation, and the constraint a shared Session's externally driven turns run under | | [claude-code-mods.md](claude-code-mods.md) | Claude Code mods through the experimental bridge: every event, `$` member, chain rule, drawing feature, and example mod that differs from Claude Code | | [web.md](web.md) | the web access seam: `WebSearchRequest`/`Result`, `WebFetchRequest`/`Result`, `WebFetchBody`, provider availability, `WebError` | | [spill.md](spill.md) | the spill storage seam: `SaveTextSpill`, `SpillOwner`/`SpillSource`, `SpillRef`, the branded `SpillLocator` | diff --git a/docs/subsystems/README.zh.md b/docs/subsystems/README.zh.md index 6239e5f2c0..289a6a71f3 100644 --- a/docs/subsystems/README.zh.md +++ b/docs/subsystems/README.zh.md @@ -52,6 +52,7 @@ | [voice-input.md](voice-input.zh.md) | 实验性具名语音识别、临时音频和受版本保护的草稿插入 | | [translation.md](translation.zh.md) | 实验性免登录文本翻译与显式 Provider 请求 | | [agent-team.md](agent-team.zh.md) | Agent Teams:隐式 Lead 身份、具名 continuable teammate、直接 peer inbox 投递 与共享任务 DAG | +| [liveshare.md](liveshare.zh.md) | 可选的 LiveShare bundle:嵌套分享模式、fork 的上限与持久性、上限安装,以及共享会话外部驱动轮次所受的约束 | | [claude-code-mods.md](claude-code-mods.zh.md) | 经实验性桥接运行的 Claude Code 模组:与 Claude Code 不同的每个事件、`$` 成员、链规则、绘制特性与示例模组 | | [web.md](web.zh.md) | Web 访问 seam:`WebSearchRequest`/`Result`、`WebFetchRequest`/`Result`、`WebFetchBody`、提供方可用性、`WebError` | | [spill.md](spill.zh.md) | spill 存储 seam:`SaveTextSpill`、`SpillOwner`/`SpillSource`、`SpillRef`、品牌类型 `SpillLocator` | diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 02e7a5d4f6..a2b168f3fd 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -30,8 +30,8 @@ en: 057db6d8bf356c0f zh: 1441b39e5142b0bf /core/repo-wide-type-patterns/the-map-derived-union-pattern: - en: 84c039e929edb897 - zh: 84241bb993f23ef2 + en: 87a19311e2a97270 + zh: 8df92c72793e8275 /core/repo-wide-type-patterns/branded-ids: en: 10f8ac0f97e7c54a zh: 75cd2311aabfe464 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 86df19aa94..48ff2ce633 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -450,12 +450,13 @@ declare module '@deepseek-ai/dsh-llm' { } ``` -Five canonical maps use this pattern; a plugin author extends these: +Six canonical maps use this pattern; a plugin author extends these: | Map | Package | Derives | Catalog | |---|---|---|---| | `ContentBlockMap` | dsh-llm | `ContentBlock` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) | | `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) | +| `LlmRequestPurposeMap` | dsh-llm | `LlmRequestPurpose` | [llm-streaming.md](llm-streaming.md#the-model-request-and-result) | | `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.md#the-model-request-and-result) | | `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) | | `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) | diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index db4bb206f5..19acf51b51 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -460,12 +460,13 @@ declare module '@deepseek-ai/dsh-llm' { } ``` -五个规范 map 使用此模式;插件作者扩展它们: +六个规范 map 使用此模式;插件作者扩展它们: | Map | 包 | 派生 | 目录 | |---|---|---|---| | `ContentBlockMap` | dsh-llm | `ContentBlock` | [llm-streaming.md](llm-streaming.zh.md#content-blocks-and-messages) | | `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.zh.md#content-blocks-and-messages) | +| `LlmRequestPurposeMap` | dsh-llm | `LlmRequestPurpose` | [llm-streaming.zh.md](llm-streaming.zh.md#the-model-request-and-result) | | `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.zh.md#the-model-request-and-result) | | `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.zh.md) | | `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.zh.md) | diff --git a/docs/subsystems/forge.i18n.yaml b/docs/subsystems/forge.i18n.yaml index 8c50e6a322..92bc2298cd 100644 --- a/docs/subsystems/forge.i18n.yaml +++ b/docs/subsystems/forge.i18n.yaml @@ -12,11 +12,11 @@ en: 26ccfa2e342e3e67 zh: fe6cc2a622757c48 /forges/change-request-actions: - en: 00e4490e416099d2 - zh: 5724d08b6ae230f7 + en: 87d2c81d9514267f + zh: 1daf31c5b0127a1c /forges/session-bindings: - en: 4d65f16562d67181 - zh: 676e411b32bc7615 + en: 13dfd36f2166bba0 + zh: a369434f52f04d57 /forges/panel: - en: 7a9044e9b9f69545 - zh: 5d25017909e3bb28 + en: 9a84a26d97cb46f0 + zh: 5e1e03b7714bcbae diff --git a/docs/subsystems/forge.md b/docs/subsystems/forge.md index 1e24d7984f..67331f49f0 100644 --- a/docs/subsystems/forge.md +++ b/docs/subsystems/forge.md @@ -45,6 +45,8 @@ type ChangeRequestKey = Branded<'ChangeRequestKey'> interface ForgeProvider { /** Forge family name stored in every repository this provider returns. */ readonly forge: string + /** Publish a comment or completed review. Omission declares that this provider cannot write reviews. */ + submitReview?(ref: ChangeRequestRef, request: ForgeReviewRequest, signal: AbortSignal): Promise /** Map a Git remote URL to its repository, or undefined when this provider does not serve it. */ repositoryFromRemote(url: string): ForgeRepository | undefined /** Map a change request web URL to its reference, or undefined when this provider does not serve it. */ @@ -246,9 +248,40 @@ interface ForgeActionFailure { } ``` +`ctx.forge.submitReview` delegates an explicit comment or completed review to the provider. A provider that omits this capability returns unsupported. Consumers retain pending drafts and reconcile unknown write outcomes before resubmitting. + +```ts type-equiv +/** One inline comment addressed to the old or new side of a pull request diff. */ +interface ForgeLineComment { + readonly path: string + readonly side: 'L' | 'R' + readonly startLine: number + readonly endLine: number + readonly body: string +} +``` + +```ts type-equiv +/** A single published comment or one completed review containing all pending comments. */ +type ForgeReviewRequest = { + /** Commit the user reviewed; providers must check it before writing and address that commit explicitly. */ + readonly headSha: string +} & ( + | { readonly kind: 'comment'; readonly comment: ForgeLineComment } + | { readonly kind: 'review'; readonly event: 'comment' | 'approve' | 'request-changes'; readonly body: string; readonly comments: readonly ForgeLineComment[] } +) +``` + +```ts type-equiv +/** Result of a review write; unknown outcomes must be checked on the host before another submission. */ +type ForgeReviewResult = + | { readonly ok: true; readonly url: string } + | { readonly ok: false; readonly code: 'unsupported' | 'stale' | 'failed' | 'unknown'; readonly message: string } +``` + ## Session bindings -`ctx.forgeSession` derives each Session's bindings from its own log: `gh pr create` results bind the URLs they print, `gh pr view`, `gh pr edit`, and `gh pr checkout` calls bind the change request they act on, and `git push` results trigger head-branch discovery through `ctx.forge`; the panel also reports the branch its checkout tracks for the same discovery. Track, dismiss, and discovery actions are `plugin:forge-session/binding` records, which stay outside the model-visible conversation. The `forgeChangeRequests` projection stays on the Host. The [Session binding README](../../packages/experimental/forge-session/README.md#use-this-package) owns the recognition and discovery rules. +`ctx.forgeSession` derives each Session's bindings from its own log: `gh pr create` results bind the URLs they print, `gh pr view`, `gh pr edit`, and `gh pr checkout` calls bind the change request they act on, and `git push` results trigger head-branch discovery through `ctx.forge`; the panel also reports the branch its checkout tracks for the same discovery. Track, dismiss, and discovery actions are `plugin:forge-session/binding` records, which stay outside the model-visible conversation. The `forgeChangeRequests` projection's client view, which the Session summary carries, lists the bindings without their status. The [Session binding README](../../packages/experimental/forge-session/README.md#use-this-package) owns the recognition and discovery rules. ```ts type-equiv /** @@ -506,6 +539,32 @@ interface ForgeBranches { } ``` +The Session list reads through the same Remote. `changeRequestStates` streams `ForgeChangeRequestStatesFrame` values: a `baseline` with the last known `ForgeChangeRequestListState` of every change request that a listed Session the caller may read binds, then the states that change. One Host run serves every stream; it takes the bindings of loaded Sessions from `ctx.forgeSession` and those of other Sessions from their cached Session summary without loading them, keeps each bound change request subscribed, including closed requests polled at the idle cadence to detect reopening, and stores every change, so a restart shows the last known states before the next poll. The Session summary also carries the `forgeWorktree` projection's client view: the created worktree while the Session's directory is that worktree or lies below it, else null. + +```ts type-equiv +/** + * The last polled state of one change request that a listed Session binds, as + * the Session list shows it. + */ +type ForgeChangeRequestListState = Pick< + ChangeRequestSnapshot, 'url' | 'title' | 'state' | 'draft' | 'checksRollup' | 'reviewDecision' | 'mergeability' +> & { + /** Change request number on its code host. */ + readonly number: number +} +``` + +```ts type-equiv +/** + * One frame of `forgePanel.changeRequestStates`: the known state of every + * change request the reader may see first, then the states that changed or + * became visible since the previous frame. + */ +type ForgeChangeRequestStatesFrame = + | { readonly type: 'baseline'; readonly states: readonly ForgeChangeRequestListState[] } + | { readonly type: 'changed'; readonly states: readonly ForgeChangeRequestListState[] } +``` + The changes view reads through the same Remote. `localDiff` reads one `ForgeLocalScope` of the Session's checkout, and `changeRequestDiff` reads a bound change request at the head of its latest status through the provider's `diff`; both return files that the Host parsed into `ForgeDiffFile` hunks. `localText` and `changeRequestText` read the new side of one file, which supplies the unchanged lines between hunks. Each inspection also sizes the `branch` scope as `ForgePanelState.local` for the panel's local changes card. ```ts type-equiv @@ -718,6 +777,15 @@ async merge(ref: ChangeRequestRef, request: MergeRequest, signal: AbortSignal): */ async diff(ref: ChangeRequestRef, signal: AbortSignal): Promise +/** + * Publish one comment or a completed review, then refresh the watched status. + * @param ref - change request served by a registered provider. + * @param request - explicitly selected commit, comments, and review decision. + * @param signal - request cancellation. + * @returns a publication URL or a failure, including an unknown write outcome. + */ +async submitReview(ref: ChangeRequestRef, request: ForgeReviewRequest, signal: AbortSignal): Promise + /** * Read the text of one file at a commit of a change request's repository. * @param ref - change request served by a registered provider. @@ -785,6 +853,20 @@ Serves the forge panel. Following a Session inspects the checkout of its current */ @Remote({ mode: 'stream' }) async *follow(session: Session, signal: AbortSignal): AsyncIterable +/** + * Stream the last known state of every change request that a listed + * Session the caller may read binds: the stored states first, then each + * state a poll changes or a new binding reveals. Listing reads no Session + * log: loaded Sessions report their bindings, others the cached Session + * summary, and a Session without one shows nothing until it is opened. + * Closed requests are polled at the idle cadence so reopening is observed. The + * stream ends when `signal` aborts or the Remote stops. + * @param signal - ends the stream. + * @returns frames in publication order. + * @throws when the Host lacks the Session query, projection cache, storage, or Session store service. + */ +@Remote({ mode: 'stream' }) async *changeRequestStates(signal: AbortSignal): AsyncIterable + /** * List the local branches that can start a worktree of the Session's current checkout. * @param session - Session whose current directory is listed. @@ -875,6 +957,16 @@ Serves the forge panel. Following a Session inspects the checkout of its current */ @Remote async changeRequestText(session: Session, url: string, commit: string, path: string, signal: AbortSignal): Promise +/** + * Publish a line comment or completed review for a visible Session binding. + * @param session - Session retaining the change request. + * @param url - visible binding URL. + * @param request - reviewed head, ranges, plain text, and explicit review decision. + * @param signal - request cancellation. + * @returns confirmed publication or a typed failure; unknown outcomes require reconciliation on the host. + */ +@Remote async submitReview(session: Session, url: string, request: ForgeReviewRequest, signal: AbortSignal): Promise + /** * Mark a draft change request of a Session ready for review; the change * request is polled again once the host answers. diff --git a/docs/subsystems/forge.zh.md b/docs/subsystems/forge.zh.md index d426886829..2d726ca39d 100644 --- a/docs/subsystems/forge.zh.md +++ b/docs/subsystems/forge.zh.md @@ -45,6 +45,8 @@ type ChangeRequestKey = Branded<'ChangeRequestKey'> interface ForgeProvider { /** Forge family name stored in every repository this provider returns. */ readonly forge: string + /** Publish a comment or completed review. Omission declares that this provider cannot write reviews. */ + submitReview?(ref: ChangeRequestRef, request: ForgeReviewRequest, signal: AbortSignal): Promise /** Map a Git remote URL to its repository, or undefined when this provider does not serve it. */ repositoryFromRemote(url: string): ForgeRepository | undefined /** Map a change request web URL to its reference, or undefined when this provider does not serve it. */ @@ -246,9 +248,40 @@ interface ForgeActionFailure { } ``` +`ctx.forge.submitReview` 将明确指定的评论或完整评审交给 provider;未实现此能力的 provider 返回不支持。消费者保存待提交草稿,并在再次提交前核对结果不明的写操作。 + +```ts type-equiv +/** One inline comment addressed to the old or new side of a pull request diff. */ +interface ForgeLineComment { + readonly path: string + readonly side: 'L' | 'R' + readonly startLine: number + readonly endLine: number + readonly body: string +} +``` + +```ts type-equiv +/** A single published comment or one completed review containing all pending comments. */ +type ForgeReviewRequest = { + /** Commit the user reviewed; providers must check it before writing and address that commit explicitly. */ + readonly headSha: string +} & ( + | { readonly kind: 'comment'; readonly comment: ForgeLineComment } + | { readonly kind: 'review'; readonly event: 'comment' | 'approve' | 'request-changes'; readonly body: string; readonly comments: readonly ForgeLineComment[] } +) +``` + +```ts type-equiv +/** Result of a review write; unknown outcomes must be checked on the host before another submission. */ +type ForgeReviewResult = + | { readonly ok: true; readonly url: string } + | { readonly ok: false; readonly code: 'unsupported' | 'stale' | 'failed' | 'unknown'; readonly message: string } +``` + ## Session 绑定 -`ctx.forgeSession` 从每个 Session 自己的日志派生其绑定:`gh pr create` 的结果绑定其输出的 URL,`gh pr view`、`gh pr edit` 与 `gh pr checkout` 调用绑定它们所操作的变更请求,`git push` 的结果通过 `ctx.forge` 触发按 head 分支的发现;面板也会报告其检出所跟踪的分支,触发同样的发现。track、dismiss 与发现操作都是 `plugin:forge-session/binding` 插件记录,位于模型可见对话之外。`forgeChangeRequests` 投影只位于宿主侧。识别与发现规则见 [Session 绑定 README](../../packages/experimental/forge-session/README.zh.md#use-this-package)。 +`ctx.forgeSession` 从每个 Session 自己的日志派生其绑定:`gh pr create` 的结果绑定其输出的 URL,`gh pr view`、`gh pr edit` 与 `gh pr checkout` 调用绑定它们所操作的变更请求,`git push` 的结果通过 `ctx.forge` 触发按 head 分支的发现;面板也会报告其检出所跟踪的分支,触发同样的发现。track、dismiss 与发现操作都是 `plugin:forge-session/binding` 插件记录,位于模型可见对话之外。`forgeChangeRequests` 投影的客户端视图随 Session 摘要下发,列出绑定但不含其状态。识别与发现规则见 [Session 绑定 README](../../packages/experimental/forge-session/README.zh.md#use-this-package)。 ```ts type-equiv /** @@ -506,6 +539,32 @@ interface ForgeBranches { } ``` +会话列表经同一个 Remote 读取。`changeRequestStates` 以流的形式推送 `ForgeChangeRequestStatesFrame`:先是一个 `baseline`,包含调用方可读的已列出 Session 所绑定的每个变更请求最近一次已知的 `ForgeChangeRequestListState`,之后是发生变化的状态。宿主用一次运行服务所有流:已加载 Session 的绑定取自 `ctx.forgeSession`,其他 Session 的绑定取自缓存的 Session 摘要而不加载它们;每个已绑定的变更请求保持订阅,已关闭的按空闲间隔轮询以检测重新打开,每次变化都会存储,因此重启后在下次轮询之前就能显示最近已知的状态。Session 摘要还带有 `forgeWorktree` 投影的客户端视图:Session 的目录就是所创建的 worktree 或位于其下时为该 worktree,否则为 null。 + +```ts type-equiv +/** + * The last polled state of one change request that a listed Session binds, as + * the Session list shows it. + */ +type ForgeChangeRequestListState = Pick< + ChangeRequestSnapshot, 'url' | 'title' | 'state' | 'draft' | 'checksRollup' | 'reviewDecision' | 'mergeability' +> & { + /** Change request number on its code host. */ + readonly number: number +} +``` + +```ts type-equiv +/** + * One frame of `forgePanel.changeRequestStates`: the known state of every + * change request the reader may see first, then the states that changed or + * became visible since the previous frame. + */ +type ForgeChangeRequestStatesFrame = + | { readonly type: 'baseline'; readonly states: readonly ForgeChangeRequestListState[] } + | { readonly type: 'changed'; readonly states: readonly ForgeChangeRequestListState[] } +``` + 变更视图经同一个 Remote 读取。`localDiff` 读取 Session 检出的某个 `ForgeLocalScope`;`changeRequestDiff` 经提供方的 `diff` 读取已绑定变更请求在其最新状态 head 处的变更;两者返回的文件都已在宿主上解析为 `ForgeDiffFile` 的 hunk。`localText` 与 `changeRequestText` 读取一个文件的新版本,用来补出 hunk 之间未变更的行。每次检查还会把 `branch` 范围的规模统计为 `ForgePanelState.local`,供面板的变更卡片使用。 ```ts type-equiv @@ -718,6 +777,15 @@ async merge(ref: ChangeRequestRef, request: MergeRequest, signal: AbortSignal): */ async diff(ref: ChangeRequestRef, signal: AbortSignal): Promise +/** + * Publish one comment or a completed review, then refresh the watched status. + * @param ref - change request served by a registered provider. + * @param request - explicitly selected commit, comments, and review decision. + * @param signal - request cancellation. + * @returns a publication URL or a failure, including an unknown write outcome. + */ +async submitReview(ref: ChangeRequestRef, request: ForgeReviewRequest, signal: AbortSignal): Promise + /** * Read the text of one file at a commit of a change request's repository. * @param ref - change request served by a registered provider. @@ -785,6 +853,20 @@ Serves the forge panel. Following a Session inspects the checkout of its current */ @Remote({ mode: 'stream' }) async *follow(session: Session, signal: AbortSignal): AsyncIterable +/** + * Stream the last known state of every change request that a listed + * Session the caller may read binds: the stored states first, then each + * state a poll changes or a new binding reveals. Listing reads no Session + * log: loaded Sessions report their bindings, others the cached Session + * summary, and a Session without one shows nothing until it is opened. + * Closed requests are polled at the idle cadence so reopening is observed. The + * stream ends when `signal` aborts or the Remote stops. + * @param signal - ends the stream. + * @returns frames in publication order. + * @throws when the Host lacks the Session query, projection cache, storage, or Session store service. + */ +@Remote({ mode: 'stream' }) async *changeRequestStates(signal: AbortSignal): AsyncIterable + /** * List the local branches that can start a worktree of the Session's current checkout. * @param session - Session whose current directory is listed. @@ -875,6 +957,16 @@ Serves the forge panel. Following a Session inspects the checkout of its current */ @Remote async changeRequestText(session: Session, url: string, commit: string, path: string, signal: AbortSignal): Promise +/** + * Publish a line comment or completed review for a visible Session binding. + * @param session - Session retaining the change request. + * @param url - visible binding URL. + * @param request - reviewed head, ranges, plain text, and explicit review decision. + * @param signal - request cancellation. + * @returns confirmed publication or a typed failure; unknown outcomes require reconciliation on the host. + */ +@Remote async submitReview(session: Session, url: string, request: ForgeReviewRequest, signal: AbortSignal): Promise + /** * Mark a draft change request of a Session ready for review; the change * request is polled again once the host answers. diff --git a/docs/subsystems/liveshare.i18n.yaml b/docs/subsystems/liveshare.i18n.yaml new file mode 100644 index 0000000000..e1ba0426b4 --- /dev/null +++ b/docs/subsystems/liveshare.i18n.yaml @@ -0,0 +1,25 @@ +# Bilingual-pair consistency record for liveshare.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 docs/subsystems/liveshare.md +/liveshare: + en: 196b06ffccedbdef + zh: 0ca8df19400afc40 +/liveshare/enabling-and-visibility: + en: f6144f8e81359a9c + zh: 1924869ac7475b59 +/liveshare/share-modes: + en: 985cb7a5c20c1cbd + zh: a5f5372dddfefaa3 +/liveshare/forks-and-their-ceiling: + en: 357a2147f48f5bac + zh: a8a297603f984ede +/liveshare/retries-recovery-and-revocation: + en: 6b1346e73ab6eab9 + zh: 888cfc08c6bc8522 +/liveshare/constraining-a-shared-session: + en: 289e8955320935f6 + zh: 7038c4283a4f33c6 +/liveshare/viewer-states: + en: b5b4d4d83dbb58c2 + zh: 498f26938dc872df diff --git a/docs/subsystems/liveshare.md b/docs/subsystems/liveshare.md new file mode 100644 index 0000000000..78c59825ee --- /dev/null +++ b/docs/subsystems/liveshare.md @@ -0,0 +1,158 @@ +# LiveShare + +English | [中文](liveshare.zh.md) + +The opt-in LiveShare bundle (`@deepseek-ai/dsh-experimental-liveshare`) lets one owner share a single Session with an external viewer through the separate Liveshare server. A viewer never receives the owner's tools: the owner host reads each viewer request itself, dispatches it as that viewer's own Peer, whose `share-viewer` principal the Gateway decides through the [remote access](../../packages/api/remote-access/README.md) policy this package registers, and constrains everything a shared Session runs on the viewer's behalf to the workspace the share granted. The [package README](../../packages/experimental/liveshare/README.md) owns configuration, the client faces, and the limitations; the [Owner note](../../.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.md) owns the security posture behind them. + +Source: [`packages/experimental/liveshare/src/host/fork-enforcement.ts`](../../packages/experimental/liveshare/src/host/fork-enforcement.ts) + + + +## Enabling and visibility + +The package ships with dsh but stays out of the plugin manager's lists until a profile enables it. A profile either inserts the two rows the bundle declares into its own `cordis.patch.yml`, which keeps the bundle unselected while its plugins run, or selects the bundle in `dsh.profile.bundles`. The [package README](../../packages/experimental/liveshare/README.md#default-visibility-and-enabling) owns the exact entries and the placeholders a deployment replaces. + +The plugin manager lists the card under **Experimental plugins** only while at least one row the bundle declares runs, and removing the rows, disabling them, or deselecting the bundle takes it out of both that list and **More** again. Both routes apply on the profile's next recomposition rather than mid-task: with HMR enabled the profile is re-read, and without it the change waits for the next start. + +## Share modes + +`ViewerSessionMode` has two tiers, and both name a sandbox: `view` reads the shared Session and drives its loop inside a read-only sandbox in the granted workspace, so it never writes a file; it may use the read and search surface (`read`, `read_image`, `glob`, `grep`, `present`) and the tools with no local file effect (`todo_write`, `skill`, `web_search`, `web_fetch`), with every declared path checked against the grant. `write` runs in a bounded writable sandbox that also modifies files inside that workspace by adding bounded `edit` and `write`, under the Host's write ceiling. Forking is not a tier: the share carries its own `allowsFork` switch, independent of the tier. Attachment upload is not a tier either: every Session a share serves the reader accepts files, whatever the tier and the fork switch. The withdrawn `fork-readonly` name is not read back as `view`: a persisted owner record that names it is rejected when the owner state loads. Nothing infers `write`, and a share whose mode the host cannot establish stays readable but is never driven. + +## Forks and their ceiling + +A fork creates a new Session the viewer owns and leaves the source untouched. Fork authorization checks the share, its fork switch, expiry, viewer admission, source identity, and the requested boundary. `READ_ONLY_FORK_CEILING` permits the view surface — `read`, `read_image`, `glob`, `grep`, `present`, `todo_write`, `skill`, `web_search`, `web_fetch` — and `WRITE_FORK_CEILING` adds the Host’s bounded `edit` and `write`. Each path-bearing tool gets a confined replacement that resolves every declared path against the granted root and delegates with its canonical form; `bash`, `pwsh`, `cua`, `plugin_manager`, the `schedule_*` tools, and every name the surface does not list are refused. + +Each child is recorded with the share that authorized it, the trusted viewer the Liveshare server asserted, and the boundary it started from, so a viewer's list shows only its own children and a removed share stops them. A child is driven only after its ceiling is installed on its own scope: `ForkRegistry` refuses every prompt for a child whose guard is not installed (`fork-ceiling-uninstalled`), and installation is verified per child rather than per host. A child whose scope refuses the confined replacement or the limit is refused instead of being published with the deployment's own tools. + +Cold source and child activation uses the Host’s ordinary Agent lookup, not a Remote `session/prepare` endpoint. Serial `agent/created` listeners install scoped enforcement before queued work is released; an already live Agent needs its own installation check. `forkLineage` is a LiveShare-owned, per-viewer UI projection, not a Session-summary field, durable log event, or permission grant. + +## Retries, recovery, and revocation + +One logical fork request may be sent more than once. Its identity binds the trusted viewer, the share, the source, and the caller's operation id; the boundary is compared against that identity rather than folded into it, so a retry repeats the child it already produced, an identity naming another boundary conflicts instead of silently receiving the earlier child, and the reservation is durable before any child exists. The host records the child the owner's fork answer returned, and a retry of a recorded operation replays that answer. The owner's `SessionForkRequest` mints the child id, so a host that dies between creating the child and recording it cannot name that child on retry and creates a second one; closing that window needs a Session Controller request that accepts a caller-supplied child identity. + +Child tool guards recheck the share’s current mode, expiry, viewer admission, and revocation record. Viewer withdrawal cancels affected child work and pending calls. A `write` to `view` downgrade denies later writes through the write guard without granting any new tool, and does not promise immediate cancellation of a running turn. Removing a share revokes recorded children and stops the Session's running turn; the source enforcement layer is released once no share over the Session is left, and with Agent disposal. Revoked children retain their logs, and the source’s owner-visible history is never rewritten. + +## Constraining a shared Session + +Every share lets its viewer drive the *source* Session through the normal prompt path, and any share at either tier installs the bounded sandbox while it is active. `installSourceSessionAccess` registers a confined replacement for every view-surface tool the deployment ships in that Session's own scope, and bounded `edit` and `write` replacements whenever any share over the Session allows writing. The layer is mounted on the Session, but it confines only the turns a share viewer starts: `agent/pre-step` records, in Host memory, whether a turn's admitted input names a share viewer, and the guard and the replacements read that answer for every execution of that turn. A viewer-started turn runs those replacements and nothing else; a turn the owner starts keeps the Session's own tools, including `bash` and its unrestricted filesystem tools. The answer is decided when the turn is admitted and never written to the Session log; a restart recomputes it from the next admitted step. An owner's steering input inside a viewer's turn stays confined with it. Stopping every share over the Session disposes the layer, which restores the Session's own tools; narrowing the last write share to `view` re-installs the read-only layer. + +The installation reports whether the source Session is actually confined. A deployment without the `read` tool, or a scope that refuses that replacement's registration, leaves the guard in place and reports the refusal; a tool whose own replacement failed stays refused while the rest of the surface runs, and the reader refuses the prompt with `source-write-unavailable`: no viewer-started turn of a shared Session is driven against the owner's own tools because a limit could not be installed. + +Each operation re-reads the Relay grants of the shares that keep the Session drivable, and each reading is synchronous with the tool call it authorizes, so input queued past the grant that admitted it cannot act on that stale admission. The layer is per Session: several shares over one Session share the one installation, and a share that narrows or ends changes what the layer permits — its bounded write is refused once no active share may write, and the layer is released once no share is left. The Host does not attribute a running turn to the viewer that admitted it, so ending one share's grant stops the Session's running turn rather than singling out that viewer's work; the in-memory viewer-start marker only decides which tool surface that turn runs. + +Confined file tools resolve every path their own arguments declare, check that the granted root contains each result, and delegate with the target’s canonical path. A pathless `glob` or `grep` searches the Session’s working directory, which the replacement supplies and checks like a named path; a `present` call checks every file it lists; the tools with no local file effect are delegated unchanged. Bounded edits run the same containment check, use `fs/edit-intent`, and publish the version `editText` returns; bounded writes use `fs/write-intent` and publish the version `writeText` returns. After the asynchronous intent wait, the Host resolves the current Session sandbox policy and rechecks share authority. It passes that policy as the fifth `editText` or `writeText` argument, after `signal`, so the sandboxing backend fences the mutation by it. Containment is checked by path, so a directory inside the grant that a local process replaces with a symlink between the check and the open can redirect the operation; anchoring operations to a directory handle is deferred to a separate change. + +A viewer's source prompt and source stop both travel the standard Gateway dispatch under the `share-viewer` policy's source-write role: the policy admits them while the share's tier is established and its Session is confined, and the owner's own Session controller then stops whatever turn the Session is running. Cancellation performs no per-viewer comparison, so a reader's stop of the shared Session also ends a turn the owner started. The [Session Controller](../../packages/api/session-controller/README.md) reads `remoteAccess.allows()` again inside `prompt` immediately before delivery and in the same synchronous step as a `cancel`, so a viewer whose access ends after the Gateway admitted its call delivers or cancels nothing. Queued external source input refreshes Liveshare server authority in `agent/pre-step`; missing authority rejects the step before a model request. + + + +## Viewer states + +A viewer read that ends in a refusal names its own condition rather than one failure for every cause: the Liveshare server answers a missing link, a share the owner stopped, an expired share, an owner whose machine holds no route, an expired sign-in, a reader who has not asked, a request still waiting, a request the owner refused, and a notification the owner never received each with their own status and code, and a page that cannot reach the Liveshare server states that itself. `VIEWER_STATE_ANSWERS` in `@deepseek-ai/dsh-experimental-liveshare-protocol` is the whole vocabulary, and the [shared protocol](../../packages/experimental/liveshare-protocol/README.md) owns the answer each condition carries. No answer widens what a viewer may read: an offline owner, a stopped share, and a refused request all deny. + +The workspace feed a viewer's page renders is filtered through the same policy as its session list: a workspace is delivered while it holds the shared Session or a branch that viewer made from it, and its membership and its manual order keep only those. A workspace that loses them leaves the feed when that workspace or the workspace order next changes, and the stream then replaces the reader's baseline; an access change alone sends no frame. A reader therefore sees the owner's workspace names and grouping around the one Session it was granted, never the rest of the owner's workspaces. + +Viewer streams bind the opening actor across polling, replacement, and cancellation. Session notifications, control baselines, and workspace membership expose only the source and that actor’s children: the Gateway forwards a reader only the `api-session/*` notifications the policy allows on those Sessions, and the Session and Workspace controllers filter their lists and streams through the same policy; interactive approvals and questions stay on the Host. Refused reads open no upstream subscription. Expiry and revocation cancel affected pending calls and streams; unpolled streams are reclaimed after 120 seconds. Worker IPC responses and cancellations must match the request’s token, share id, Liveshare server id, and original socket or worker generation; late answers cannot settle a replacement request. + +The Liveshare server’s optional `share-access-state` capability publishes a complete, versioned authority snapshot on connection and committed mutations, including mutations on another Liveshare server instance. Authenticated `GET /__liveshare/requests` returns the same fields. The Host clears cached authority on connection loss, rejects stale revisions within a generation, and fences asynchronous refreshes against generation changes. Missing complete authority refuses admission; this does not establish immediate distributed stop semantics. + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.liveshareShares` — `LiveshareShares` + +Owner-only Remote surface over the share-management operations. + +The Gateway decides each call through remote access, so a `share-viewer` Peer is refused before a method runs; the owner Peer reaches the one implementation that owns each share-management side effect. `create` takes its Session as the `sessionId` of its single request object, so the Session resource kind's default resolver names that Session for the call without a reader declared here. + +```ts cordis-catalog +/** + * Publish one Session and store the share. + * @param request - Session, title, access, lifetime, and both switches the owner chose. + * @returns The created share's link and stored values. + */ +@Remote('create') create(request: CreateShareRequest): Promise + +/** + * Refresh titles and Liveshare server authority, then list this host's shares. + * @returns Every share this host publishes. + */ +@Remote('list') list(): Promise + +/** + * Stop one share locally, revoke its forks, and delete its row on the Liveshare server when reachable. + * @param request - Share to stop. + * @returns Whether the row on the Liveshare server still needs a retry. + */ +@Remote('stop') stop(request: StopShareRequest): Promise + +/** + * Change one share's tier, fork switch, or both. + * @param request - Share and the switches to change. + * @returns The permissions the share now carries. + */ +@Remote('updatePermissions') updatePermissions(request: UpdateSharePermissionsRequest): Promise + +/** + * Replace one share's exposed plugin endpoints. + * @param request - Share and the selection to store. + * @returns The capability selection the share now carries. + */ +@Remote('setCapabilities') setCapabilities(request: SetShareCapabilitiesRequest): Promise + +/** + * Read the endpoints plugins currently offer. + * @returns The live offer list. + */ +@Remote('capabilities') capabilities(): ShareCapabilityOffers + +/** + * Read the Liveshare server address the owner saved. + * @returns The current Liveshare server state, with both address fields absent while none is saved. + */ +@Remote('relay') relay(): OwnerRelayState + +/** + * Choose the Liveshare server address later shares publish to. + * @param request - Normalized address or null to clear the saved one. + * @returns The Liveshare server state after the change. + */ +@Remote('setRelay') setRelay(request: SetRelayRequest): Promise + +/** + * Report this host's owner identity for one Liveshare server address. + * @param request - Address to report, or the effective one when absent. + * @returns The owner identity status, without the credential. + */ +@Remote('identity') identity(request: OwnerIdentityRequest): Promise + +/** + * Read the Liveshare server's durable audit trail for one share. + * @param request - Share whose audit is read. + * @returns The audit events, or a structured failure when the Liveshare server cannot answer. + */ +@Remote('audit') audit(request: ShareAuditRequest): Promise + +/** + * Check that the effective Liveshare server answers at all. + * @returns The anonymous connectivity result. + */ +@Remote('testRelay') testRelay(): Promise + +/** + * Register one throwaway share to check whether this host may publish. + * @param request - Session the throwaway registration names. + * @returns The Liveshare server's decision about this host's credential. + */ +@Remote('testPublish') testPublish(request: TestPublishRequest): Promise +``` + +Source: [`packages/experimental/liveshare/src/host/share-remote.ts`](../../packages/experimental/liveshare/src/host/share-remote.ts) + diff --git a/docs/subsystems/liveshare.zh.md b/docs/subsystems/liveshare.zh.md new file mode 100644 index 0000000000..e794ce64cf --- /dev/null +++ b/docs/subsystems/liveshare.zh.md @@ -0,0 +1,158 @@ +# LiveShare + +[English](liveshare.md) | 中文 + +可选的 LiveShare bundle(`@deepseek-ai/dsh-experimental-liveshare`)让一位所有者通过独立的 Liveshare 服务端 把一个会话分享给外部 viewer。viewer 永远拿不到所有者的工具:所有者宿主机自己读取 viewer 的每个请求,以该 viewer 自己的 Peer 分派它,Gateway 依据本包注册的 [remote access](../../packages/api/remote-access/README.zh.md) 策略判定该 Peer 的 `share-viewer` principal,并把共享会话代表 viewer 运行的一切限制在分享所授予的工作区内。[包 README](../../packages/experimental/liveshare/README.zh.md)负责配置、客户端界面与限制;[Owner 说明](../../.agents/notes/implemented/feature/2026-09-13-liveshare-protocol-and-owner-surfaces.zh.md)负责其背后的安全姿态。 + +源码:[`packages/experimental/liveshare/src/host/fork-enforcement.ts`](../../packages/experimental/liveshare/src/host/fork-enforcement.ts) + + + +## 启用与可见性 + +本包随 dsh 一同交付,但在 profile 启用它之前不会出现在插件管理器的任何列表中。profile 可以把自己的 `cordis.patch.yml` 插入组合包声明的两条行,这样组合包本身保持未选中而插件照常运行;也可以改在 `dsh.profile.bundles` 中选中该组合包。[包 README](../../packages/experimental/liveshare/README.zh.md#default-visibility-and-enabling)负责具体条目和部署需要替换的占位符。 + +插件管理器只有在组合包声明的行至少有一条在运行时才把卡片列在**实验性插件**下;删除这些行、把它们置为 disabled,或取消选中组合包,卡片会再次从该列表和**更多**页消失。两条路线都在 profile 下一次重新组合时生效,而不是在当前任务中途:启用 HMR 时会重新读取 profile,未启用时则等下一次启动。 + +## 分享模式 + +`ViewerSessionMode` 只有两档,两档都对应一个沙箱:`view` 读取共享会话,并在授予工作区的只读沙箱里驱动它的 loop,绝不写文件;它可以使用读取与检索面(`read`、`read_image`、`glob`、`grep`、`present`)和无本地文件副作用的工具(`todo_write`、`skill`、`web_search`、`web_fetch`),每个声明路径都按授予范围检查。`write` 在受宿主写入上限约束的可写沙箱里运行,通过有界 `edit` 与 `write` 修改该工作区内的文件。fork 不是一档:分享自带独立的 `allowsFork` 开关,与档位无关。附件上传也不是一档:分享向读者提供的每条会话都接受文件,与档位和分叉开关无关。已退场的 `fork-readonly` 名称不会读回为 `view`:持久化的 owner 记录若写有该值,owner 状态加载时会直接拒绝。任何环节都不会推断出 `write`,而宿主机无法确定模式的分享仍可读取,但绝不会被驱动。 + +## fork 与其上限 + +fork 创建查看者拥有的新会话,源会话保持不变。Fork 授权检查分享、其 fork 开关、有效期、查看者准入、源身份与请求的边界。`READ_ONLY_FORK_CEILING` 允许读取与检索面——`read`、`read_image`、`glob`、`grep`、`present`、`todo_write`、`skill`、`web_search`、`web_fetch`——,`WRITE_FORK_CEILING` 增加 Host 的有界 `edit` 与 `write`。每个带路径的工具都有受限替代定义:解析它声明的所有路径并按授予根目录检查,再以规范路径委托;`bash`、`pwsh`、`cua`、`plugin_manager`、`schedule_*` 及工具面未列出的名字一律拒绝。 + +每个分叉会话都会记录授权它的分享、Liveshare 服务端断言的可信 viewer,以及它起始的边界,因此 viewer 的列表只显示它自己的分叉会话,被移除的分享会终止它们。只有在分叉会话自身作用域上安装好上限之后它才会被驱动:`ForkRegistry` 会拒绝每个上限未安装的分叉会话的 prompt(`fork-ceiling-uninstalled`),且安装是按分叉会话逐个验证,而非按宿主机验证。若分叉会话作用域拒绝注册受限替换或上限,就拒绝该分叉会话,而不是让它带着部署自身的工具发布。 + +冷态源会话与分叉会话通过 Host 常规 Agent lookup 激活,而非 Remote `session/prepare` 端点。串行 `agent/created` 监听器在排队工作放行前安装作用域执行限制;已经在线的 Agent 仍需检查自身安装。`forkLineage` 是 LiveShare 自有的按查看者 UI 投影,不是会话摘要字段、持久日志事件或权限授予。 + +## 重试、恢复与撤销 + +同一个逻辑 fork 请求可能被发送多次。其身份绑定可信 viewer、分享、源会话与调用方的操作 id;边界是与该身份比较而非折入其中,因此重试会重复它已经产生的分叉会话,同一身份指向另一个边界会冲突而不是悄悄拿到更早的分叉会话,且预留在任何分叉会话存在之前就已持久化。宿主机记录 owner 的 fork 应答返回的分叉会话,重试已记录的操作会重放该应答。owner 的 `SessionForkRequest` 自己生成分叉会话 id,因此在创建分叉会话与记录它之间崩溃的宿主机重试时无法指名该分叉会话,会再创建一个;要关闭这一窗口,需要 Session Controller 的请求接受调用方提供的分叉会话身份。 + +分叉会话工具守卫重新检查分享当前模式、有效期、查看者准入与撤销记录。撤回查看者会取消受影响的分叉会话工作与未决调用。`write` 降级为 `view` 后,写入守卫拒绝后续写入,不授予任何新工具,也不承诺立即取消正在运行的轮次。移除分享撤销已记录分叉会话并停止该会话正在运行的轮次;源执行层在该会话不再有任何分享时释放,并随 Agent 释放。已撤销分叉会话保留日志,源会话所有者可见历史绝不改写。 + +## 约束共享会话 + +每个分享都让它的 viewer 通过正常 prompt 路径驱动*源*会话,任一档位的分享在活跃期间都会安装有界沙箱。`installSourceSessionAccess` 在该会话自身作用域中为部署提供的每个读取与检索面工具注册受限替代,并在任一同会话分享允许写入时再注册有界 `edit` 与 `write` 替代。执行层挂在会话上,但只约束分享查看者发起的轮次:`agent/pre-step` 在 Host 内存中记录一个轮次的已准入输入是否带有 share-viewer principal,守卫与替代定义对该轮次的每次执行读取这一结论。查看者发起的轮次只能运行这些替代定义;所有者自己发起的轮次保留会话原生工具,包括 `bash` 与不受限的文件工具。该结论在轮次准入时确定,不写入会话日志;重启后从下一个准入步骤重新计算。所有者在查看者轮次中追加的输入仍留在该受限轮次内。该会话的所有分享停止后执行层被释放,会话恢复自己的工具;最后一个可写分享降为 `view` 时重装只读层。 + +安装结果报告源会话是否真正受到限制。没有 `read` 工具的部署或拒绝该替代注册的作用域会保留守卫并报告失败;某个工具自己的替代注册失败时,该工具保持拒绝,其余工具面照常运行。读取处理器以 `source-write-unavailable` 拒绝消息:限制未安装时,共享会话中查看者发起的轮次不会用所有者工具驱动。 + +每次操作都会重新读取维持该会话可驱动的分享的 Relay 授权,且读取与其授权的工具调用在同一同步步骤内完成,因此排队超过其准入授权的输入无法凭过期准入行动。执行层按会话安装:同一会话的多个分享共用一个安装;分享收窄或结束时改变该层允许的操作——没有任何活跃分享可写时拒绝有界写入,会话不再有分享时释放执行层。Host 不把正在运行的轮次归属给准入它的查看者,因此结束某个分享的授权会停止该会话正在运行的轮次,而不是只挑出该查看者的工作;内存中的查看者轮次标记只决定该轮次运行哪套工具面。 + +受限文件工具解析其自身参数声明的每个路径,检查获授根目录包含每个目标,再以目标的规范路径委托。无路径的 `glob` 或 `grep` 搜索会话的工作目录,由替代定义补齐并按命名路径一样检查;`present` 调用检查它列出的每个文件;无本地文件副作用的工具原样委托。有界编辑执行同样的包含检查,使用 `fs/edit-intent`,并发布 `editText` 返回的版本;有界写入使用 `fs/write-intent`,并发布 `writeText` 返回的版本。异步意图等待后,Host 解析当前会话沙箱策略并重新检查分享授权。该策略作为 `editText` 或 `writeText` 的第五个参数,位于 `signal` 之后,由沙箱后端据此限制变更。包含关系按路径检查,若本地进程在检查与打开之间把获授范围内的目录换成符号链接,操作可能被改指别处;基于目录句柄锚定操作留待单独变更引入。 + +查看者对源会话的提示词与停止都按标准 Gateway 分派走 `share-viewer` 策略的源会话写入角色:分享档位已确立且其会话受沙箱限制时策略放行,随后由所有者自己的会话控制器停止该会话当前运行的轮次。取消不按查看者比对,因此读者对共享会话的停止也会结束所有者发起的轮次。[Session Controller](../../packages/api/session-controller/README.zh.md) 在 `prompt` 投递前、以及与 `cancel` 同一同步步骤内再次读取 `remoteAccess.allows()`,因此 Gateway 放行调用之后访问权才结束的查看者不会投递或取消任何内容。排队的外部源输入在 `agent/pre-step` 刷新 Liveshare 服务端授权;缺少授权时在模型请求之前拒绝步骤。 + + + +## viewer 状态 + +一次以拒绝结束的 viewer 读取会指明它自己的状态,而不是让所有原因共用一种失败:链接不存在、owner 已停止分享、分享已过期、owner 的机器没有路由、登录已过期、读者尚未申请、申请仍在等待、申请被 owner 拒绝、owner 从未收到通知,每种都有各自的 HTTP 状态与错误码;页面联系不上 Liveshare 服务端时由页面自己陈述。`@deepseek-ai/dsh-experimental-liveshare-protocol` 中的 `VIEWER_STATE_ANSWERS` 就是这份词汇表,每种状态对应的答案由 [共享协议](../../packages/experimental/liveshare-protocol/README.zh.md) 维护。没有任何一种答案会扩大 viewer 能读到的内容:owner 离线、分享已停止、申请被拒,三者都是拒绝。 + +viewer 页面渲染所用的工作区流与会话列表经同一策略过滤:只要工作区还持有共享会话或该 viewer 自己创建的分支就会下发,其成员与手工顺序只保留这些。失去它们的工作区要到该工作区或工作区顺序下次变化时才离开工作区流,届时流替换读者的基线;仅访问变化不会发送帧。因此读者看到的是围绕它被授予的那一个会话的 owner 工作区名称与分组,而不是 owner 其余的工作区。 + +查看流在轮询、替换与取消时均绑定打开它的主体。会话通知、控制基线与工作区成员只暴露源会话及该主体的分叉会话:Gateway 只向读者转发策略在这些会话上允许的 `api-session/*` 通知,Session 与 Workspace 控制器也经同一策略过滤其列表和流;交互审批与提问保留在 Host。被拒绝的读取不创建上游订阅。过期与撤权取消受影响的未决调用和流;120 秒未轮询的流被回收。Worker IPC 响应与取消必须匹配请求 token、分享 ID、Liveshare 服务端 ID 及原始 socket 或 worker generation;迟到响应不能结算替换请求。 + +Liveshare 服务端可选的 `share-access-state` 能力在连接和已提交变更时发布完整、带版本的授权快照,包括其他 Liveshare 服务端实例提交的变更。鉴权 `GET /__liveshare/requests` 返回同一组字段。Host 在连接丢失时清空缓存授权,在同一 generation 内拒绝过期修订,并对异步刷新检查 generation。缺少完整授权时拒绝准入;这不构成立即的分布式停止语义保证。 + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.liveshareShares` — `LiveshareShares` + +Owner-only Remote surface over the share-management operations. + +The Gateway decides each call through remote access, so a `share-viewer` Peer is refused before a method runs; the owner Peer reaches the one implementation that owns each share-management side effect. `create` takes its Session as the `sessionId` of its single request object, so the Session resource kind's default resolver names that Session for the call without a reader declared here. + +```ts cordis-catalog +/** + * Publish one Session and store the share. + * @param request - Session, title, access, lifetime, and both switches the owner chose. + * @returns The created share's link and stored values. + */ +@Remote('create') create(request: CreateShareRequest): Promise + +/** + * Refresh titles and Liveshare server authority, then list this host's shares. + * @returns Every share this host publishes. + */ +@Remote('list') list(): Promise + +/** + * Stop one share locally, revoke its forks, and delete its row on the Liveshare server when reachable. + * @param request - Share to stop. + * @returns Whether the row on the Liveshare server still needs a retry. + */ +@Remote('stop') stop(request: StopShareRequest): Promise + +/** + * Change one share's tier, fork switch, or both. + * @param request - Share and the switches to change. + * @returns The permissions the share now carries. + */ +@Remote('updatePermissions') updatePermissions(request: UpdateSharePermissionsRequest): Promise + +/** + * Replace one share's exposed plugin endpoints. + * @param request - Share and the selection to store. + * @returns The capability selection the share now carries. + */ +@Remote('setCapabilities') setCapabilities(request: SetShareCapabilitiesRequest): Promise + +/** + * Read the endpoints plugins currently offer. + * @returns The live offer list. + */ +@Remote('capabilities') capabilities(): ShareCapabilityOffers + +/** + * Read the Liveshare server address the owner saved. + * @returns The current Liveshare server state, with both address fields absent while none is saved. + */ +@Remote('relay') relay(): OwnerRelayState + +/** + * Choose the Liveshare server address later shares publish to. + * @param request - Normalized address or null to clear the saved one. + * @returns The Liveshare server state after the change. + */ +@Remote('setRelay') setRelay(request: SetRelayRequest): Promise + +/** + * Report this host's owner identity for one Liveshare server address. + * @param request - Address to report, or the effective one when absent. + * @returns The owner identity status, without the credential. + */ +@Remote('identity') identity(request: OwnerIdentityRequest): Promise + +/** + * Read the Liveshare server's durable audit trail for one share. + * @param request - Share whose audit is read. + * @returns The audit events, or a structured failure when the Liveshare server cannot answer. + */ +@Remote('audit') audit(request: ShareAuditRequest): Promise + +/** + * Check that the effective Liveshare server answers at all. + * @returns The anonymous connectivity result. + */ +@Remote('testRelay') testRelay(): Promise + +/** + * Register one throwaway share to check whether this host may publish. + * @param request - Session the throwaway registration names. + * @returns The Liveshare server's decision about this host's credential. + */ +@Remote('testPublish') testPublish(request: TestPublishRequest): Promise +``` + +Source: [`packages/experimental/liveshare/src/host/share-remote.ts`](../../packages/experimental/liveshare/src/host/share-remote.ts) + diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 368a82dc27..918f4882bf 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.i18n.yaml @@ -36,8 +36,8 @@ en: 58dec2259b0b1c6f zh: fc99fe965acaabf3 /llm-streaming/the-model-request: - en: 2b0831aaae8e1cdf - zh: f669d93d1b9d6025 + en: 834d3d8d6b5871ab + zh: 393889ec6075f622 /llm-streaming/the-model-request/the-request-envelope-llmcallconfig-and-the-logged-header: en: b0eb4620c6953646 zh: fabf51c7245a4855 diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index 23b7c6c2bd..cf8d4c8310 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -525,6 +525,26 @@ interface LlmNativeAuth { } ``` +A catalog entry may carry a provider-authored `badge` that selectors show beside the model name. It is display data: `resolveModelInfo` omits it, and no model request carries it. + +```ts type-equiv +/** + * Emphasis of a model badge: `neutral` states a plain fact, `info` highlights + * the model, `success` marks a benefit, and `warning` asks for attention. + */ +type LlmModelBadgeTone = 'neutral' | 'info' | 'success' | 'warning' +``` + +```ts type-equiv +/** Short provider-authored label a selector shows beside one advertised model name. */ +interface LlmModelBadge { + /** One literal, or translations keyed by lowercase language id with a required English entry. */ + text: LocalizedText + /** Emphasis of the label. */ + tone: LlmModelBadgeTone +} +``` + ```ts type-equiv /** One adapter-discovered model; catalog membership is advisory, not request validation. */ interface LlmModelInfo { @@ -538,6 +558,11 @@ interface LlmModelInfo { description?: string /** Accepted request modalities; absent means unknown, while an explicit omission is negative capability. */ inputModalities?: readonly ModelModality[] + /** + * Selector label advertised by catalog discovery; exact-route resolution + * omits it, and no model request or Session event carries it. + */ + badge?: LlmModelBadge } ``` @@ -617,8 +642,21 @@ type RequestMessage = Message | RequestUserInput ``` ```ts type-equiv -/** A single model request, fully assembled. */ -interface GenerateOptions { +/** Auxiliary request purposes declared by their producer packages. */ +interface LlmRequestPurposeMap {} +``` + +```ts type-equiv +/** Declared auxiliary request purpose; ordinary conversation requests leave it absent. */ +type LlmRequestPurpose = Extract +``` + +```ts type-equiv +/** + * A single model request, fully assembled. + * @typeParam Purpose - declared purpose keys for callers; forwarding adapters may accept `string`. + */ +interface GenerateOptions { /** Registered provider route selecting the adapter instance. */ provider: string model: string @@ -659,7 +697,7 @@ interface GenerateOptions { * map the purpose to model-hidden transport metadata. Ordinary conversation * requests leave it unset. */ - purpose?: 'compaction' | 'session-title' + purpose?: Purpose } ``` @@ -1170,6 +1208,23 @@ The provider topology changed: an adapter registered or unregistered routes, or Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + + +#### `llm/models-updated` — emit + +A provider's advertised models or display metadata changed; consumers reload `listModels()`. + +```ts cordis-catalog +/** + * A provider's advertised models or display metadata changed; consumers reload `listModels()`. + * @mode emit + * @param provider - route whose model catalog changed. + */ +'llm/models-updated'(provider: string): void +``` + +Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + #### `llm/stream` — waterfall diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index 98783a10e1..0feb80a1a1 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -531,6 +531,26 @@ interface LlmNativeAuth { } ``` +目录条目可以携带提供方编写的 `badge`,供选择器显示在模型名称旁。它只是展示数据:`resolveModelInfo` 不返回它,模型请求也不携带它。 + +```ts type-equiv +/** + * Emphasis of a model badge: `neutral` states a plain fact, `info` highlights + * the model, `success` marks a benefit, and `warning` asks for attention. + */ +type LlmModelBadgeTone = 'neutral' | 'info' | 'success' | 'warning' +``` + +```ts type-equiv +/** Short provider-authored label a selector shows beside one advertised model name. */ +interface LlmModelBadge { + /** One literal, or translations keyed by lowercase language id with a required English entry. */ + text: LocalizedText + /** Emphasis of the label. */ + tone: LlmModelBadgeTone +} +``` + ```ts type-equiv /** One adapter-discovered model; catalog membership is advisory, not request validation. */ interface LlmModelInfo { @@ -544,6 +564,11 @@ interface LlmModelInfo { description?: string /** Accepted request modalities; absent means unknown, while an explicit omission is negative capability. */ inputModalities?: readonly ModelModality[] + /** + * Selector label advertised by catalog discovery; exact-route resolution + * omits it, and no model request or Session event carries it. + */ + badge?: LlmModelBadge } ``` @@ -623,8 +648,21 @@ type RequestMessage = Message | RequestUserInput ``` ```ts type-equiv -/** A single model request, fully assembled. */ -interface GenerateOptions { +/** Auxiliary request purposes declared by their producer packages. */ +interface LlmRequestPurposeMap {} +``` + +```ts type-equiv +/** Declared auxiliary request purpose; ordinary conversation requests leave it absent. */ +type LlmRequestPurpose = Extract +``` + +```ts type-equiv +/** + * A single model request, fully assembled. + * @typeParam Purpose - declared purpose keys for callers; forwarding adapters may accept `string`. + */ +interface GenerateOptions { /** Registered provider route selecting the adapter instance. */ provider: string model: string @@ -665,7 +703,7 @@ interface GenerateOptions { * map the purpose to model-hidden transport metadata. Ordinary conversation * requests leave it unset. */ - purpose?: 'compaction' | 'session-title' + purpose?: Purpose } ``` @@ -1176,6 +1214,23 @@ The provider topology changed: an adapter registered or unregistered routes, or Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + + +#### `llm/models-updated` — emit + +A provider's advertised models or display metadata changed; consumers reload `listModels()`. + +```ts cordis-catalog +/** + * A provider's advertised models or display metadata changed; consumers reload `listModels()`. + * @mode emit + * @param provider - route whose model catalog changed. + */ +'llm/models-updated'(provider: string): void +``` + +Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts) + #### `llm/stream` — waterfall diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 29991cd13f..b38666ddb4 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.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/session-projection.md /session-projections: - en: 45e5f8cb8ca18d6b - zh: 4ecfad40ec9e221c + en: b2789f5257a935c5 + zh: 12d430a9cd454592 /session-projections/the-unit: en: d48814d3ab0e997f zh: d0337d8eba097a63 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index e56896f455..cee8103ef0 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -18,6 +18,13 @@ interface ProjectionHandle { * @returns the projection state at the Session's current cursor. */ stateOf(session: Session): S + /** + * Create a reader for one exact Session without reading state or preparing it. + * The reader retains this unit after unregistration and owns no disposal. + * @param session - Session whose committed events the bound reader folds. + * @returns a synchronous callback with the same state and failures as `stateOf(session)`. + */ + readerFor(session: Session): () => S /** * Unregister from global drives and snapshots without invalidating this reader. * @returns a promise that resolves when this registration is released. @@ -28,6 +35,8 @@ interface ProjectionHandle { The [compatibility rules](../../packages/session/session-projection/README.md#deprecated-plugin-compatibility) define legacy disposal, keyed reads, and their once-per-key deprecation warnings. +`ProjectionHandle.readerFor(session)` returns a `() => S` callback for one exact Session. Calling it reads the original unit and preserves borrowed state, readiness requirements and synchronous failures. Owners can compose several readers in a typed object; domain services keep them private. See [binding several states](../../packages/session/session-projection/README.md#bind-several-states-to-one-session). + ## The unit `ProjectionDefinition` carries each unit's host state type locally; registration infers `S` from `stateSchema` and returns `ProjectionHandle`. Only client-visible keys merge into `SessionProjectionMap`, which types their whole values. A `wire` block makes a unit client-visible, and rendering belongs to the slot system, never this layer: diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index da53b76a7e..6e076a18a3 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -18,6 +18,13 @@ interface ProjectionHandle { * @returns the projection state at the Session's current cursor. */ stateOf(session: Session): S + /** + * Create a reader for one exact Session without reading state or preparing it. + * The reader retains this unit after unregistration and owns no disposal. + * @param session - Session whose committed events the bound reader folds. + * @returns a synchronous callback with the same state and failures as `stateOf(session)`. + */ + readerFor(session: Session): () => S /** * Unregister from global drives and snapshots without invalidating this reader. * @returns a promise that resolves when this registration is released. @@ -28,6 +35,8 @@ interface ProjectionHandle { [兼容规则](../../packages/session/session-projection/README.zh.md#deprecated-plugin-compatibility)规定旧释放操作、按键读取及其每个键一次的弃用警告。 +`ProjectionHandle.readerFor(session)` 为一个确切的 Session 返回 `() => S` 回调。调用它会读取原有单元,保留借用状态、就绪要求和同步失败。所有者可将多个读取函数组合成一个类型化对象;领域服务将它们保留在内部。参见[绑定多个状态](../../packages/session/session-projection/README.zh.md#bind-several-states-to-one-session)。 + ## 投影单元 `ProjectionDefinition` 在各单元内部携带宿主状态类型;注册从 `stateSchema` 推导 `S`,并返回 `ProjectionHandle`。只有客户端可见的 key 合并到 `SessionProjectionMap`,由它约束对应的全量值。`wire` 块使单元对客户端可见,渲染归 slot 体系管,永远不归本层: diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 53d89173b4..b4cd4bba34 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.i18n.yaml @@ -68,3 +68,6 @@ /sessions/remote-catalog-and-workspace-opening: en: dd27307e0d7c893c zh: b77d2a749b7b641c +/sessions/session-log-upload-exclusions: + en: cfe3323aade972be + zh: 8423b490c47c5013 diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 7cbc91298d..fb3e56ee87 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -817,6 +817,29 @@ The backends that consume this contract are on [persistence.md](persistence.md). The optional `application` selects a registered file handler without changing the system default; `workspacePathApplications({ path })` returns current handlers, names, icons, and default selection after Host path verification. +## Session-log upload exclusions + +`ctx.sessionLogDeepSeek.registerExclusion()` registers a caller-owned predicate over `SessionLogUploadRequest`. Any match omits the log contribution and its acceptance callback before reading Session events. No match preserves normal upload eligibility. The [package README](../../packages/session/session-log-deepseek/README.md#upload-exclusions) defines failure and disposal behavior. + +```ts type-equiv +/** Request identity inspected before preparing a Session-log contribution. */ +interface SessionLogUploadRequest { + /** Live Session whose canonical log would be contributed. */ + readonly sessionId: SessionId + /** Auxiliary request classification; absent for ordinary conversation requests. */ + readonly purpose?: string +} +``` + +```ts type-equiv +/** + * Synchronous, side-effect-free upload exclusion owned by its registering plugin. + * @param request - request identity; the predicate must not mutate it. + * @returns `true` to omit the log field and acceptance callback, or `false` to keep checking eligibility. + */ +type SessionLogUploadExclusion = (request: SessionLogUploadRequest) => boolean +``` + @@ -1035,6 +1058,26 @@ Types: [SessionId](core.md) · [SessionSearchRequest](session-query.md) Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts) + + +### `ctx.sessionLogDeepSeek` — `SessionLogDeepSeekService` + +Incremental Session-log contributor with caller-owned upload exclusions. + +```ts cordis-catalog +/** + * Exclude matching requests before reading or preparing their log contribution. + * Any matching rule suppresses both the field and its acceptance callback. With + * no matching rules, normal upload eligibility applies. A throwing rule fails + * this contribution; the request-extension registry omits it and reports the error. + * @param exclude - synchronous, side-effect-free predicate; `true` skips this upload. + * @returns effect disposer that removes this registration when called or when the caller unloads. + */ +registerExclusion(exclude: SessionLogUploadExclusion): () => Promise +``` + +Source: [`packages/session/session-log-deepseek/src/index.ts`](../../packages/session/session-log-deepseek/src/index.ts) + ### `ctx.sessions` — `SessionStore` diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 1f40042967..fb26a5105c 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -821,6 +821,29 @@ interface PluginRecord { 可选的 `application` 指定文件关联应用,不修改系统默认值;`workspacePathApplications({ path })` 在校验 Host 路径后返回当前关联应用、名称、图标和默认项。 +## Session 日志上传排除条件 + +`ctx.sessionLogDeepSeek.registerExclusion()` 注册归调用方所有的判断函数,输入为 `SessionLogUploadRequest`。任意条件匹配时,在读取 Session 事件前省略日志贡献与接受回调。没有条件匹配时保留普通上传资格判断。[包 README](../../packages/session/session-log-deepseek/README.zh.md#upload-exclusions) 定义失败与释放行为。 + +```ts type-equiv +/** Request identity inspected before preparing a Session-log contribution. */ +interface SessionLogUploadRequest { + /** Live Session whose canonical log would be contributed. */ + readonly sessionId: SessionId + /** Auxiliary request classification; absent for ordinary conversation requests. */ + readonly purpose?: string +} +``` + +```ts type-equiv +/** + * Synchronous, side-effect-free upload exclusion owned by its registering plugin. + * @param request - request identity; the predicate must not mutate it. + * @returns `true` to omit the log field and acceptance callback, or `false` to keep checking eligibility. + */ +type SessionLogUploadExclusion = (request: SessionLogUploadRequest) => boolean +``` + @@ -1039,6 +1062,26 @@ Types: [SessionId](core.zh.md) · [SessionSearchRequest](session-query.zh.md) Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts) + + +### `ctx.sessionLogDeepSeek` — `SessionLogDeepSeekService` + +Incremental Session-log contributor with caller-owned upload exclusions. + +```ts cordis-catalog +/** + * Exclude matching requests before reading or preparing their log contribution. + * Any matching rule suppresses both the field and its acceptance callback. With + * no matching rules, normal upload eligibility applies. A throwing rule fails + * this contribution; the request-extension registry omits it and reports the error. + * @param exclude - synchronous, side-effect-free predicate; `true` skips this upload. + * @returns effect disposer that removes this registration when called or when the caller unloads. + */ +registerExclusion(exclude: SessionLogUploadExclusion): () => Promise +``` + +Source: [`packages/session/session-log-deepseek/src/index.ts`](../../packages/session/session-log-deepseek/src/index.ts) + ### `ctx.sessions` — `SessionStore` diff --git a/docs/subsystems/slots.md b/docs/subsystems/slots.md index ee31122d13..d64184dbc0 100644 --- a/docs/subsystems/slots.md +++ b/docs/subsystems/slots.md @@ -140,7 +140,10 @@ Factory: conversation.content [ConversationContent] │ │ ├─ conversation.chat.text-interaction [list] │ │ └─ conversation.chat.flow [ChatFlow] │ │ └─ conversation.chat.node [ChatNodeSeat dispatch] -│ │ ├─ key=user,steering [UserMessageNodeView] +│ │ ├─ key=user [UserMessageNodeView] +│ │ │ ├─ conversation.chat.user.before [chain] +│ │ │ └─ calls Factory: conversation.chat.user.content +│ │ ├─ key=steering [UserMessageNodeView] │ │ │ └─ calls Factory: conversation.chat.user.content │ │ ├─ key=assistant-step [AssistantNodeView / AssistantMarkdown] │ │ │ ├─ conversation.chat.message.images [MessageImages] diff --git a/docs/subsystems/slots.zh.md b/docs/subsystems/slots.zh.md index ca13d60008..db7509aefe 100644 --- a/docs/subsystems/slots.zh.md +++ b/docs/subsystems/slots.zh.md @@ -142,7 +142,10 @@ Factory: conversation.content [ConversationContent] │ │ ├─ conversation.chat.text-interaction [list] │ │ └─ conversation.chat.flow [ChatFlow] │ │ └─ conversation.chat.node [ChatNodeSeat dispatch] -│ │ ├─ key=user,steering [UserMessageNodeView] +│ │ ├─ key=user [UserMessageNodeView] +│ │ │ ├─ conversation.chat.user.before [chain] +│ │ │ └─ calls Factory: conversation.chat.user.content +│ │ ├─ key=steering [UserMessageNodeView] │ │ │ └─ calls Factory: conversation.chat.user.content │ │ ├─ key=assistant-step [AssistantNodeView / AssistantMarkdown] │ │ │ ├─ conversation.chat.message.images [MessageImages] diff --git a/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.i18n.yaml new file mode 100644 index 0000000000..fdea1cb921 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.md +/: + en: e3db78299cefc568 + zh: 0c1892abef438638 +/the-ui-deepseek-model-label-1017-package-is-removed: + en: 772b7e3479b2e52b + zh: 9d7a225cf77c2664 +/the-ui-deepseek-model-label-1017-package-is-removed/change: + en: 48543e623bac7b47 + zh: a22640ced4b587fa +/the-ui-deepseek-model-label-1017-package-is-removed/migration: + en: 3e366a0f2daf220c + zh: 859d6bc103213809 diff --git a/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.md new file mode 100644 index 0000000000..9511ee9834 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "The fixed 1017 model badge package is removed; model badges come from the DeepSeek model listing." +--- +# The `ui-deepseek-model-label-1017` package is removed + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` and its `ui-deepseek-model-label-1017` row in the Web bundle are removed, and the package receives no further releases. The composer model selector shows the badge that the DeepSeek `GET /models` listing declares for a model; without a declared badge, `DeepSeek-V4.1-Flash` shows no `1017`. A profile patch that disables or edits the removed row matches no entry, so boot warns that the entry is not found and skips the patch. + +## Migration + +1. Delete the patch whose `id` is `ui-deepseek-model-label-1017`, with its `disabled` or other fields, from `$DSH_HOME/profiles//cordis.patch.yml` and from any overlay passed with `--patch`. +2. Remove `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` from the dependencies of any package that lists it. +3. To change how model names or badges render, contribute to the `conversation.input.model.label` slot; its owner props carry the catalog `model.badge` ([label extensions](../../../../packages/client/ui-model-selection/README.md#use-this-package)). +4. Restart DSH and confirm that boot reports no missing `ui-deepseek-model-label-1017` entry. diff --git a/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.zh.md new file mode 100644 index 0000000000..6106f94486 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.2/model-badge-package-removal/guide.zh.md @@ -0,0 +1,18 @@ +--- +kind: upgrade-guide +description: "固定的 1017 模型角标包已移除,模型角标改由 DeepSeek 模型列表下发。" +--- +# `ui-deepseek-model-label-1017` 包已移除 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` 及其在 Web bundle 中的 `ui-deepseek-model-label-1017` 条目已移除,该包不再发布新版本。composer 模型选择器显示 DeepSeek `GET /models` 列表为模型声明的角标;列表没有声明角标时,`DeepSeek-V4.1-Flash` 不再显示 `1017`。停用或修改该条目的 profile patch 不再匹配任何条目,启动时会警告找不到该条目并跳过这条 patch。 + +## 迁移 + +1. 从 `$DSH_HOME/profiles//cordis.patch.yml` 以及通过 `--patch` 传入的 overlay 中,删除 `id` 为 `ui-deepseek-model-label-1017` 的 patch,连同其中的 `disabled` 或其他字段。 +2. 若有包在依赖中列出 `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017`,将其删除。 +3. 如需改变模型名称或角标的显示方式,向 `conversation.input.model.label` slot 注册组件;其 owner props 携带目录中的 `model.badge`([名称扩展](../../../../packages/client/ui-model-selection/README.zh.md#use-this-package))。 +4. 重启 DSH,确认启动输出中没有找不到 `ui-deepseek-model-label-1017` 条目的警告。 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.i18n.yaml new file mode 100644 index 0000000000..f8584a6842 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.md +/: + en: 23336f139ca68e47 + zh: 20ef9419a53642d5 +/authorize-the-computer-use-helper: + en: f8335f2dca7a15cf + zh: 469a0a753fea2a93 +/authorize-the-computer-use-helper/change: + en: 1552866718133b10 + zh: 1c760c5c2d5cfc0b +/authorize-the-computer-use-helper/migration: + en: a79e5995415ff456 + zh: 6f818a092e53af2c diff --git a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.md new file mode 100644 index 0000000000..c913488785 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Computer Use needs permission for its own helper application instead of the terminal that launches DSH." +--- + +# Authorize the Computer Use helper + +English | [中文](guide.zh.md) + +## Change + +macOS Computer Use runs as `DSH Computer Use.app`, launched through LaunchServices. Accessibility and Screen Recording grants for a terminal or Desktop launcher do not grant access to this helper. Source preparation builds and signs the application; installed packages require the prepared application and its `current.json` receipt. A custom `helperPath` pointing at a bare native binary is no longer a supported native launch path. + +## Migration + +1. Computer Use now installs on demand. In **Plugins → Add plugin**, install `@deepseek-ai/dsh-cua-bundle` and enable it; profiles that previously selected the shipped copy need this installation. Restart DSH if requested. In the Computer Use plugin page, grant Accessibility and Screen Recording access to `DSH Computer Use.app` when macOS requests it. +2. Remove an obsolete `helperPath` override or `DSH_CUA_HELPER` value to use automatic preparation. For a custom build, point it to the executable at `DSH Computer Use.app/Contents/MacOS/dsh-cua-native` inside the complete application bundle. +3. Confirm that the permission page identifies `DSH Computer Use.app` and both permissions are allowed before resuming native application tasks. Target-application consent remains required. +4. If a terminal or Desktop launcher was granted these permissions only for Computer Use, revoke those obsolete grants in System Settings. Authorizing the helper does not revoke other applications’ existing permissions; programs the model runs still follow their launcher’s system permissions and the DSH sandbox policy. + +Source builds use ad-hoc signing and may need authorization again after code changes. Release packagers must preserve a stable Developer ID signing identity and notarize their distribution. See the [native build instructions](../../../../packages/experimental/cua-macos/native/README.md#build-and-service). diff --git a/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.zh.md new file mode 100644 index 0000000000..f25a4b805d --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/computer-use-helper-permissions/guide.zh.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Computer Use 需要给自己的 helper 应用授权,启动 DSH 的终端授权不再适用。" +--- + +# 给 Computer Use helper 授权 + +[English](guide.md) | 中文 + +## 变更 + +macOS Computer Use 通过 LaunchServices 以 `DSH Computer Use.app` 运行。终端或 Desktop 启动器的辅助功能和屏幕录制授权不赋予 helper 权限。源码准备会构建并签名应用;安装版需要预构建的应用及 `current.json` 记录。自定义 `helperPath` 指向裸原生二进制的启动方式不再受支持。 + +## 迁移 + +1. Computer Use 改为按需安装。在“插件 → 添加插件”中安装 `@deepseek-ai/dsh-cua-bundle` 并启用;此前选中过内置副本的 profile 也需完成这次安装。按提示重启 DSH。在 Computer Use 插件页按 macOS 提示,给 `DSH Computer Use.app` 授予辅助功能和屏幕录制权限。 +2. 删除过时的 `helperPath` 覆盖配置或 `DSH_CUA_HELPER` 值,使用自动准备。使用自定义构建时,将其指向完整应用包内的 `DSH Computer Use.app/Contents/MacOS/dsh-cua-native` 可执行文件。 +3. 恢复原生应用任务前,确认权限页显示 `DSH Computer Use.app`,且两项权限均已允许。具体目标应用仍需另行许可。 +4. 若终端或 Desktop 启动器的这两项权限只用于 Computer Use,可在系统设置中撤销这些旧授权。给 helper 授权不会撤销其他应用已有的权限;模型运行的程序仍遵循启动器的系统权限和 DSH 沙箱策略。 + +源码构建使用临时签名,代码变化后可能需要重新授权。正式发布需要保持稳定的 Developer ID 签名身份,并完成分发公证。参见[原生构建说明](../../../../packages/experimental/cua-macos/native/README.zh.md#build-and-service)。 diff --git a/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.i18n.yaml new file mode 100644 index 0000000000..5dea1bc8a2 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.md +/: + en: 87b8371f353630ce + zh: 545cd66ec2055405 +/model-listings-refresh-automatically: + en: f4613eb3beef2045 + zh: 7c1301d90456a6c2 +/model-listings-refresh-automatically/change: + en: 6ed436b9f0a93aef + zh: 16a09e92ba786d89 +/model-listings-refresh-automatically/migration: + en: e479027c4c4811df + zh: 7cc7cde8aa4e0135 diff --git a/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.md b/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.md new file mode 100644 index 0000000000..3455c2bbfe --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.md @@ -0,0 +1,22 @@ +--- +kind: upgrade-guide +description: "DeepSeek model listings refresh automatically after first use, and custom listing-cache owners must supply live settings and change notifications." +--- +# Model listings refresh automatically + +English | [中文](guide.zh.md) + +## Change + +DeepSeek account and API-key routes refresh their model listing automatically after first use, including while idle. `modelsMaxAgeMs` keeps its ten-minute default and now schedules background requests instead of waiting for another access. Failed refreshes retain the last successful listing, including badges. Successful refreshes replace badge metadata, including omission or `null`, and notify open Web selectors. + +`ModelListingCache` consumers must supply `settings` and `changed` callbacks. Its `dispose()` now returns a promise that settles after active loads and automatic credential lookups finish. `ListedCatalog` also requires a `badges` map, and `ModelSelectInjected` requires a `resolveText` callback. + +## Migration + +1. Keep existing `modelsMaxAgeMs` values to use the same interval for automatic refresh. Use `0` only when each access should refresh and no automatic timer should run. +2. Custom `ModelListingCache` owners must provide `settings: () => ListingSettings | undefined` for the latest route configuration and `changed: () => void` to notify catalog consumers. Return `undefined` from `settings` when the route stops following a listing. +3. Return or await `cache.dispose()` from the owning plugin's cleanup. +4. Code that constructs `ListedCatalog` must include `badges: new Map()` when it has no badges, or a map from model id to `LlmModelBadge`. Prefer `resolveListedCatalog` to construct this result from decoded listing entries. +5. Code that constructs `ModelSelectInjected` must provide `resolveText: text => ctx.locale.resolveText(text)` from its owning locale service. Test doubles can use `text => typeof text === 'string' ? text : text.en` when they intentionally render English. +6. After the next refresh, confirm that an already-open model selector displays the current badge without reloading the page. Remove the badge field or set it to `null` to clear it. diff --git a/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.zh.md new file mode 100644 index 0000000000..4999430d2f --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.4/model-listing-refresh/guide.zh.md @@ -0,0 +1,22 @@ +--- +kind: upgrade-guide +description: "DeepSeek 模型列表首次使用后自动刷新,自定义列表缓存的持有者必须提供实时配置与变化通知。" +--- +# 模型列表自动刷新 + +[English](guide.md) | 中文 + +## 变更 + +DeepSeek 账号与 API key 路由首次使用模型列表后会自动刷新,空闲时也会刷新。`modelsMaxAgeMs` 保留十分钟默认值,现在会安排后台请求,无需等待下一次访问。刷新失败时保留上次成功加载的列表及其角标。刷新成功时替换角标元数据,省略或 `null` 也会生效,并通知已打开的 Web 选择器。 + +`ModelListingCache` 的消费方必须提供 `settings` 与 `changed` 回调。其 `dispose()` 现在返回一个 promise,等待进行中的加载与自动凭据查询结束后才完成。`ListedCatalog` 还要求提供 `badges` 映射,`ModelSelectInjected` 要求提供 `resolveText` 回调。 + +## 迁移 + +1. 保留现有 `modelsMaxAgeMs` 值即可按相同间隔自动刷新。只有需要每次访问都刷新、且不运行自动定时器时,才使用 `0`。 +2. 自定义 `ModelListingCache` 持有者必须提供 `settings: () => ListingSettings | undefined` 以读取最新路由配置,并通过 `changed: () => void` 通知目录消费方。路由停止跟随列表时,`settings` 返回 `undefined`。 +3. 在持有该缓存的插件清理函数中返回或等待 `cache.dispose()`。 +4. 构造 `ListedCatalog` 的代码在没有角标时必须提供 `badges: new Map()`,否则提供模型 id 到 `LlmModelBadge` 的映射。优先使用 `resolveListedCatalog` 从已解码的列表条目构造该结果。 +5. 构造 `ModelSelectInjected` 的代码必须通过所属的 locale 服务提供 `resolveText: text => ctx.locale.resolveText(text)`。明确使用英语渲染的测试替身可以使用 `text => typeof text === 'string' ? text : text.en`。 +6. 下一次刷新后,确认已打开的模型选择器无需重新加载页面就显示当前角标。移除角标字段或将其设为 `null` 即可清除角标。 diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.i18n.yaml new file mode 100644 index 0000000000..7bae7f2845 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.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 docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.md +/: + en: 0a5cfa851101f3e9 + zh: 1c8e0e437718d4e2 +/declare-auxiliary-llm-request-purposes: + en: 5d5e1b6cb5b9c95e + zh: 11cf31babb07da28 +/declare-auxiliary-llm-request-purposes/change: + en: 0682a43eb52792fd + zh: 954b186cc66fdb08 +/declare-auxiliary-llm-request-purposes/migration: + en: eae671d1e185ecbf + zh: c042a276a1a7ad17 diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.md b/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.md new file mode 100644 index 0000000000..d71d8f6bef --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.md @@ -0,0 +1,29 @@ +--- +kind: upgrade-guide +description: "Auxiliary LLM request purposes require producer-owned type declarations instead of a shared fixed union." +--- + +# Declare auxiliary LLM request purposes + +English | [中文](guide.zh.md) + +## Change + +`GenerateOptions.purpose` derives from the declaration-mergeable `LlmRequestPurposeMap` exported by `@deepseek-ai/dsh-llm/types`. The shared package no longer declares `compaction` or `session-title`. Plugins that construct auxiliary requests must load the producer's declarations or declare their own purpose. Existing runtime purpose strings keep their meaning. + +## Migration + +1. Import `@deepseek-ai/dsh-compaction-basic` for direct compaction requests, or `@deepseek-ai/dsh-session-title-llm` for title requests, using a type-only import when only the declaration is needed. +2. For a plugin-owned purpose, augment `LlmRequestPurposeMap` from the plugin's public type entry. Map the purpose key to `true`: + + ```ts + import type {} from '@deepseek-ai/dsh-llm/types' + + declare module '@deepseek-ai/dsh-llm/types' { + interface LlmRequestPurposeMap { + 'my-plugin-purpose': true + } + } + ``` + +3. Compile the plugin against the new package declarations. Confirm that its request accepts the declared purpose and rejects an undeclared key. diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.zh.md b/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.zh.md new file mode 100644 index 0000000000..d1fd550142 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/llm-request-purpose-declarations/guide.zh.md @@ -0,0 +1,29 @@ +--- +kind: upgrade-guide +description: "辅助 LLM 请求用途改为生产方拥有的类型声明,不再使用共享固定联合。" +--- + +# 声明辅助 LLM 请求用途 + +[English](guide.md) | 中文 + +## 变更 + +`GenerateOptions.purpose` 由 `@deepseek-ai/dsh-llm/types` 导出的可声明合并的 `LlmRequestPurposeMap` 派生。共享包不再声明 `compaction` 或 `session-title`。构造辅助请求的插件必须加载生产方的声明,或声明自己的用途。现有运行时用途字符串保持原有含义。 + +## 迁移 + +1. 直接构造压缩请求时导入 `@deepseek-ai/dsh-compaction-basic`,构造标题请求时导入 `@deepseek-ai/dsh-session-title-llm`;只需要声明时使用仅类型导入。 +2. 对插件拥有的用途,在插件公开类型入口中扩展 `LlmRequestPurposeMap`,将用途键映射到 `true`: + + ```ts + import type {} from '@deepseek-ai/dsh-llm/types' + + declare module '@deepseek-ai/dsh-llm/types' { + interface LlmRequestPurposeMap { + 'my-plugin-purpose': true + } + } + ``` + +3. 使用新包的声明编译插件,确认请求接受已声明用途,并拒绝未声明的键。 diff --git a/package.json b/package.json index be86fa996d..9dccd6008b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-root", - "version": "0.2.1-rc.3", + "version": "0.2.2-alpha.2", "license": "MIT", "private": true, "type": "module", diff --git a/packages/acp/acp/package.json b/packages/acp/acp/package.json index 41962dd596..877006761a 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/account-controller/package.json b/packages/api/account-controller/package.json index 30aa41c6da..1bd865aeeb 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index 2b4183a776..f67e41c300 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/job-controller/package.json b/packages/api/job-controller/package.json index 40649ebff3..c550f1f912 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/remote-access/package.json b/packages/api/remote-access/package.json index f4e3e13975..5e81e757c2 100644 --- a/packages/api/remote-access/package.json +++ b/packages/api/remote-access/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-api-remote-access", "description": "Method-level Remote access control for non-owner Peers and the per-Peer access snapshot", - "version": "0.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 288aabea9b..8735bc4339 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, @@ -98,6 +98,7 @@ "@deepseek-ai/dsh-office-to-pdf": "workspace:*", "@deepseek-ai/dsh-api-account-controller": "workspace:*", "@deepseek-ai/dsh-deepseek-account": "workspace:*", - "@deepseek-ai/dsh-client-product-analytics": "workspace:*" + "@deepseek-ai/dsh-client-product-analytics": "workspace:*", + "@deepseek-ai/dsh-client-ui-trajectory": "workspace:*" } } diff --git a/packages/api/remotes/src/client/index.ts b/packages/api/remotes/src/client/index.ts index da6f814458..8a4b7dea8a 100644 --- a/packages/api/remotes/src/client/index.ts +++ b/packages/api/remotes/src/client/index.ts @@ -14,6 +14,7 @@ import scheduleRemote from '@deepseek-ai/dsh-schedule/remote' import llmRemote from '@deepseek-ai/dsh-llm/remote' import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote' import pluginManagerRemote from '@deepseek-ai/dsh-plugin-manager/remote' +import toolResultsRemote from '@deepseek-ai/dsh-client-ui-trajectory/remote' import pluginRegistryProbeRemote from '@deepseek-ai/dsh-client-ui-plugin-manager/remote' import pluginMarketRemote from '@deepseek-ai/dsh-plugin-market/remote' import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote' @@ -41,6 +42,7 @@ export type { } from '@deepseek-ai/dsh-plugin-manager/types' export type {} from '@deepseek-ai/dsh-plugin-manager/remote' export type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/remote' +export type {} from '@deepseek-ai/dsh-client-ui-trajectory/remote' export type {} from '@deepseek-ai/dsh-plugin-market/remote' export type { MarketCompatibility, MarketIndexSummary, MarketInstallation, MarketInstallationStatus, MarketListingEntry, MarketListingVersion, @@ -189,7 +191,7 @@ export async function apply(ctx: Context): Promise<() => Promise> { for (const contribution of [ productAnalyticsRemote, agentPresetsRemote, commandsRemote, settingsControllerRemote, accountRemote, goalsRemote, llmRemote, dynamicRemote, scheduleRemote, - pluginInventoryRemote, pluginManagerRemote, pluginMarketRemote, pluginRegistryProbeRemote, + pluginInventoryRemote, pluginManagerRemote, pluginMarketRemote, pluginRegistryProbeRemote, toolResultsRemote, messageFeedbackRemote, sessionFeedbackRemote, fileUploadsRemote, sessionReferencesRemote, permissionPresetsRemote, subagentsRemote, sessionRemote, jobRemote, workspaceRemote, workspaceFilesRemote, terminalRemote, diff --git a/packages/api/remotes/src/remote-events.ts b/packages/api/remotes/src/remote-events.ts index ef4ac40a45..38f87e99b4 100644 --- a/packages/api/remotes/src/remote-events.ts +++ b/packages/api/remotes/src/remote-events.ts @@ -44,6 +44,7 @@ export const API_REMOTE_FORWARDED_EVENTS = [ { event: 'cordis/inspect-query', mode: 'emit' }, { event: 'cordis/inspect-query-resolved', mode: 'emit' }, { event: 'llm/adapters-updated', mode: 'emit' }, + { event: 'llm/models-updated', mode: 'emit' }, { event: 'permission-presets/catalog-changed', mode: 'emit' }, { event: 'plugin-manager/changed', mode: 'emit' }, { event: 'plugin-manager/install-log', mode: 'emit' }, diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index 3eadb51df1..4a7e14aa2b 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/session-controller/src/catalog.ts b/packages/api/session-controller/src/catalog.ts index 83ee7a8b93..842f15429f 100644 --- a/packages/api/session-controller/src/catalog.ts +++ b/packages/api/session-controller/src/catalog.ts @@ -44,6 +44,7 @@ export async function buildModelCatalog( name: model.name, ...(model.description === undefined ? {} : { description: model.description }), ...(reasoning === undefined ? {} : { reasoning }), + ...(model.badge === undefined ? {} : { badge: model.badge }), } })) return { diff --git a/packages/api/session-controller/src/client/sessions/session.ts b/packages/api/session-controller/src/client/sessions/session.ts index 9cb61e55bf..253da9c463 100644 --- a/packages/api/session-controller/src/client/sessions/session.ts +++ b/packages/api/session-controller/src/client/sessions/session.ts @@ -800,7 +800,10 @@ export class Session implements SessionFace { } } this.observeSubmissionInsertions(target, inserted, start, event.seq) - for (const message of inserted) this.observeSubmissionMessage(message, false) + // A queued echo waits for the inbox projection the queue dock renders, which can arrive after this + // event; retiring it here would empty the dock for a frame. A claim in the same projection notification + // still retires it through the durable user message. + for (const message of inserted) this.observeSubmissionMessage(message, false, false) return } if (event.type === 'request/context' || event.type === 'turn/end') { @@ -818,21 +821,24 @@ export class Session implements SessionFace { const source = message.source if (source.kind !== 'user' || !('rpcId' in source)) continue const settlement = this.submissionSettlements.get(source.rpcId) - if (settlement === undefined || settlement.placement === 'queued' - || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq) continue + if (settlement === undefined || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq) continue + // Either proof accepts a prompt; an Inbox row can reach the Client before the prompt receipt. Later splices + // move the recorded position, so a canceled splice that removes the row retires its echo, queued ones + // included; a queued echo otherwise leaves once the queue dock renders its row or its message is admitted. settlement.receipt = { target, seq, index: start + index, attachments: attachmentRefsIn(message.content) } - // An Inbox row can reach the Client before the prompt receipt. this.acceptPrompt(source.rpcId) } } - private observeSubmissionMessage(message: UserMessage, admitted: boolean): void { + private observeSubmissionMessage(message: UserMessage, admitted: boolean, retireQueued = true): void { const source = message.source if (source.kind !== 'user' || !('rpcId' in source)) return const settlement = this.submissionSettlements.get(source.rpcId) if (settlement === undefined || settlement.retiring) return if (!admitted) { - if (settlement.placement === 'queued') this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content)) + if (settlement.placement === 'queued' && retireQueued) { + this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content)) + } return } settlement.admitted = attachmentRefsIn(message.content) diff --git a/packages/api/session-controller/src/types.ts b/packages/api/session-controller/src/types.ts index e3e2ea375a..9e399d8f3b 100644 --- a/packages/api/session-controller/src/types.ts +++ b/packages/api/session-controller/src/types.ts @@ -7,7 +7,7 @@ import type { } from '@deepseek-ai/dsh-attachment' import type { Branded } from '@deepseek-ai/dsh-brand' import type { LlmAttemptId, MessageId } from '@deepseek-ai/dsh-llm/brand' -import type { TextBlock } from '@deepseek-ai/dsh-llm' +import type { LlmModelBadge, TextBlock } from '@deepseek-ai/dsh-llm' import type { SessionId, SessionSeqCursor } from '@deepseek-ai/dsh-session/types' import type { SessionProjectionMap, SessionProjectionSummaryExcluded, SessionProjectionSummaryValues } from '@deepseek-ai/dsh-session-projection/types' import type { Principal } from '@deepseek-ai/dsh-typert-protocol' @@ -110,6 +110,8 @@ export interface ModelCatalogModel { readonly name: string readonly description?: string readonly reasoning?: ModelReasoning + /** Provider-authored label shown beside the name; display data only. */ + readonly badge?: LlmModelBadge } /** One provider and its successfully loaded model catalog. */ diff --git a/packages/api/session-controller/tests/session-models.host.spec.ts b/packages/api/session-controller/tests/session-models.host.spec.ts index 7c7db7e856..f8fc4920c5 100644 --- a/packages/api/session-controller/tests/session-models.host.spec.ts +++ b/packages/api/session-controller/tests/session-models.host.spec.ts @@ -432,6 +432,7 @@ describe('Web session model selection', () => { const { ctx } = await harness() ctx.llm.registerAdapter(['plain'], new CatalogAdapter('Plain', [ { provider: 'plain', id: 'plain-model', name: 'Plain Model' }, + { provider: 'plain', id: 'badged-model', name: 'Badged Model', badge: { text: { en: 'New', zh: '新' }, tone: 'info' } }, ])) ctx.llm.registerAdapter(['described-reasoning'], new CatalogAdapter('Described Reasoning', [ { provider: 'described-reasoning', id: 'reasoning-model', name: 'Reasoning Model' }, @@ -451,7 +452,10 @@ describe('Web session model selection', () => { const catalog = await buildModelCatalog(ctx) expect(catalog.groups).toEqual(expect.arrayContaining([ - { id: 'plain', name: 'Plain', models: [{ id: 'plain-model', name: 'Plain Model' }] }, + { id: 'plain', name: 'Plain', models: [ + { id: 'plain-model', name: 'Plain Model' }, + { id: 'badged-model', name: 'Badged Model', badge: { text: { en: 'New', zh: '新' }, tone: 'info' } }, + ] }, { id: 'described-reasoning', name: 'Described Reasoning', diff --git a/packages/api/session-controller/tests/session-pending-submissions.client.spec.ts b/packages/api/session-controller/tests/session-pending-submissions.client.spec.ts index 608c59a8e0..09b411ecdf 100644 --- a/packages/api/session-controller/tests/session-pending-submissions.client.spec.ts +++ b/packages/api/session-controller/tests/session-pending-submissions.client.spec.ts @@ -119,6 +119,7 @@ describe('beginSubmission', () => { for (const { mode, running, target, placement } of [ { mode: 'queue', running: false, target: 'next-turn', placement: 'transcript' }, { mode: 'steer', running: true, target: 'next-step', placement: 'steering' }, + { mode: 'queue', running: true, target: 'next-turn', placement: 'queued' }, ] as const) { for (const path of ['projection', 'durable event'] as const) { it(`accepts a ${placement} prompt once the Inbox holds it, ahead of the Host receipt (${path})`, async ({ mock, start }) => { @@ -384,8 +385,11 @@ describe('observed retirement', () => { await pushEvent(mock, { type: 'agent/inbox/spliced', seq: SessionSeq(1), time: 2, data: { target: 'next-step', start: 0, inserted: [steerMessage] } }) flush() - expect(pending()).toEqual(['opening', 'steering']) - expect(queueRetired).toHaveBeenCalledExactlyOnceWith({ reason: 'observed', attachments: [imageRef('queued-image')] }) + // A queued echo waits for the inbox projection the queue dock renders, not the history event. + expect(pending()).toEqual(projectionFirst ? ['opening', 'steering'] : ['opening', 'queued', 'steering']) + if (projectionFirst) { + expect(queueRetired).toHaveBeenCalledExactlyOnceWith({ reason: 'observed', attachments: [imageRef('queued-image')] }) + } else expect(queueRetired).not.toHaveBeenCalled() await pushEvent(mock, ev.turnStart(SessionSeq(2), 1)) await pushEvent(mock, { type: 'agent/inbox/spliced', seq: SessionSeq(3), time: 4, data: { target: 'next-step', start: 0, removedCount: 1, inserted: [] } }) @@ -396,15 +400,17 @@ describe('observed retirement', () => { await pushEvent(mock, ev.stepStart(SessionSeq(5), 1, 1)) await pushEvent(mock, { type: 'user/message', seq: SessionSeq(6), time: 7, surfaceOp: 'append', data: steerMessage }) flush() - expect(pending()).toEqual(projectionFirst ? ['opening'] : ['opening', 'steering']) + expect(pending()).toEqual(projectionFirst ? ['opening'] : ['opening', 'queued', 'steering']) expect(normalRetired).not.toHaveBeenCalled() if (!projectionFirst) expect(steerRetired).not.toHaveBeenCalled() await pushEvent(mock, { type: 'user/message', seq: SessionSeq(7), time: 8, surfaceOp: 'append', data: normalMessage }) flush() if (!projectionFirst) { - expect(pending()).toEqual(['opening', 'steering']) + expect(pending()).toEqual(['opening', 'queued', 'steering']) session.projections.apply('inbox', accepted, SessionSeq(1)) + await Promise.resolve() flush() + expect(queueRetired).toHaveBeenCalledExactlyOnceWith({ reason: 'observed', attachments: [imageRef('queued-image')] }) expect(normalRetired).not.toHaveBeenCalled() session.projections.apply('inbox', claimed, SessionSeq(4)) await Promise.resolve() @@ -461,16 +467,21 @@ describe('observed retirement', () => { expect(onRetire).toHaveBeenCalledExactlyOnceWith({ reason: 'observed', attachments: refs }) }) - for (const target of ['next-step', 'next-turn'] as const) { - it(`retires canceled or unadmitted ${target} input without removing still-pending input`, async ({ mock, start }) => { + for (const { target, running, mode, placement } of [ + { target: 'next-step', running: true, mode: 'steer', placement: 'steering' }, + { target: 'next-turn', running: false, mode: 'steer', placement: 'transcript' }, + { target: 'next-turn', running: true, mode: 'queue', placement: 'queued' }, + ] as const) { + it(`retires canceled or unadmitted ${placement} input without removing still-pending input`, async ({ mock, start }) => { const session = await sessionBench(mock, start, SID) await session.open() - session.handleRunning(target === 'next-step') + session.handleRunning(running) const canceled = vi.fn() const rejected = vi.fn() - const first = await submitAccepted(session, { mode: 'steer', text: 'cancel', attachments: [], onRetire: canceled }) - const second = await submitAccepted(session, { mode: 'steer', text: 'reject', attachments: [], onRetire: rejected }) - const unclaimed = await submitAccepted(session, { mode: 'steer', text: 'later', attachments: [] }) + const first = await submitAccepted(session, { mode, text: 'cancel', attachments: [], onRetire: canceled }) + const second = await submitAccepted(session, { mode, text: 'reject', attachments: [], onRetire: rejected }) + const unclaimed = await submitAccepted(session, { mode, text: 'later', attachments: [] }) + expect(session.getSnapshot().pendingSubmissions.map(echo => echo.placement)).toEqual([placement, placement, placement]) const foreign = createUserMessage({ source: { kind: 'user' }, content: [] }) await pushEvent(mock, { type: 'agent/inbox/spliced', seq: SessionSeq(0), time: 1, diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 1a01d513c4..e424a9e808 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/terminal-controller/package.json b/packages/api/terminal-controller/package.json index a7e7ca3a0e..84a53b5df6 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index 802e26fb0e..8a7363da4f 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/api/workspace-files/package.json b/packages/api/workspace-files/package.json index 4d1e0ca86a..5170995310 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment-local/package.json b/packages/attachment/attachment-local/package.json index 6ad5b54083..8e7071c71d 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/attachment/attachment/package.json b/packages/attachment/attachment/package.json index 7ef5c29d66..650a98b1ae 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 07e9757746..6bcc5fa3d2 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/cmdline/package.json b/packages/boot/cmdline/package.json index 804bb2bc11..04f5f23be1 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/config-editor/package.json b/packages/boot/config-editor/package.json index d8bfc04659..783ce890db 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/hmr/package.json b/packages/boot/hmr/package.json index 93fa7fb127..eeba988bfb 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/plugin-manager/package.json b/packages/boot/plugin-manager/package.json index 196eeb5a20..cee821053b 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/boot/plugin-manager/src/index.ts b/packages/boot/plugin-manager/src/index.ts index 271e6af0d3..7c70cc8526 100644 --- a/packages/boot/plugin-manager/src/index.ts +++ b/packages/boot/plugin-manager/src/index.ts @@ -522,6 +522,7 @@ export class PluginManager extends TypertRemoteService { catch (error) { platformError = managementError(error) } const error = platformError ?? targetError ?? (enabled ? { code: 'not-bundle' as const } : undefined) bundles.push({ name, official, availability: 'missing', ...catalogMeta, ...target, enabled, installed, optional, + ...offered.listedByDefault === undefined ? {} : { listedByDefault: offered.listedByDefault }, ...platformError?.code === 'unsupported-platform' ? { listedByDefault: false } : {}, removable: enabled, rows: [], overrides: [], ...error === undefined ? {} : { error } }) diff --git a/packages/boot/plugin-manager/tests/manager.spec.ts b/packages/boot/plugin-manager/tests/manager.spec.ts index 0f8290a48d..ef895f55cf 100644 --- a/packages/boot/plugin-manager/tests/manager.spec.ts +++ b/packages/boot/plugin-manager/tests/manager.spec.ts @@ -1773,9 +1773,13 @@ it('lists absent Official entries offline without resolving their packages or co onTestFinished(() => { resolve.mockRestore(); registry.mockRestore() }) const bundles = await manager.listBundles() for (const entry of OFFICIAL_ON_DEMAND_CATALOG) { - expect(bundles.filter(bundle => bundle.name === entry.packageName)).toEqual([{ + expect(bundles.filter(bundle => bundle.name === entry.packageName)).toHaveLength(1) + expect(bundles.filter(bundle => bundle.name === entry.packageName)).toMatchObject([{ name: entry.packageName, official: true, availability: 'missing', enabled: false, installed: false, optional: false, removable: false, updatable: false, rows: [], overrides: [], meta: entry.meta, + ...entry.listedByDefault === undefined ? {} : { listedByDefault: entry.listedByDefault }, + ...entry.platforms !== undefined && !entry.platforms.includes(process.platform) + ? { listedByDefault: false, error: { code: 'unsupported-platform' } } : {}, installTarget: { spec: `${entry.packageName}@${getDshRuntimeVersion()}`, version: getDshRuntimeVersion() }, }]) expect(resolve.mock.calls.some(([name]) => name === entry.packageName)).toBe(false) diff --git a/packages/boot/plugin-market/package.json b/packages/boot/plugin-market/package.json index 863b4300b3..86c97b79b7 100644 --- a/packages/boot/plugin-market/package.json +++ b/packages/boot/plugin-market/package.json @@ -1,7 +1,7 @@ { "name": "@deepseek-ai/dsh-plugin-market", "description": "Configured plugin market indexes for the Web Discover page and the plugin_market agent tool", - "version": "0.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/browser-use/browser-use/package.json b/packages/browser-use/browser-use/package.json index a32c90047e..5a67143621 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/acp-app/package.json b/packages/bundle/acp-app/package.json index 0d3500d904..d48b0bf711 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index e6daed00a9..6c6e508732 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/headless/package.json b/packages/bundle/headless/package.json index d54cb3bb06..dd523037fe 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-app/package.json b/packages/bundle/sdk-app/package.json index 9734be57f2..7be81f2b9c 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/sdk-minimal/package.json b/packages/bundle/sdk-minimal/package.json index c7b1195003..499aad21e4 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 6e842cf61a..c0ae7077d9 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -413,9 +413,6 @@ - id: ui-model-selection name: '@deepseek-ai/dsh-client-ui-model-selection' - - id: ui-deepseek-model-label-1017 - name: '@deepseek-ai/dsh-client-ui-deepseek-model-label-1017' - - id: ui-permission name: '@deepseek-ai/dsh-client-ui-permission-presets' diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index aee3da55b1..3a94c4aeda 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, bash runtime variables, URL line)", - "version": "0.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, @@ -91,7 +91,6 @@ "@deepseek-ai/dsh-client-ui-layout": "workspace:*", "@deepseek-ai/dsh-client-ui-message-feedback": "workspace:*", "@deepseek-ai/dsh-client-ui-model-selection": "workspace:*", - "@deepseek-ai/dsh-client-ui-deepseek-model-label-1017": "workspace:*", "@deepseek-ai/dsh-client-ui-open-in-app": "workspace:*", "@deepseek-ai/dsh-client-ui-permission-presets": "workspace:*", "@deepseek-ai/dsh-client-ui-plan": "workspace:*", diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index 500bb30266..4ec2000650 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: 3cae9938f3a11040 - zh: 8ed2edf0ffe6eed0 + en: faf349da96142e7c + zh: 2565c9785b010940 /client-web-gui-browser-half/related-documentation: en: d1d506e68fab7333 zh: 1a90fe6f3f8f24a9 diff --git a/packages/client/README.md b/packages/client/README.md index 428498b221..22e971cd88 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -68,7 +68,6 @@ The kernel packages boot and serve the page; the UI feature packages present it. | [`ui-jobs/`](ui-jobs/README.md) | Lists this session's background jobs with on-demand streaming record panels | — | | [`ui-schedule/`](ui-schedule/README.md) | Lists the current Session's active reminders in a read-only header catalog | — | | [`ui-model-selection/`](ui-model-selection/README.md) | Provides model selection in conversation surfaces | — | -| [`ui-deepseek-model-label-1017/`](ui-deepseek-model-label-1017/README.md) | Adds the temporary 1017 badge to official DeepSeek Flash model labels | — | | [`ui-permission-presets/`](ui-permission-presets/README.md) | Configures default permissions and switches the current session's access | — | | [`ui-plan/`](ui-plan/README.md) | Presents active plan-mode status and its exit control | — | | [`ui-settings-plugins/`](ui-settings-plugins/README.md) | Owns the Built-in plugins settings section shell and its tab extension point | — | diff --git a/packages/client/README.zh.md b/packages/client/README.zh.md index c9912d944f..92fcb266a5 100644 --- a/packages/client/README.zh.md +++ b/packages/client/README.zh.md @@ -68,7 +68,6 @@ kind: "package-group" | [`ui-jobs/`](ui-jobs/README.zh.md) | 列出本会话的后台任务,并提供按需展开的流式输出面板 | — | | [`ui-schedule/`](ui-schedule/README.zh.md) | 在只读标题栏目录中列出当前会话中生效的提醒 | — | | [`ui-model-selection/`](ui-model-selection/README.zh.md) | 在对话界面中提供模型选择 | — | -| [`ui-deepseek-model-label-1017/`](ui-deepseek-model-label-1017/README.zh.md) | 为官方 DeepSeek Flash 模型名称添加临时的 1017 角标 | — | | [`ui-permission-presets/`](ui-permission-presets/README.zh.md) | 配置默认权限并切换当前会话的访问模式 | — | | [`ui-plan/`](ui-plan/README.zh.md) | 展示生效中的 plan mode 状态及其退出控件 | — | | [`ui-settings-plugins/`](ui-settings-plugins/README.zh.md) | 负责「内置插件」设置分区壳及其标签页扩展点 | — | diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index 3392544e2d..22f21df125 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index ff5eb04d4e..d91c931e21 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/hmr/package.json b/packages/client/hmr/package.json index 2e64b3e705..0ad8192ce6 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index 023f018f5e..44227c6ebe 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/modules/package.json b/packages/client/modules/package.json index 0ad5561a70..558dc133e9 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/product-analytics/package.json b/packages/client/product-analytics/package.json index e73a62160b..9848ca3c08 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/resources/package.json b/packages/client/resources/package.json index 944f8f8092..fd54263983 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/shortcuts/package.json b/packages/client/shortcuts/package.json index 77316c991f..8ae1eb16a2 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/store/package.json b/packages/client/store/package.json index 70f255823e..81ce31353c 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-agent-preset/package.json b/packages/client/ui-agent-preset/package.json index d370dff454..18982db48e 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-approval/package.json b/packages/client/ui-approval/package.json index 9ab90c9d66..a1a6a8e7c2 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json index b63d7f9ce2..ed6496c2cf 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-attachment/tsconfig.json b/packages/client/ui-attachment/tsconfig.json index 1b7d6e0129..6d6e4792bc 100644 --- a/packages/client/ui-attachment/tsconfig.json +++ b/packages/client/ui-attachment/tsconfig.json @@ -27,7 +27,7 @@ "path": "../ui-tool" }, { - "path": "../ui-trajectory" + "path": "../ui-trajectory/tsconfig.client.json" }, { "path": "../ui-slots" diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index e70b38f4d2..468dd1613d 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 2ff8f221ce..d182421a5d 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -15,8 +15,8 @@ en: 41cd21266011d571 zh: 3172d1a2acbb27b7 /deepseek-ai-dsh-client-ui-chat/reference-previews: - en: a05d85ff4949b526 - zh: 7261ffc98665e9e7 + en: 9386791a9e5efa00 + zh: 7cb9f3bbc42319d6 /deepseek-ai-dsh-client-ui-chat/hidden-chat-rows: en: cbf1f0a2205046a5 zh: 2e59d0815351349f @@ -36,8 +36,8 @@ en: 3afad0b4c6bf9ce0 zh: d8179ba4ee9fcd61 /deepseek-ai-dsh-client-ui-chat/scroll-ownership: - en: 009ecb4fecd4ee9e - zh: ec2714337a1102c5 + en: 91e1f5f12ca51211 + zh: b92090966d58b82c /deepseek-ai-dsh-client-ui-chat/model-experience: en: c17b4dd43a61c569 zh: f7414309810c3a08 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index ceeae1d086..c9d97805f3 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -37,13 +37,15 @@ Image drag actions arrive through the image entry’s inject face from [ui-file- Sent generic-file attachments retain the draft's [file-card radius](../../../docs/ui-radius.md#radius-scale). -`ChatView` supplies file and HTTP(S) navigation through the existing `MarkdownDelegateProvider` around its node list, passing `requestOpenFile`, `openExternalLink`, and `fileImages` directly. Its file opener retains Chat's failure feedback and retry handling. Assistant Markdown file links open in the right Sidebar after the message settles, including references to unmodified files. Relative paths resolve in the viewed Session's workspace; absolute paths retain the same Session's filesystem access. `#L24` and `#L24-L30` navigate to the first specified line and reuse an existing file tab. Missing files show the preview's error state. +`ChatView` supplies file and HTTP(S) navigation through the existing `MarkdownDelegateProvider` around its node list, passing `requestOpenFile`, `openExternalLink`, and `fileImages` directly. Its file opener retains Chat's failure feedback and retry handling. Assistant Markdown file links open in the right Sidebar after the message settles, including references to unmodified files. Relative paths resolve in the viewed Session's workspace; absolute paths retain the same Session's filesystem access. [Supported Markdown line references](../ui-primitives/README.md#rendering-agent-output) navigate to the first specified line and reuse an existing file tab. Missing files show the preview's error state. Native message images use separate user and output positions. The `conversation.chat.user.content` Factory declares `conversation.chat.user.images`, which `UserContent` renders; the Assistant node registration declares `conversation.chat.message.images`, which `AssistantMarkdown` renders. Both receive image sources, the authorized `loadImage`, and layout options through explicit props. Their official `MessageImages` entries call the shared root-scoped `conversation.images.content` Factory. Each position can be replaced independently without duplicating the gallery implementation. The session-scoped `conversation.chat.user.content` Factory renders only the user content stack: text, extra blocks, image/file attachments in input order, and reference summaries. Its [`UserContentInput`](src/client/contract/slots.ts) carries prepared `content`, transform `facts`, display props, and explicit `loadImage`; node-owned injections provide reference actions. It calls the shared `conversation.user.text` Factory for text and its own image Slot for images. Direct callers need a Session binding and supply their own row wrapper. -`UserStyleBubble` and the [source-specific wrappers]() retain the user row, keys, pending/echo markers, timestamps, copy controls, and other message actions. The content Factory reads neither source-specific models nor private Context, and is not a global user-content override entry point. +`UserStyleBubble` and the [source-specific wrappers]() retain the user row, keys, pending/echo markers, timestamps, copy controls, and other message actions. The content Factory reads neither source-specific models nor private Context, and is not a global user-content override entry point. + +The official user and steering renderer declares the session-scoped `conversation.chat.user.before` chain slot at its `user` registration and renders it directly above the bubble with `renderSlotChain`; the `steering` registration shares the renderer without owning the seat. Owner props carry the durable message `source` and row `kind`; each entry’s `select` matches its own sources, and the first non-null result reaches the elected component as `matched`. No match displays no content. Entries own their markup and copy; Chat does not interpret message sources. Standalone Markdown images show contained previews and open the shared image lightbox; local paths resolve against the viewed workspace after settlement. Image file links keep their sidebar activation and show a thumbnail after hover dwell or keyboard focus. Escape dismisses the thumbnail. Failed images retain a localized status and their description; no duplicate-image filtering is applied. @@ -134,7 +136,7 @@ Process rows share one text and icon color in every Work details mode: tertiary Chat waits for initial history before automatic content, scroll, or size updates initialize reading state. Running Sessions restore their initial visibility and position before fold animations become eligible. -Chat restores semantic anchors across history prepend and renderer remounts, with browser scroll anchoring disabled on its scrollport only while following the tail. Pinned scroll deliveries without reader movement, and reader input that reaches the floor within subpixel rounding, update follow ownership immediately, before subsequent layout changes can invalidate their floor. Other reader movement remains pending until the sampling interval or `scrollend`, even inside the follow threshold, so layout growth cannot erase small scroll gestures. Under completion-time folding, submitting transcript input or steering immediately restores tail following and clears an older pending reader sample. While the reader is pinned to the floor, `ResizeObserver` follows the new floor and selects the latest loaded Turn without reading row geometry. Once the reader moves away, flow-height changes preserve the top position and the reading-line geometry selects the active Turn. Turn-rail previews paint above sticky Markdown code-block banners, while the rail frame remains inside the transcript band above the composer. +Chat restores semantic anchors across history prepend and renderer remounts, with browser scroll anchoring disabled on its scrollport only while following the tail. Pinned scroll deliveries without reader movement, and reader input that reaches the floor within subpixel rounding, update follow ownership immediately, before subsequent layout changes can invalidate their floor. Other reader movement remains pending until the sampling interval or `scrollend`, even inside the follow threshold, so layout growth cannot erase small scroll gestures. Under completion-time folding, submitting transcript input or steering immediately restores tail following and clears an older pending reader sample. Under next-message folding, transcript input that no fold batch carries lands on one native animation; steering lands at once under either timing, because the running Turn keeps streaming above its echo. While the reader is pinned to the floor, `ResizeObserver` follows the new floor and selects the latest loaded Turn without reading row geometry. Once the reader moves away, flow-height changes preserve the top position and the reading-line geometry selects the active Turn. Turn-rail previews paint above sticky Markdown code-block banners, while the rail frame remains inside the transcript band above the composer. The turn rail and back-to-bottom button sit outside the clipped transcript. They use the shared conversation scrollport for sticky positioning, or the Chat frame for absolute positioning when Chat owns its scrollport. The rail hides when the transcript's available width, excluding its horizontal padding, is at most 900px; the browser viewport width is not the criterion. @@ -142,7 +144,9 @@ The transcript root uses `overflow-x: visible; overflow-y: clip`: vertical overf Outer transcript following and each open capped group's following are independent. Native animation progress retains follow intent; only that scrollport's own `scrollend` settles its animation. Browsers without `scrollend` may retain the native follow target until a later bottom-aligned update or reader gesture. A reader gesture interrupts the animation, and actual movement determines whether following remains enabled. Scroll chaining can move the outer transcript, which then applies its own distance threshold. The back-to-bottom button restores only outer following. -Next-message folding moves every batch of closing rows and the scrollport on one frame clock, with browser scroll anchoring disabled during the fold; there is no separate reserve-then-follow path. A row that starts closing or revealing while a batch runs keeps its size until that batch settles and then changes in the next batch; a batch continues each row from the size it already has. The batch lands at the tail when this client's own input caused it (including a subagent child's continuation) or when the reader was following; otherwise the reader's visible content stays still, whoever sent the message. Content above the reading line never slides back down: when the retained answer and the new content cannot fill the viewport, only that deficit remains as bottom space, and new content fills it before following advances again. Reader gestures cancel the automatic target and active native following; resize cannot revive it in the same fold, but new submitted input can. Acknowledged scrolling releases only space that can be removed without clamping the reader. Unloading Chat cancels waiting callbacks and row transitions. Reduced-motion users receive immediate visibility and positioning changes. +Next-message folding moves every batch of closing rows and the scrollport on one frame clock, with browser scroll anchoring disabled during the fold; there is no separate reserve-then-follow path. A row that starts closing or revealing while a batch runs keeps its size until that batch settles and then changes in the next batch; a batch continues each row from the size it already has. The batch lands at the tail when this client's own input caused it (including a subagent child's continuation) or when the reader was following; otherwise the reader's visible content stays still, whoever sent the message. When a batch takes over following because content changed, such as a Turn ending while a next input waits, content that grew outside the batch since the last paint moves the scroll position before the first frame, as following does for streaming growth, so the batch animates only its own rows; growth that arrives with the reader's own input lands on the batch's animation. Content above the reading line never slides back down: when the retained answer and the new content cannot fill the viewport, only that deficit remains as bottom space, and new content fills it before following advances again. Reader gestures cancel the automatic target and active native following; resize cannot revive it in the same fold, but new submitted input can. Acknowledged scrolling releases only space that can be removed without clamping the reader. Unloading Chat cancels waiting callbacks and row transitions. Reduced-motion users receive immediate visibility and positioning changes. + +Completion-time folding closes a Turn's work rows at once. When the reader follows the tail, the first row visible before the close keeps its screen position, or, when that row closed, the first surviving row that followed it on screen, else the nearest surviving row above it; bottom space fills the gap, at most half the scrollport, and later growth takes that space before following advances. Changing Work details clears reserved bottom space after active folds finish and reconciles the scroll range with the selected mode. Readers already following the tail stay at the new bottom; other readers are not forced to follow. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 9113ed2f81..95ff3aacb5 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -37,13 +37,15 @@ kind: "package-reference" 已发送的普通文件附件保留草稿中的[文件卡片圆角](../../../docs/ui-radius.zh.md#radius-scale)。 -`ChatView` 通过节点列表外围既有的 `MarkdownDelegateProvider` 提供文件及 HTTP(S) 导航,直接传入 `requestOpenFile`、`openExternalLink` 和 `fileImages`。文件打开动作保留 Chat 的失败反馈与重试处理。Assistant Markdown 文件链接在消息落定后可于右侧栏打开,包括未修改文件的引用。相对路径基于当前查看的 Session 工作区解析;绝对路径仍使用同一 Session 的文件系统访问。`#L24` 和 `#L24-L30` 定位到指定起始行,并复用现有文件标签。文件缺失时显示预览错误状态。 +`ChatView` 通过节点列表外围既有的 `MarkdownDelegateProvider` 提供文件及 HTTP(S) 导航,直接传入 `requestOpenFile`、`openExternalLink` 和 `fileImages`。文件打开动作保留 Chat 的失败反馈与重试处理。Assistant Markdown 文件链接在消息落定后可于右侧栏打开,包括未修改文件的引用。相对路径基于当前查看的 Session 工作区解析;绝对路径仍使用同一 Session 的文件系统访问。[支持的 Markdown 行号引用](../ui-primitives/README.zh.md#rendering-agent-output)定位到指定起始行,并复用现有文件标签。文件缺失时显示预览错误状态。 原生消息图片使用独立的用户与输出位置。`conversation.chat.user.content` Factory 声明 `conversation.chat.user.images`,由 `UserContent` 渲染;Assistant 节点注册声明 `conversation.chat.message.images`,由 `AssistantMarkdown` 渲染。两者通过显式 props 接收图片来源、授权 `loadImage` 和布局参数。官方 `MessageImages` 条目调用共享的 root 作用域 `conversation.images.content` Factory。每个位置均可独立替换,无需复制画廊实现。 Session 作用域的 `conversation.chat.user.content` Factory 只渲染用户内容区域:文本、额外内容块、按输入顺序排列的图片/文件附件及引用摘要。[`UserContentInput`](src/client/contract/slots.ts) 携带预处理的 `content`、变换 `facts`、显示 props 和显式 `loadImage`;引用动作由节点自己的 Injection 提供。正文调用共享 `conversation.user.text` Factory,图片调用自身拥有的图片 Slot。直接调用需要 Session 绑定,行包装层由调用方提供。 -`UserStyleBubble` 和[各来源包装层]() 保留用户行、key、pending/echo 标记、时间、复制控件及其他消息操作。内容 Factory 不读取来源专属模型或私有 Context,也不是全局用户内容替换入口。 +`UserStyleBubble` 和[各来源包装层]() 保留用户行、key、pending/echo 标记、时间、复制控件及其他消息操作。内容 Factory 不读取来源专属模型或私有 Context,也不是全局用户内容替换入口。 + +官方 user 与 steering 共用的渲染器在 `user` 注册项上声明 session-scoped `conversation.chat.user.before` chain slot,并在气泡上方直接通过 `renderSlotChain` 渲染;`steering` 注册项复用该渲染器但不拥有该槽位。owner props 携带 Session 记录的持久化消息 `source` 与行 `kind`;各条目的 `select` 匹配自己负责的来源,首个非 null 结果作为 `matched` 交给获选组件。无匹配时不显示内容。条目自行拥有标记和文案;Chat 不解析消息来源。 独立的 Markdown 图片以内嵌预览显示,点击打开共享图片浮层;本地路径在消息落定后基于当前查看的工作区解析。图片文件链接保持点击打开侧栏,鼠标停留或键盘聚焦时显示缩略图,Esc 关闭缩略图。图片加载失败时保留本地化状态与图片说明;不执行重复图片过滤。 @@ -134,7 +136,7 @@ Chat 节点 slot 为推理与工具注入绑定重置来源的 `useDisclosure` Chat 等待初始历史就绪后,才允许自动内容、滚动或尺寸更新初始化阅读状态。进行中的 Session 先恢复初始显隐和位置,再允许折叠动画。 -Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点,并且只在跟随底部时禁用自己滚动区域的浏览器自动锚定。没有读者移动的贴底滚动事件,以及到达底部(容许小于 1px 的取整误差)的读者输入,会立即更新跟随归属,避免后续布局变化使其底部位置失效。其他读者移动即使位于跟随阈值内,也保持待处理直到采样周期或 `scrollend`,防止布局增长抵消小幅滚动操作。采用完成时收起时,提交正文输入或 steering 会立即恢复跟随底部,并清除先前待处理的读者滚动采样。读者跟随底部时,`ResizeObserver` 追随新的底部,并且无需读取行几何就选中最后一个已加载轮次;读者离开底部后,高度变化会保持顶部位置,再由阅读线几何选择活跃轮次。轮次导航预览位于 Markdown 代码块粘性头栏上方,而导航外框始终处于 composer 上方的 transcript 区域内。 +Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点,并且只在跟随底部时禁用自己滚动区域的浏览器自动锚定。没有读者移动的贴底滚动事件,以及到达底部(容许小于 1px 的取整误差)的读者输入,会立即更新跟随归属,避免后续布局变化使其底部位置失效。其他读者移动即使位于跟随阈值内,也保持待处理直到采样周期或 `scrollend`,防止布局增长抵消小幅滚动操作。采用完成时收起时,提交正文输入或 steering 会立即恢复跟随底部,并清除先前待处理的读者滚动采样。采用下次消息时收起时,没有折叠批次承载的正文输入用一次原生动画落到底部;steering 在两种时机下都立即落到底部,因为运行中的 Turn 仍在其回显上方持续输出。读者跟随底部时,`ResizeObserver` 追随新的底部,并且无需读取行几何就选中最后一个已加载轮次;读者离开底部后,高度变化会保持顶部位置,再由阅读线几何选择活跃轮次。轮次导航预览位于 Markdown 代码块粘性头栏上方,而导航外框始终处于 composer 上方的 transcript 区域内。 轮次轨道与回到底部按钮位于正文裁剪层外。它们相对共享会话滚动容器做 sticky 定位;Chat 自己持有滚动容器时,则相对 Chat 外框做 absolute 定位。正文扣除水平内边距后的可用宽度不超过 900px 时,隐藏轮次轨道;判断依据不是浏览器视口宽度。 @@ -142,7 +144,9 @@ Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点,并且 外层文本记录与每个已展开限高组的跟随状态彼此独立。原生动画的中间滚动保留跟随意图;只有该滚动容器自身的 `scrollend` 才结束其动画。不支持 `scrollend` 的浏览器可能保留原生跟随目标,直到后续贴底更新或读者手势将其结束。读者手势中断动画,是否继续跟随由实际位移决定。滚动传递可以带动外层,外层再按自身离底距离判断。回到底部按钮只恢复外层跟随。 -下次消息触发的折叠中,每一批收起行与滚动容器在同一个帧时钟上推进,折叠期间禁用浏览器滚动锚定;不存在「先预留、再跟随」的另一条路径。批次运行期间开始收起或展开的行先保持当前尺寸,等当前批次落定后在下一批中变化;批次从每一行已有的尺寸继续。由本客户端自己的输入(包括子代理会话的 continuation)触发、或读者原本在跟尾时,这一批落到底部;其余情况下,无论消息由谁发出,读者正在看的内容保持不动。阅读线以上的内容不会回滑:保留的正文和新内容填不满视口时,只把差额保留为底部空间,后续新增内容先填满它,再继续跟尾。读者手势取消自动目标及进行中的原生跟随;同一次折叠中的尺寸变化不能恢复它,但新提交的输入可以。已确认的滚动只释放不会造成位置钳制的多余空间。卸载 Chat 时取消等待回调和行过渡。启用减少动态效果偏好时,可见性和定位立即变化。 +下次消息触发的折叠中,每一批收起行与滚动容器在同一个帧时钟上推进,折叠期间禁用浏览器滚动锚定;不存在「先预留、再跟随」的另一条路径。批次运行期间开始收起或展开的行先保持当前尺寸,等当前批次落定后在下一批中变化;批次从每一行已有的尺寸继续。由本客户端自己的输入(包括子代理会话的 continuation)触发、或读者原本在跟尾时,这一批落到底部;其余情况下,无论消息由谁发出,读者正在看的内容保持不动。批次因内容变化接管跟随时(例如下一条输入在等待时一轮结束),上次绘制以来批次之外的内容增长在第一帧之前由滚动位置吸收,与跟随吸收流式增长的方式相同,批次只为自己的行做动画;随读者自己的输入到达的增长则随批次动画落定。阅读线以上的内容不会回滑:保留的正文和新内容填不满视口时,只把差额保留为底部空间,后续新增内容先填满它,再继续跟尾。读者手势取消自动目标及进行中的原生跟随;同一次折叠中的尺寸变化不能恢复它,但新提交的输入可以。已确认的滚动只释放不会造成位置钳制的多余空间。卸载 Chat 时取消等待回调和行过渡。启用减少动态效果偏好时,可见性和定位立即变化。 + +完成时收起会一次性收起一轮的工作行。读者跟随底部时,收起前第一个可见行保持屏幕位置;若该行本身被收起,则由收起前在屏幕上位于其后的第一个保留行保持不动,没有这样的行时才由其上方最近的保留行保持不动。底部空间填补空缺,最多半个滚动区域高度,后续增长先占用这部分空间,跟随才继续前进。 切换工作步骤展示档位时,当前折叠结束后清空底部预留空间,并按所选档位调整滚动范围。原本跟随底部的读者保持在新的底部;其他读者不会被强制拉回底部。 diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index ed088904bf..886912aa6c 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.2.1-rc.3", + "version": "0.2.2-alpha.2", "publishConfig": { "access": "public" }, diff --git a/packages/client/ui-chat/src/client/chat/ChatFlow.tsx b/packages/client/ui-chat/src/client/chat/ChatFlow.tsx index 561632a671..86bcdaa542 100644 --- a/packages/client/ui-chat/src/client/chat/ChatFlow.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatFlow.tsx @@ -39,6 +39,11 @@ export const ChatFlow = memo(function ChatFlow({ ? location.turn.status === 'open' ? location.turn.start?.time : undefined : undefined }) + const latestTurnOpen = useChatNode(latestTurnAnchor ?? '', (node) => { + const location = node?.location + return (location?.kind === 'turn' || location?.kind === 'step') && location.turn.status === 'open' + }) + const holding = !running && latestTurnOpen const seatProps = { nodeStore, useChatNode, useChatNodeBottom, useChatNodeProcess, usePresentation, useStore, actions, renderSlot, t, useGroupAction, deferCollapse, @@ -83,10 +88,13 @@ export const ChatFlow = memo(function ChatFlow({ && node.location.turn.status === 'open' && node.location.turn.turn !== lastInputTurn) { rows.splice(rows.length - 1, 0, ...leadingRows.splice(0)) } - // Rows enter before the keyed status and echoes after it, so React never moves its DOM node. + // Rows enter before the keyed status and echoes after it, so React never moves its DOM node. The + // running bit and the Turn's end arrive separately: after the bit clears, an open latest Turn keeps + // the status slot (hidden) until its actions row takes the slot in the same commit. return [ ...rows, ...leadingRows, ...openingRows, - ...running ? [] : [], + ...running || holding ? [] : [], ...pendingRows, ] }) 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 f05bad098f..5f640dbbad 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatView.module.css @@ -115,6 +115,13 @@ transition: none; } +/* A Turn's duration control renders empty until the Turn closes; its gap appears with it in the same + frame, so the layout below changes once instead of sliding after the control's own height. */ +.column[data-chat-motion] > [data-chat-flow-kind="turn-process"], +.column[data-chat-motion] > :global([data-slot="conversation.chat.flow"]) > [data-chat-flow-kind="turn-process"] { + transition: none; +} + @media (prefers-reduced-motion: reduce) { .column[data-chat-motion] > :not([data-chat-motion]), .column[data-chat-motion] > :global([data-slot="conversation.chat.flow"]) > :not([data-chat-motion]) { @@ -163,6 +170,15 @@ :where(.column > :global([data-slot="conversation.chat.flow"]) > .flowItem[data-chat-flow-kind="steering"]) + .running { animation: running-enters-slot 140ms cubic-bezier(0.2, 0.8, 0.2, 1); } + + /* An input fold runs the entrance at zero duration. The switch only changes + the duration of an entrance its structural change already created, as if + the entrance had it from its start: lifting the switch never replays an + entrance older than its duration, and setting it completes a running one. + The structural rule is wrapped in :where() so this rule outranks it. */ + .column[data-input-fold] .running { + animation-duration: 0s; + } } @keyframes running-enters-slot { @@ -175,66 +191,6 @@ } } -/* While the running Step has no output, the status sits directly below the - input. When the first output row appears, the status enters its slot below - that row from where it stood: it travels the row's gap plus one 24px row, - which follows the content font size. The gap is 4px for a process row or - group header (the default declared on `.running`), 12px for a response, and - 16px after the running Turn's process control, which renders empty until the - Turn closes, so it precedes a new Turn's first output. The default sits on - the single-class `.running` selector so both variants outrank it. Later - output keeps the same animation and does not restart it. */ -:is(.column, :global([data-slot="conversation.chat.flow"])) > :nth-last-child(2 of :not([hidden]):not([data-pending-steering]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))))[data-chat-group-part="response"] ~ .running { - --dsh-running-follow: calc(12px + 24px + var(--dsh-content-font-delta, 0px)); -} - -:is(.column, :global([data-slot="conversation.chat.flow"])) > [data-chat-flow-kind="turn-process"] + :nth-last-child(2 of :not([hidden]):not([data-pending-steering]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))) ~ .running { - --dsh-running-follow: calc(16px + 24px + var(--dsh-content-font-delta, 0px)); -} - -@media (prefers-reduced-motion: no-preference) { - :where(.column > :global([data-slot="conversation.chat.flow"]) > :nth-last-child(2 of :not([hidden]):not([data-pending-steering]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))):is( - .flowItem:not([data-chat-flow-kind="user"]):not([data-chat-flow-kind="steering"]):not([data-chat-flow-kind="turn-trigger"]), - [data-chat-group-key] - )) ~ .running { - animation: running-follows-output 110ms cubic-bezier(0.4, 0, 0.2, 1); - } - - /* Switches only change the duration of a move its structural change already - created. A duration change applies as if the move had it from its start, so - turning a switch off never replays a move whose structural change is older - than the move's duration, and plays the remainder of one that is younger; - turning a switch on completes a running move at once. The structural rules - above are wrapped in :where() so these rules outrank them. An input fold - runs both moves at zero duration. */ - - .column[data-input-fold] .running { - animation-duration: 0s; - } - - /* While the tail is pinned, following scrolls absorb output growth and the - status keeps its on-screen position, so the first-output slide runs at zero - duration. Admitted steering still passes its pending echo below the status, - so the steering entrance keeps its duration. */ - - .column[data-chat-tail-pinned] > :global([data-slot="conversation.chat.flow"]) > :nth-last-child(2 of :not([hidden]):not([data-pending-steering]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))):is( - .flowItem:not([data-chat-flow-kind="user"]):not([data-chat-flow-kind="steering"]):not([data-chat-flow-kind="turn-trigger"]), - [data-chat-group-key] - ) ~ .running { - animation-duration: 0s; - } -} - -@keyframes running-follows-output { - from { - opacity: 0; - translate: 0 calc(-1 * var(--dsh-running-follow)); - } - 40% { - opacity: 1; - } -} - /* 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)) { @@ -260,13 +216,19 @@ flex-direction: column; align-items: flex-start; --dsh-chat-flow-gap: 12px; - --dsh-running-follow: calc(4px + 24px + var(--dsh-content-font-delta, 0px)); --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)); } +/* After the Session stops running and before the Turn's actions row arrives, the status keeps its slot + unseen so the tail does not shrink for that interval. */ +.running[data-holding] { + visibility: hidden; + opacity: 0; +} + /* The last visible row before the status must contain output, not an input. Hidden and empty seats do not separate a user message from the status, and the steering/Inbox echoes after the status are outside the count. */ diff --git a/packages/client/ui-chat/src/client/chat/ChatView.tsx b/packages/client/ui-chat/src/client/chat/ChatView.tsx index f1a0556b9f..703fe65449 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatView.tsx @@ -192,6 +192,7 @@ export function ChatView({ lastIsUser: lastKey !== null && nodeStore.get(lastKey)?.kind === 'user', steeringId, submissionId: visibleSubmissions.at(-1)?.requestId ?? null, + submissionSteers: visibleSubmissions.at(-1)?.placement === 'steering', loadedTurns: turnNavigationItems, transcriptView, }) diff --git a/packages/client/ui-chat/src/client/chat/MessageItem.tsx b/packages/client/ui-chat/src/client/chat/MessageItem.tsx index f1535abcd9..f9551a8698 100644 --- a/packages/client/ui-chat/src/client/chat/MessageItem.tsx +++ b/packages/client/ui-chat/src/client/chat/MessageItem.tsx @@ -1,7 +1,7 @@ import { memo, useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' import type { PendingSubmission } from '@deepseek-ai/dsh-api-session-controller/client' -import type { PropsRenderFactories } from '@deepseek-ai/dsh-client-ui-slots' +import type { PropsRenderFactories, PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import type { UserTextFacts } from '@deepseek-ai/dsh-client-ui-conversation/client' import { JsonBlock, StateDot, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives' import type { @@ -160,15 +160,35 @@ function TurnMaxTokensItem({ t }: { ) } +/** + * A row the message-source seat decorates. A durable Chat node satisfies it; + * pending and local-echo rows pass a source-less stand-in. + */ +interface DecoratedRow { + readonly kind: 'user' | 'steering' + readonly data: { readonly source: unknown } +} + +/** Pending and local-echo rows carry no durable source for the seat to elect. */ +const UNDECORATED_ROW: DecoratedRow = { kind: 'user', data: { source: null } } + +/** Occupant used by rows that do not own the message-source seat. */ +const NO_MESSAGE_SOURCE: PropsRenderSlots<'conversation.chat.user.before'>['renderSlotChain'] = () => null + /** Right-aligned bubble shared by user and steering rows. */ function UserStyleBubble({ - content, loadImage, renderFactorySlot, actions, pending = false, echo = false, batchStart = false, + node, renderSlotChain, content, loadImage, renderFactorySlot, actions, + pending = false, echo = false, batchStart = false, referenceLabels, skillNames, previewAttachments, facts: inputFacts = EMPTY_TEXT_FACTS, interactive = false, }: { content: readonly unknown[] renderFactorySlot: PropsRenderFactories['renderFactorySlot'] /** Optional IconActions (or similar) below the bubble; receives the joined text. */ actions?: (text: string) => ReactNode + /** Row whose durable source the message-source seat elects above this bubble. */ + node: DecoratedRow + /** Seat renderer; rows that do not own the seat pass {@link NO_MESSAGE_SOURCE}. */ + renderSlotChain: PropsRenderSlots<'conversation.chat.user.before'>['renderSlotChain'] /** Whether this is the Host-authoritative pre-admission steering projection. */ pending?: boolean /** Whether this is a local submission echo (invisible marker; the echo renders exactly like its durable replacement). */ @@ -203,6 +223,7 @@ function UserStyleBubble({ data-submission-echo={echo || undefined} data-chat-steering-batch={batchStart ? 'start' : undefined} > + {renderSlotChain('conversation.chat.user.before', { source: node.data.source, kind: node.kind }, { fallback: null })} {renderFactorySlot('conversation.chat.user.content', { content: prepared, loadImage, @@ -227,6 +248,8 @@ export function PendingSteeringBubble({ content, loadImage, renderFactorySlot, t } & Pick): ReactNode { return ( ) { + node, renderFactorySlot, loadImage, renderSlotChain, t, +}: ChatNodeViewProps<'user' | 'steering'> & { + /** Bound by the `user` keyed registration, the only entry that declares the seat. */ + renderSlotChain: PropsRenderSlots<'conversation.chat.user.before'>['renderSlotChain'] +}) { const data = node.data return (