Merge remote-tracking branch 'origin/master' into feat/plugin-recommendations

This commit is contained in:
ZiyaZhang committed 2026-10-19 11:36:11 -07:00
commit 89fc15aa0e
222 files changed
+5726 -668

No files matched your search

@@ -0,0 +1,2 @@
2026-10-14-built-in-plugin-feature-previews.md: ae9b32340d9b1e77a99465445a05ccee05df0e79
2026-10-14-built-in-plugin-feature-previews.zh.md: 9229b0835a23a0eefffa1bd157efece333aebfa9
@@ -1,6 +1,7 @@
# Agent Note: Built-in plugin feature previews
Status: implemented
Archived: 2026-10-19
English | [中文](2026-10-14-built-in-plugin-feature-previews.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: 内置插件功能配图
Status: implemented
Archived: 2026-10-19
[English](2026-10-14-built-in-plugin-feature-previews.md) | 中文
+3
View File
@@ -2020,6 +2020,9 @@
"feature/2026-09-28-desktop-login-shell-environment.i18n.yaml": "sha256:6129edeb0a7ecf087911193ca5a15223a730ad01e30d2ead7739d01f589fafa7",
"feature/2026-09-28-desktop-login-shell-environment.md": "sha256:cd133b1ef1a178828d30b9db176a91597eedf9e67802551356f34d3e0f80206d",
"feature/2026-09-28-desktop-login-shell-environment.zh.md": "sha256:1422667a44aacc51736f31b328a8e171a39cb6b5168d6993e980e4324b23b98f",
"feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml": "sha256:4769fd7fa4881a38fa8e2682b2ea62ed5a5ec6588e87f225c2b106e1bd308200",
"feature/2026-10-14-built-in-plugin-feature-previews.md": "sha256:093e00da4cebf732861c1e5c47465fb00359eb89a86dc212f3f90b43f9b80cec",
"feature/2026-10-14-built-in-plugin-feature-previews.zh.md": "sha256:56adb70945f68b073357c437bbec9e69f110587cf302291f2f135d9fcb5537e7",
"process/2026-06-11-doc-sync-enforcement.i18n.yaml": "sha256:33b6d5874427bd7a2bd82e7e2f4f482b12448b2464aef15a9c57975edb48554d",
"process/2026-06-11-doc-sync-enforcement.md": "sha256:aa2fe83d519fc30d48dff19e596e83c8922aacc9e063e14fe2cc35b769b9100e",
"process/2026-06-11-doc-sync-enforcement.zh.md": "sha256:698017bd35f030fdea3eac51df9e43138c48140f504739d687b7251d13fced2b",
@@ -9,11 +9,11 @@
en: 47ba548b869be190
zh: c2e5038a56fe741c
/agent-note-asynchronous-projection-registration-and-retained-readers/decision:
en: 9e56d4bcbdc3a291
zh: bebaaab338a15e40
en: be0d17bfbd1b59d4
zh: 7a23bccdf95c21ff
/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered:
en: ee5b0cd715ed1fef
zh: 6b1478dcde21916d
en: e7c66403fab991d2
zh: 1580ec29116c7d42
/agent-note-asynchronous-projection-registration-and-retained-readers/consequences:
en: 6f0e04ef7924e2c6
zh: e3730273e323440d
en: 31ce6f02dd3bf39d
zh: bcd29b2146f689b5
@@ -10,12 +10,14 @@ Host readers coupled to registry key lookup lose access when a plugin unloads, a
## Decision
`SessionProjectionRegistry.register` returns a promise for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules.
`SessionProjectionRegistry.register` returns an awaitable registration for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules.
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives, snapshots, and checkpoints. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. AgentLoop acquires its inbox handle once before publishing its factory, reads the last turn before creating an Agent scope, and passes an inbox callback plus that turn number into synchronous Agent construction. The callback retains its handle through cleanup after the loop fiber removes global registrations. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state.
Plugins compiled against the synchronous API keep running during migration. The awaitable registration is also the original Cordis effect disposer, so calling it releases the registration and a composing effect adopts its ownership; that call is deprecated. Registry-level `stateOf(session, key)` remains a deprecated read returning `unknown`; it loses access after the final release and logs one `ctx.logger.warn` per key per registry. Registration itself does not warn, because an ignored result is indistinguishable from one awaited later. A legacy plugin may never observe the registration, so the registry attaches its own rejection handler: once the current macrotask and its microtasks finish, a rejection nobody observed through `then`, `catch`, or `finally` is logged as an error while the plugin stays active, and one whose owner is no longer loading or active is dropped. TypeScript consumers migrate their declarations; the host state map is not restored.
## Alternatives considered
**Keep registry-level lookup and delay unregistering.** This couples unrelated plugin disposal order to every reader's final access. Retained readers make cleanup independent of that order.
@@ -26,6 +28,12 @@ AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. Ag
**Convert function plugins to classes.** Cordis already awaits promises returned by `apply`. Preserving namespace exports avoids a published entry-point and diagnostic-name break.
**Require every external plugin to migrate immediately.** Compiled plugins fail at runtime on the missing disposer and keyed read, although the projection keys and state fields they read are unchanged.
**Return a new wrapper around the disposer.** Cordis composition adopts effects by their returned identity. A wrapper leaves the original registration owned by the fiber, so owner disposal can release it before the composed cleanup finishes.
**Dispose the plugin after an ignored rejection.** This approximates the old failed activation, but it needs a startup observer, runs only after the plugin's services became visible, and ends in `DISPOSED` rather than a retryable `FAILED`. Logging keeps the failure visible without a second lifecycle path.
## Consequences
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown.
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess verify the deprecated calls.
@@ -10,12 +10,14 @@ Status: implemented
## Decision
`SessionProjectionRegistry.register` 返回类型化句柄的 Promise。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
`SessionProjectionRegistry.register` 返回可等待的注册结果,用于获取类型化句柄。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`。AgentLoop 在发布工厂前只获取一次 inbox 句柄,在创建 Agent 作用域前读取最后轮次,并将 inbox 回调与该轮次编号传入同步 Agent 构造过程。loop fiber 移除全局注册后,回调仍在清理期间保留其句柄。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。
按同步 API 编译的插件在迁移期间继续运行。可等待的注册结果同时就是原有的 Cordis effect 释放函数:调用它会释放注册,组合 effect 也会接管其所有权;该调用已弃用。注册表级 `stateOf(session, key)` 作为已弃用的读取保留,返回 `unknown`;最后一次释放后它失去访问,并且每个 registry 对每个键输出一次 `ctx.logger.warn`。注册本身不产生警告,因为被忽略的结果与稍后才等待的结果无法区分。旧插件可能从不观察注册结果,因此 registry 挂上自己的拒绝处理函数:当前宏任务及其微任务结束时,没有任何调用方通过 `then`、`catch` 或 `finally` 观察的拒绝会被记录为错误,插件保持激活;owner 已不在加载或激活状态的拒绝会被丢弃。TypeScript 消费方仍需迁移声明;宿主状态映射不再恢复。
## Alternatives considered
**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。
@@ -26,6 +28,12 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
**将函数插件改为类。** Cordis 已等待 `apply` 返回的 Promise。保留命名空间导出可避免发布入口与诊断名称的破坏性变更。
**要求所有外部插件立即迁移。** 已编译的插件会因缺失的释放函数和按键读取在运行时失败,尽管它们读取的投影键与状态字段都没有变化。
**返回包装释放函数的新函数。** Cordis 组合按返回值身份接管 effect。包装函数会让原注册继续归 fiber 所有,因此 owner 卸载可能在组合清理结束前释放它。
**被忽略的拒绝出现后卸载插件。** 这近似于旧 API 的激活失败,但需要启动观察器,只能在插件服务已经可见后执行,并以 `DISPOSED` 而非可重试的 `FAILED` 结束。记录日志能让失败可见,而无需第二条生命周期路径。
## Consequences
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
@@ -1,19 +0,0 @@
# Bilingual-pair consistency record for 2026-10-14-built-in-plugin-feature-previews.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-14-built-in-plugin-feature-previews.md
/agent-note-built-in-plugin-feature-previews:
en: f6abb7d00b13d362
zh: b89d5da18677f31a
/agent-note-built-in-plugin-feature-previews/problem:
en: e38fa1e5b3a62b5a
zh: c612ea229705d695
/agent-note-built-in-plugin-feature-previews/decision:
en: aa12e3f72f7c2498
zh: 7dda9f061009aaa0
/agent-note-built-in-plugin-feature-previews/alternatives-considered:
en: 4a4416610894d7c6
zh: 4256e81e7ab70db5
/agent-note-built-in-plugin-feature-previews/consequences:
en: a3dbd70080416314
zh: 4d802da45085dfb0
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-19-sdk-native-user-questions.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-19-sdk-native-user-questions.md
/agent-note-sdk-native-user-questions:
en: b506991bd07d87dc
zh: 9f3e1ecb42163d72
/agent-note-sdk-native-user-questions/problem:
en: b3dbcdb4b5bce2d1
zh: 60df882dc253fcf5
/agent-note-sdk-native-user-questions/decision:
en: 0f7b71f0ef58f5f8
zh: 07bb12acad27d2fc
/agent-note-sdk-native-user-questions/consequences:
en: 46e3a450c6dce54e
zh: 9f99efc6f8d2ab8d
/agent-note-sdk-native-user-questions/alternatives-considered:
en: 9861066ae00e7bde
zh: 0dec00f4e3c692e0
/agent-note-sdk-native-user-questions/verification:
en: c16fb364fe8ec13b
zh: fa23c162348ba682
@@ -0,0 +1,29 @@
# Agent Note: SDK native user questions
Status: implemented
English | [中文](2026-10-19-sdk-native-user-questions.zh.md)
## Problem
The SDK can expose the shared `ask_user_question` tool without a Web GUI, but an out-of-process host cannot answer the native user-questions event through the SDK. Publishing the tool alone leaves calls without an answerer. The SDK must carry the interaction while the application chooses its user interface or simulated-user policy.
## Decision
The TypeScript and Python clients accept a question callback and advertise it during initialization. The SDK server adapts the existing user-questions seam to a bidirectional RPC only for its exact owned root Agents. Clients without the capability keep native in-process answerers. Tool definitions and the service's live-root and delegation checks remain shared with the desktop composition.
Question identity is independent of the JSON-RPC envelope so cancellation can identify outstanding host work. The runtime validates a complete answer batch, aborts pending requests when the tool or server stops, and ignores late responses. The host owns answer collection; the SDK neither chooses answers nor treats missing input as approval. The [protocol reference](../../../../packages/sdk/protocol/README.md#native-user-questions) owns wire details.
## Consequences
A GUI and runtime patch module are unnecessary. A host can use the same interface for a terminal, browser, or simulated user. The Python SDK keeps its synchronous API by running callbacks outside the reader thread; callbacks must observe cancellation and release their own resources. Late callbacks cannot publish into a later runtime process. Question waits are process-local; completed tool results retain ordinary Session persistence.
A Web Session controller is not an interaction provider for the SDK. Reminder restoration uses the SDK's own Session owner and initialized model options; it does not claim a second write handle through a GUI controller.
## Alternatives considered
Requiring a Web GUI or mounting its controller couples headless execution to another lifecycle owner. Asking each host to inject a runtime patch repeats the transport bridge outside the maintained SDK. Removing the shared question tool would reduce the model's available capabilities rather than supply its missing answer path.
## Verification
Server tests cover ownership, native fallback, answer validation, cancellation, and transport failure. Client tests cover concurrent bidirectional transport and cancellation, including callback advertisement and answer delivery with either explicit startup or a direct `initialize()` call. Initialization starts the runtime before constructing the handshake capabilities so both entry paths advertise the installed callback. Python tests also cover reentrant requests and a late callback after process replacement. Real shipped-profile tests drive the built-in question in standard and PTC modes and close a runtime while the host is answering. The [server tests](../../../../packages/sdk/server/tests/user-questions.spec.ts), [client tests](../../../../packages/sdk/client/tests/user-questions.spec.ts), [Python tests](../../../../python/sdk/tests/test_user_questions.py), and [profile tests](../../../../apps/cli/tests/profiles/sdk/user-questions.e2e.ts) retain this evidence.
@@ -0,0 +1,29 @@
# Agent Note: SDK 原生用户问答
Status: implemented
[English](2026-10-19-sdk-native-user-questions.md) | 中文
## 问题
SDK 可以在没有 Web GUI 时暴露共享的 `ask_user_question` 工具,但进程外宿主无法通过 SDK 回答原生用户问答事件。仅发布工具会使调用缺少回答方。SDK 必须承载交互,应用则自行选择用户界面或模拟用户策略。
## 决策
TypeScript 与 Python 客户端接收问答回调,并在初始化时声明。SDK server 仅为其确切拥有的根 Agent,把现有用户问答 seam 适配为双向 RPC。未声明能力的客户端保留原生进程内回答方。工具定义以及服务对存活根 Agent 和委派关系的检查仍与桌面端组合共享。
问题标识独立于 JSON-RPC 帧,使取消能够定位待决的宿主工作。运行时校验完整答案批次,在工具或服务器停止时中止待决请求,并忽略晚到响应。宿主拥有答案收集过程;SDK 不选择答案,也不把缺失输入当作批准。[协议参考](../../../../packages/sdk/protocol/README.zh.md#native-user-questions)拥有具体协议说明。
## 影响
此流程不要求 GUI 或运行时补丁模块。宿主可以用同一接口接入终端、浏览器或模拟用户。Python SDK 在读取线程之外执行回调,以保留同步 API;回调必须响应取消并释放自己的资源。晚到回调无法向后续新运行时进程发送结果。问题等待仅属于当前进程;已完成工具结果仍通过普通 Session 路径持久化。
Web Session controller 不能充当 SDK 的交互提供方。提醒恢复使用 SDK 自身的 Session 所有者和初始化模型选项,不通过 GUI 控制器争用第二个写入 handle。
## 已考虑的替代方案
要求 Web GUI 或挂载其控制器,会将 headless 执行与另一个生命周期所有者耦合。要求各宿主注入运行时补丁,会在受维护的 SDK 外重复实现传输适配。移除共享问答工具只会减少模型可用能力,无法提供缺失的回答通道。
## 验证
服务器测试覆盖所有权、原生回退、答案校验、取消和传输失败。客户端测试覆盖并发双向传输和取消,包括显式启动和直接调用 `initialize()` 两条路径的回调声明与答案投递。初始化在构造握手能力之前启动运行时,使两条入口均声明已安装的回调。Python 测试还覆盖回调内再次请求,以及进程替换后的晚到回调。真实发行 profile 测试在标准模式和 PTC 模式调用内建问答,并在宿主回答期间关闭运行时。[服务器测试](../../../../packages/sdk/server/tests/user-questions.spec.ts)、[客户端测试](../../../../packages/sdk/client/tests/user-questions.spec.ts)、[Python 测试](../../../../python/sdk/tests/test_user_questions.py)和 [profile 测试](../../../../apps/cli/tests/profiles/sdk/user-questions.e2e.ts)保留这些依据。
@@ -26,6 +26,9 @@
/agent-note-frontend-large-session-performance-budgets/calibration/trajectory-readiness-polling:
en: bdcfefd7358e335b
zh: f718f66ca5944203
/agent-note-frontend-large-session-performance-budgets/calibration/history-selector-invalidation:
en: 2d2f78f4cf6aad8a
zh: aec6af5f7c66de16
/agent-note-frontend-large-session-performance-budgets/alternatives-considered:
en: 4d2a4a64ccd14507
zh: 8a4aaf4d4abea13d
@@ -86,6 +86,22 @@ A controlled macOS arm64 diagnostic on Node 24.19.0, Chromium 149.0.7827.55 and
Complete local 240-turn runs produce Trajectory samples of 157.795/146.737/148.571 ms before the observer change and 130.184/191.247/157.543 ms afterward, with medians 148.571 and 157.543 ms. Both runs pass every fixed endpoint budget; no local speedup or hosted calibration is claimed. The readiness regression rejects a hidden searchbox or record despite a visible unrelated table row; restoring the original selector waits makes that test fail.
### History selector invalidation
The older-history control is the immediate sibling before the `display: contents` flow anchor. Leading spacing therefore uses `.column > .older + [data-slot="conversation.chat.flow"]`, preserving the earlier selector's specificity and visible-row filtering. An ancestor `.column:has(> .older)` combined with the descendant empty-row `:has()` makes unrelated draft mutations invalidate the history subtree. A temporary Chromium invalidation trace identified this exact spacing selector and style recalculations covering 13,591–13,641 elements. The trace is causal evidence, not a latency measurement: tracing itself adds substantial overhead.
On Linux x64, Node 24.19.0 and Chromium 149.0.7827.55, separate three-sample runs of the unchanged browser benchmark measured these medians using the original and sibling selectors. Both retained all 25,409 DOM nodes and every trusted input overlapped the streaming reply.
| Endpoint | Original selector | Sibling selector |
|---|---:|---:|
| Draft typing | 517.39 ms | 378.50 ms |
| First reply | 1556.89 ms | 801.58 ms |
| Stream main-thread task | 2540.59 ms | 1774.75 ms |
These local runs both failed the independent open budget: their open medians were 1192.85 and 1666.99 ms. A separate six-sample diagnostic alternated the original and sibling selectors in the browser CSSOM before timing, using the same built artifacts. Its original/sibling open wall medians were 2598.99/1349.22 ms, while Chromium task medians were 325.67/305.90 ms. This finds no browser-computation regression in those samples, but does not explain the remaining wait or certify the whole benchmark. The alternating diagnostic measured typing medians of 350.18/362.82 ms, so the earlier typing improvement is not uniform across runs; first-reply medians were 1498.48/815.85 ms and stream-task medians 2449.69/1915.73 ms. The diagnostic changes are not shipped; workload, overlap assertions, budgets and benchmark source remain unchanged.
Post-GC browser heap was 80.70/80.71 MiB. GNU time reported maximum RSS of 535.78/561.80 MiB for each benchmark command and its children; this is not input-phase memory or an aggregate browser-memory measurement. Moving the composer's selection reveal into an animation frame was rejected: profiling showed the style flush moved to another geometry read, without improving typing. The product fix removes the invalidation source instead.
## Alternatives considered
**Use the Node fold as paint evidence.** Rejected because it never performs DOM mutation, layout, or browser scheduling. The focused reconnect case likewise makes no GUI speed claim.
@@ -86,6 +86,22 @@ Trajectory 激活在动画帧上采样可见搜索框与 `[data-trajectory-scrol
完整本地 240 轮运行在修改观察器前得到 Trajectory 样本 157.795/146.737/148.571 ms,修改后为 130.184/191.247/157.543 ms,中位数分别为 148.571 和 157.543 ms。两次运行均通过所有固定终点预算;不声称本地提速或托管环境校准。即使存在可见的无关表格行,就绪回归测试仍拒绝隐藏的搜索框或记录;恢复原选择器等待会使该测试失败。
### 历史区域选择器失效
更早历史控件紧邻 `display: contents` 流锚点之前。因此首行间距使用 `.column > .older + [data-slot="conversation.chat.flow"]`,保持原选择器的优先级与可见行过滤。祖先 `.column:has(> .older)` 与后代空行 `:has()` 组合,会让无关的草稿修改使历史子树样式失效。临时 Chromium 失效跟踪定位到这条具体的间距选择器,样式重算覆盖 13,591–13,641 个元素。跟踪用于定位原因,不用于测量延迟:跟踪本身会增加大量开销。
在 Linux x64、Node 24.19.0 与 Chromium 149.0.7827.55 上,使用原选择器与兄弟选择器分别执行未修改的浏览器基准,每组取三个样本的中位数。两组都保留全部 25,409 个 DOM 节点,且每个受信任的输入都与流式回复重叠。
| 终点 | 原选择器 | 兄弟选择器 |
|---|---:|---:|
| 草稿键入 | 517.39 ms | 378.50 ms |
| 首段回复 | 1556.89 ms | 801.58 ms |
| 流式主线程任务 | 2540.59 ms | 1774.75 ms |
这两次本地运行都未通过独立的打开预算:打开中位数分别为 1192.85 与 1666.99 ms。另一项六样本诊断使用同一份构建产物,在计时前通过浏览器 CSSOM 交替使用原选择器与兄弟选择器。原版/修改版的打开壁钟中位数为 2598.99/1349.22 ms,Chromium 任务中位数为 325.67/305.90 ms。这些样本未观察到浏览器计算回归,但没有解释剩余等待,也不代表完整基准已通过。交替诊断的键入中位数为 350.18/362.82 ms,因此前述键入改善并非每次运行都能观察到;首段回复中位数为 1498.48/815.85 ms,流式任务中位数为 2449.69/1915.73 ms。诊断改动不随产品提交;负载、重叠断言、预算与基准源码保持不变。
强制 GC 后的浏览器 heap 为 80.70/80.71 MiB。GNU time 为两次基准命令及其子进程报告的最大 RSS 为 535.78/561.80 MiB;这不是输入阶段内存,也不是浏览器总内存测量。曾尝试将输入框的选区显示推迟到动画帧,但分析显示样式刷新转移到另一处几何读取,没有改善键入,因此未采用。产品修复直接移除样式失效的来源。
## 考虑过的替代方案
**用 Node 折叠作为绘制证据。** 拒绝,因为它不执行 DOM 修改、布局或浏览器调度。聚焦重连用例同样不声称 GUI 提速。
@@ -9,7 +9,7 @@ Draw recognizable artwork that belongs beside the existing official icons. Offic
## Optional feature previews
Before drawing a larger image below a built-in bundle's description, read and follow [adding a built-in plugin feature preview](../../../docs/cookbook/adding-a-plugin-preview.md). It owns optionality, image count, design canvas, display size and position, language variants, asset limits, and acceptance. The guide also owns the embedded-raster exception for SVG previews; small icons remain geometry-only. The icon dimensions below do not apply to previews. Inspect the real feature, preserve its produced assets, and keep built-in previews in the Plugin Manager client assets directory and its internal bilingual map.
Before drawing a larger image for the top of an official bundle's detail document, read and follow [adding a plugin feature preview](../../../docs/cookbook/adding-a-plugin-preview.md). It owns optionality, image count, design canvas, position, language variants, file location, and acceptance. The guide also owns the embedded-raster exception for SVG previews; small icons remain geometry-only. The icon dimensions below do not apply to previews. Inspect the real feature and preserve its produced assets.
## Establish the subject
@@ -0,0 +1,10 @@
/** Frame one text or tool response from the local Messages endpoint. */
export function messagesResponse(content: Record<string, unknown>, stopReason: 'end_turn' | 'max_tokens' | 'tool_use'): string {
return [
{ type: 'message_start', message: { id: 'sdk-smoke-response', model: 'deepseek-v4-pro', usage: { input_tokens: 3, output_tokens: 0 } } },
{ type: 'content_block_start', index: 0, content_block: content },
{ type: 'content_block_stop', index: 0 },
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
{ type: 'message_stop' },
].map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('')
}
@@ -0,0 +1,10 @@
/** Freeze only the isolated runtime's wall clock before reminder activation starts. */
export function apply(ctx) {
const original = Date.now
const now = Number(process.env.DSH_TEST_SCHEDULE_NOW)
ctx.effect(() => {
Date.now = () => now
return () => { Date.now = original }
})
ctx.provide('sdkTestClock', { ready: true })
}
@@ -10,6 +10,8 @@ import { execa } from 'execa'
import { describe, expect, it, onTestFinished } from 'vitest'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
import { messagesResponse } from './fixtures/messages-response.ts'
const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
const launch = resolveExampleLaunch({
srcBin: fileURLToPath(new URL('../../../src/bin.ts', import.meta.url)),
@@ -17,16 +19,6 @@ const launch = resolveExampleLaunch({
})
const decompress = promisify(zstdDecompress)
/** Frame one text or tool response from the local Messages endpoint. */
function messagesResponse(content: Record<string, unknown>, stopReason: 'end_turn' | 'max_tokens' | 'tool_use'): string {
return [
{ type: 'message_start', message: { id: 'sdk-smoke-response', model: 'deepseek-v4-pro', usage: { input_tokens: 3, output_tokens: 0 } } },
{ type: 'content_block_start', index: 0, content_block: content },
{ type: 'content_block_stop', index: 0 },
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
{ type: 'message_stop' },
].map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('')
}
function waitForLine(
lines: string[],
@@ -66,7 +58,11 @@ describe('Python SDK dsh profile keyless smoke', () => {
{ label: 'reports max-token turns with mapping enabled through env', envValue: 'true', editorEnabled: false },
{ label: 'reports max-token turns with mapping disabled through env', envValue: 'false', editorEnabled: false },
{ label: 'allows an explicit patch to enable str_replace_editor', envValue: undefined, editorEnabled: true },
])('$label', async ({ envValue, editorEnabled }) => {
{ label: 'uses desktop tools through the PTC environment switch', envValue: undefined, editorEnabled: false, ptc: true },
])('$label', async (scenario) => {
const { envValue, editorEnabled } = scenario
const ptc = 'ptc' in scenario && scenario.ptc
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-smoke-'))
const editorPatch = join(root, 'editor.patch.yml')
if (editorEnabled) await writeFile(editorPatch, [
@@ -103,6 +99,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
DSH_HOME: join(root, '.dsh'),
DSH_PERMISSION_MODE: 'danger-full-access',
DSH_TELEMETRY_DISABLED: '1',
DSH_TOOLS_MODE: ptc ? 'ptc' : 'native',
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }),
@@ -176,9 +173,23 @@ describe('Python SDK dsh profile keyless smoke', () => {
const toolNames = tools.map(tool => tool.name)
expect(modelRequests[0]?.output_config).toEqual({ effort: 'max' })
expect(modelRequests[0]?.max_tokens).toBe(1234)
expect(toolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch', 'web_search']))
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
expect(toolNames).not.toContain('list_subagent_models')
const expectedTools = ['read', 'write', 'edit', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
'present', 'ask_user_question', 'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update']
const requestText = JSON.stringify(modelRequests[0])
if (ptc) {
expect(toolNames).toEqual(['run_code'])
for (const tool of expectedTools) expect(requestText.includes(`${tool}:`), tool).toBe(true)
} else {
expect(toolNames).toEqual(expect.arrayContaining(expectedTools))
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
expect(toolNames).not.toContain('list_subagent_models')
}
for (const text of ['Tokens prefixed with @', 'Prefer showing the primary results',
'Time sampled while preparing']) {
expect(requestText.includes(text), text).toBe(true)
}
expect(requestText).not.toContain('Current DeepSeek Harness implementation checkout:')
expect(requestText).not.toContain('Current DeepSeek Harness Web GUI URL:')
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr)
@@ -0,0 +1,72 @@
/** Reminder activation must share SDK ownership and initialized model limits after process restart. */
import { createServer } from 'node:http'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
import { expect, it, onTestFinished, vi } from 'vitest'
import { messagesResponse } from './fixtures/messages-response.ts'
it('delivers an overdue reminder with the SDK route and lets the client resume that same Session', async () => {
const root = await mkdtemp(join(tmpdir(), 'sdk-schedule-resume-'))
const patch = join(root, 'clock.patch.yml')
const fixture = fileURLToPath(new URL('./fixtures/schedule-clock.mjs', import.meta.url))
await writeFile(patch, [
'- insert:', ' - id: sdk-test-clock', ` name: ${JSON.stringify(fixture)}`,
'- id: sdk-jsonrpc-server', ' inject: [sdkAppStartup, loader, sdkTestClock]', '',
].join('\n'))
const requests: Record<string, unknown>[] = []
const restored: Record<string, unknown>[] = []
let restarting = false
const server = createServer((incoming, response) => {
let body = ''
incoming.setEncoding('utf8')
incoming.on('data', (chunk: string) => { body += chunk })
incoming.on('end', () => {
const request = JSON.parse(body) as Record<string, unknown>
requests.push(request)
if (restarting) restored.push(request)
const content = requests.length === 1 ? {
type: 'tool_use', id: 'create-reminder', name: 'schedule_create',
input: { title: 'Check SDK', prompt: 'Report the SDK reminder', after_seconds: 86400 },
} : { type: 'text', text: 'ready' }
response.writeHead(200, { 'content-type': 'text/event-stream' })
response.end(messagesResponse(content, requests.length === 1 ? 'tool_use' : 'end_turn'))
})
})
const clients: DeepSeekHarness[] = []
onTestFinished(async () => {
try { await Promise.all(clients.map(client => client.close())) } finally {
try {
await new Promise<void>((resolve, reject) => {
server.close((error) => { if (error === undefined) resolve(); else reject(error) })
})
} finally { await rm(root, { recursive: true, force: true }) }
}
})
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('model fixture has no port')
const env = { ...process.env, DSH_TOOLS_MODE: 'native', DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'keyless-schedule-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
clearAmbientProxyEnv(env)
const first = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
model: 'deepseek-v4-pro', maxTokens: 5678, initializeTimeoutMs: 30_000,
env: { ...env, DSH_TEST_SCHEDULE_NOW: String(Date.parse('2030-01-01T00:00:00Z')) } })
clients.push(first)
expect((await first.session('saved').run('Set a reminder')).finalResponse).toBe('ready')
expect(requests).toHaveLength(2)
await first.close()
restarting = true
const second = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
model: 'deepseek-v4-flash', maxTokens: 1234, initializeTimeoutMs: 30_000,
env: { ...env, DSH_TEST_SCHEDULE_NOW: String(Date.parse('2030-01-03T00:00:00Z')) } })
clients.push(second)
await second.start()
await vi.waitFor(() => { expect(restored).toHaveLength(1) }, { timeout: 30_000 })
expect(restored[0]).toMatchObject({ model: 'deepseek-v4-flash', max_tokens: 1234 })
const session = await second.resumeSession('saved')
expect((await session.run('Continue after the reminder')).finalResponse).toBe('ready')
})
@@ -0,0 +1,84 @@
/** Keyless SDK questions through the shipped profile, native tools, and both tool-call modes. */
import { createServer } from 'node:http'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { DeepSeekHarness, type SdkUserQuestionRequest } from '@deepseek-ai/dsh-sdk-client'
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
import { expect, it, onTestFinished } from 'vitest'
import { messagesResponse } from './fixtures/messages-response.ts'
it.each([
{ mode: 'native', cancel: false }, { mode: 'ptc', cancel: false }, { mode: 'native', cancel: true },
])('handles a built-in question through $mode (cancel: $cancel)', async ({ mode, cancel }) => {
const root = await mkdtemp(join(tmpdir(), 'sdk-question-profile-'))
const requests: Record<string, unknown>[] = []
const questions = [{ id: 'format', question: 'Which format?', options: [{ label: 'CSV' }, { label: 'JSON' }] }]
const server = createServer((incoming, response) => {
let body = ''
incoming.setEncoding('utf8')
incoming.on('data', (chunk: string) => { body += chunk })
incoming.on('end', () => {
requests.push(JSON.parse(body) as Record<string, unknown>)
const tool = mode === 'ptc' ? {
type: 'tool_use', id: 'question-call', name: 'run_code', input: {
code: `return await tools.ask_user_question(${JSON.stringify({ questions })});`, description: 'Ask which output format to use',
},
} : { type: 'tool_use', id: 'question-call', name: 'ask_user_question', input: { questions } }
response.writeHead(200, { 'content-type': 'text/event-stream' })
response.end(messagesResponse(requests.length === 1 ? tool : { type: 'text', text: 'answer received' },
requests.length === 1 ? 'tool_use' : 'end_turn'))
})
})
const state: { harness?: DeepSeekHarness } = {}
onTestFinished(async () => {
try { await state.harness?.close() } finally {
try {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error === undefined) resolve()
else reject(error)
})
})
}
finally { await rm(root, { recursive: true, force: true }) }
}
})
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('model fixture has no port')
const env = { ...process.env, DSH_TOOLS_MODE: mode, DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'keyless-question-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
clearAmbientProxyEnv(env)
const seen: SdkUserQuestionRequest[] = []
const entered = Promise.withResolvers<AbortSignal>()
const harness = state.harness = new DeepSeekHarness({
cwd: root, dshHome: join(root, '.dsh'), env, model: 'deepseek-v4-pro', maxTokens: 1234, initializeTimeoutMs: 30_000,
onUserQuestion: async (request, { signal }) => {
expect(signal.aborted).toBe(false)
seen.push(request)
entered.resolve(signal)
if (cancel) return new Promise((_resolve, reject) => {
signal.addEventListener('abort', () => { reject(new Error('question cancelled')) }, { once: true })
})
return { answers: [{ id: 'format', selected: ['CSV'] }] }
},
})
const pending = harness.session('question-session').run('Ask which output format to use')
if (cancel) {
const settled = pending.catch(() => undefined)
const signal = await entered.promise
await harness.close()
await settled
expect(signal.aborted).toBe(true)
expect(requests).toHaveLength(1)
return
}
const result = await pending
expect(seen).toEqual([{ requestId: expect.any(String) as string, sessionId: 'question-session', questions }])
expect(result.finalResponse).toBe('answer received')
expect(requests).toHaveLength(2)
expect(requests[1]?.max_tokens).toBe(1234)
expect(JSON.stringify(requests[1]?.messages)).toContain('CSV')
expect(result.events.some(event => event.type === 'tool/result' && event.data.error !== undefined)).toBe(false)
})
@@ -5,54 +5,9 @@
- paragraph:
- code: "@deepseek-ai/dsh-experimental-badge-skill-bundle"
- paragraph: 让 Agent 为文档、PR 等产出添加「powered by dsh」徽章。
- img "DSH 徽章技能功能预览"
- region "详细介绍":
- heading "@deepseek-ai/dsh-experimental-badge-skill-bundle" [level=1]
- paragraph: English | 中文
- heading "Summary" [level=2]
- paragraph: Let Agents whose presets load skills add a “powered by dsh” badge to documents, pull requests, and other outputs. The shared provider adds no model tool. It ships switched off in Web and Desktop. Select it in Plugins to enable it.
- heading "Table of Contents" [level=2]
- list:
- listitem: Use this package
- listitem: Understand the implementation
- listitem: Further Exploration
- listitem: Model Experience
- listitem: Known Limitations and Deferred Work
- listitem: Dev Note
- separator
- paragraph
- heading "Use this package" [level=2]
- paragraph:
- text: In Web or Desktop, open Plugins and enable
- strong: DSH badge skill
- text: in Official. Switch it off to remove its profile layer.
- paragraph: The switch adds or removes a Host-wide skill provider for existing and new Agents. Presets that expose skill loading can use the badge; minimal keeps its tool catalog unchanged.
- separator
- paragraph
- heading "Understand the implementation" [level=2]
- paragraph:
- code: cordis.patch.yml
- text: contributes the
- code: skill-badge
- text: provider row on the Host. Later root patches can disable that row; an override alone does not select the bundle. The profile composer owns patch ordering and errors.
- separator
- paragraph
- heading "Further Exploration" [level=2]
- paragraph: Capability implementation
- separator
- paragraph
- heading "Model Experience" [level=2]
- paragraph: Indirectly, through the capability implementation, which owns model context and results.
- heading "KV Cache effect" [level=4]
- paragraph: The layer adds no request content directly; the capability implementation owns cache effects from tools, prompts, and results.
- heading "Known Limitations and Deferred Work" [level=2]
- paragraph
- list:
- listitem: A preset must include skill discovery and loading to use the badge. The minimal preset does not gain skill tools from this bundle.
- separator
- paragraph
- heading "Dev Note" [level=3]
- paragraph: None.
- img "DSH 徽章技能功能预览"
- heading "包含的组件" [level=4]
- text: 共 1 个 · 1 已停用
- list:
@@ -1 +1 @@
Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
+6 -6
View File
@@ -690,13 +690,13 @@ describe('web e2e: plugin manager', () => {
expect(tripwire.pageErrors).toEqual([])
})
it('shows an optional feature preview before activation and leaves other plugin details unchanged', async () => {
it('shows a bundle detail document image before activation and leaves other plugin details unchanged', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-preview'))
const panel = await openPluginsPanel()
await panel.getByRole('button', { name: '更多', exact: true }).click()
await panel.getByRole('button', { name: '查看 DSH 徽章技能', exact: true }).click()
const detail = panel.locator('[data-plugin-detail]')
const preview = detail.getByRole('img', { name: 'DSH 徽章技能功能预览', exact: true })
const preview = detail.locator('[data-detail-document]').getByRole('img', { name: 'DSH 徽章技能功能预览', exact: true })
await preview.waitFor()
await preview.evaluate(async (image: HTMLImageElement) => { await image.decode() })
expect(await preview.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(640)
@@ -709,8 +709,6 @@ describe('web e2e: plugin manager', () => {
expect(fits).toBe(true)
await detail.getByText(`v${badgeVersion}`, { exact: true }).waitFor()
await detail.getByText(badgeVersion, { exact: true }).waitFor()
// The bundle's README is its detail document, shown between its components and its source.
await detail.locator('[data-detail-document] h1').waitFor()
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'preview.expected.md'),
await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd, {
replacements: [[badgeVersion, '{{version}}']],
@@ -718,7 +716,8 @@ describe('web e2e: plugin manager', () => {
const chineseSource = await preview.getAttribute('src')
try {
await setLanguage('en')
const englishPreview = detail.getByRole('img', { name: 'DSH badge skill feature preview', exact: true })
const englishPreview = detail.locator('[data-detail-document]')
.getByRole('img', { name: 'DSH badge skill feature preview', exact: true })
await englishPreview.waitFor()
await englishPreview.evaluate(async (image: HTMLImageElement) => { await image.decode() })
expect(await englishPreview.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(640)
@@ -729,7 +728,8 @@ describe('web e2e: plugin manager', () => {
expect(await preview.getAttribute('src')).toBe(chineseSource)
await panel.getByRole('button', { name: '返回 实验性插件', exact: true }).click()
await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click()
// Detail icons have alt=""; only feature previews expose an image role.
// Detail icons have alt=""; this bundle's README detail document has no images.
await panel.locator('[data-detail-document] h1').waitFor()
expect(await panel.getByRole('img').count()).toBe(0)
await panel.getByRole('button', { name: '返回 实验性插件', exact: true }).click()
expect(tripwire.pageErrors).toEqual([])
+6 -4
View File
@@ -12,6 +12,8 @@ import { newEnglishPage } from './support.ts'
const bundle = fileURLToPath(new URL('../../../packages/experimental/voice-input-bundle', import.meta.url))
const expected = fileURLToPath(new URL('./expected/voice-download.expected.md', import.meta.url))
// The input-device picker on the same page shows its own alert when headless Chromium offers no microphone.
const DOWNLOAD_ALERT = '[role="alert"]:has-text("Download source")'
it('shows the failed asset, actual download source and recovery advice, then retries on request', async () => {
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
@@ -45,14 +47,14 @@ it('shows the failed asset, actual download source and recovery advice, then ret
await page.getByRole('button', { name: 'Plugins', exact: true }).click()
await page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]').getByRole('button').click()
await page.getByRole('button', { name: 'Download and prepare', exact: true }).click()
const alert = page.getByRole('alert')
const alert = page.getByRole('alert').filter({ hasText: 'Download source' })
await alert.waitFor()
expect(await alert.textContent()).toContain('HTTP 503')
expect(await alert.textContent()).toContain(`Download source: ${origin}`)
expect(requests).toHaveLength(1)
expect(requests[0]).toContain('/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07/model.int8.onnx')
await compareOrRefreshGolden(expected,
(await captureStableAria(page, '[role="alert"]', scaffold.workspaceCwd)).replaceAll(origin, 'https://model-mirror.example'), webSnapshotMode())
(await captureStableAria(page, DOWNLOAD_ALERT, scaffold.workspaceCwd)).replaceAll(origin, 'https://model-mirror.example'), webSnapshotMode())
status = 404
await page.getByRole('button', { name: 'Retry preparation', exact: true }).click()
await expect.poll(() => alert.textContent()).toContain('HTTP 404')
@@ -108,7 +110,7 @@ it.each([{ winner: 0, manual: false }, { winner: 1, manual: false }, { winner: 0
await picker.waitFor()
if (manual) await picker.selectOption(origins[winner]!)
await page.getByRole('button', { name: 'Download and prepare', exact: true }).click()
const alert = page.getByRole('alert')
const alert = page.getByRole('alert').filter({ hasText: 'Download source' })
await alert.waitFor()
expect(downloads).toEqual(manual ? [winner] : [winner, 1 - winner])
expect(requestedPaths).toHaveLength(manual ? 1 : 4)
@@ -118,7 +120,7 @@ it.each([{ winner: 0, manual: false }, { winner: 1, manual: false }, { winner: 0
expect(await alert.textContent()).toContain(manual ? 'HTTP 503' : 'HTTP 502')
expect(await alert.textContent()).toContain(`Download source: ${failedOrigin}`)
await compareOrRefreshGolden(manual ? expected : fileURLToPath(new URL('./expected/voice-download-fallback.expected.md', import.meta.url)),
(await captureStableAria(page, '[role="alert"]', scaffold.workspaceCwd)).replaceAll(failedOrigin, manual ? 'https://model-mirror.example' : 'https://fallback-model-source.example'),
(await captureStableAria(page, DOWNLOAD_ALERT, scaffold.workspaceCwd)).replaceAll(failedOrigin, manual ? 'https://model-mirror.example' : 'https://fallback-model-source.example'),
webSnapshotMode())
expect(tripwire.pageErrors).toEqual([])
})
+4 -3
View File
@@ -2799,8 +2799,8 @@ export interface Config {
## `@deepseek-ai/dsh-schedule`
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionController` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:72`](../packages/schedule/schedule/src/index.ts)
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:83`](../packages/schedule/schedule/src/index.ts)
```ts config-catalog
/** Configuration for the Host Schedule domain. */
@@ -4714,7 +4714,7 @@ 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-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` | `systemPrompt` · `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-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) |
| `@deepseek-ai/dsh-client-ui-goal` | — | [`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts) |
@@ -4764,6 +4764,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) |
| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) |
| `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) |
| `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) |
| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) |
| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) |
| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) |
+4 -3
View File
@@ -2801,8 +2801,8 @@ export interface Config {
## `@deepseek-ai/dsh-schedule`
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionController` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:72`](../packages/schedule/schedule/src/index.ts)
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:83`](../packages/schedule/schedule/src/index.ts)
```ts config-catalog
/** Configuration for the Host Schedule domain. */
@@ -4716,7 +4716,7 @@ 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-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` | `systemPrompt` · `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-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) |
| `@deepseek-ai/dsh-client-ui-goal` | — | [`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts) |
@@ -4766,6 +4766,7 @@ export interface Config {
| `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) |
| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) |
| `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) |
| `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) |
| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) |
| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) |
| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) |
+2 -2
View File
@@ -21,8 +21,8 @@
en: 0894bd9f35430753
zh: 2d28090e71134edd
/cookbook-adding-a-workspace-package/5-add-optional-plugin-display-metadata:
en: 68b463cd3d2e5fb0
zh: 80ea797c5770c573
en: ed501a72ab8ba6e7
zh: 8ad775b735887a10
/cookbook-adding-a-workspace-package/6-verify:
en: 80c7fb90b1175aae
zh: 2ba37962dbc57299
+1 -1
View File
@@ -147,7 +147,7 @@ Export `<package name>/locale/en.json` for locale lookup; expose `<package name>
For an image on bundle cards, details, and component rows, a package-root plugin sets top-level `"icon": "./icon.svg"` in its exported manifest, or exports `./icon` when that field is omitted. A subpath plugin exports its own icon, such as `"./search/icon": "./search.svg"`. Include each image in `files`. A manifest `icon` is relative to the manifest's directory; absolute paths, URLs, paths outside that directory, and symlinks resolving outside it are rejected. An exported icon resolves through Node exports like locale resources and must remain inside its package. SVG, PNG, JPEG (`.jpg`/`.jpeg`), and WebP files are supported up to 256 KiB. Images must be self-contained; SVG is rendered as an image, not inline HTML. The Host returns a data URL without activating the plugin. Invalid declarations or unreadable files produce a metadata diagnostic while retaining valid text, without trying the export; missing or undecodable images use the panel's default artwork.
A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; include the file and its images in `files`. A bundle's page in **Plugins**, and its market entry page when the [custom plugin markets bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and a market entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file.
A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; keep its images beside it, and list its top-level directory, such as `docs`, in `files` right after the locale resources, as `check-workspace-constraints` requires. The named file must exist in a subdirectory of the package; a root-level document is rejected because its neighboring images would not be published. A bundle's page in **Plugins**, and its market entry page when the [custom plugin markets bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and a market entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file.
Installed bundle cards and details, component lists and configuration details, and Settings' plugin inventory display this metadata, including disabled and preset plugins. Reads do not activate plugins.
+1 -1
View File
@@ -147,7 +147,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
要在组合包卡片、详情和组件行显示图片,包根插件在导出清单顶层设置 `"icon": "./icon.svg"`,或在省略该字段时导出 `./icon`。子路径插件导出自己的图标,例如 `"./search/icon": "./search.svg"`。将每张图片加入 `files`。清单 `icon` 相对于清单所在目录;绝对路径、URL、目录外路径,以及解析到目录外的符号链接均被拒绝。导出的图标像 locale 资源一样经 Node exports 解析,且必须位于所属包内。支持不超过 256 KiB 的 SVG、PNG、JPEG(`.jpg`/`.jpeg`)和 WebP 文件。图片必须自包含;SVG 作为图片渲染,不作为内联 HTML。Host 返回 data URL,不激活插件。声明无效或文件不可读时,显示元信息诊断并保留有效文本,不再尝试导出的图标;图片缺失或无法解码时使用面板的默认插画。
组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;将该文件及其图片加入 `files`。组合包在**插件**中的页面,以及启用[自定义插件市场组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其市场条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;市场条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。
组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;图片放在该文件旁边,并按 `check-workspace-constraints` 的要求,在 `files` 中紧接语言资源之后列出该文件的顶层目录,例如 `docs`。该文件必须位于包的子目录中且已存在;包根目录下的文档会被拒绝,因为其旁边的图片不会被发布。组合包在**插件**中的页面,以及启用[自定义插件市场组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其市场条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;市场条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。
已安装 bundle 的卡片和详情、组件列表与配置详情、设置中的插件清单都展示这些元信息,包括禁用插件和预设内插件。读取时不激活插件。
+18 -18
View File
@@ -2,21 +2,21 @@
# 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/cookbook/adding-a-plugin-preview.md
/cookbook-add-a-built-in-plugin-feature-preview:
en: 1038debc95283b43
zh: a41eae3d9203c400
/cookbook-add-a-built-in-plugin-feature-preview/1-decide-whether-an-image-helps:
en: d2b84c3a4d590f43
zh: b064b42f148bd323
/cookbook-add-a-built-in-plugin-feature-preview/2-draw-one-fixed-size-svg:
en: 1abb5284f499ae7e
zh: b4dacd16d8f04728
/cookbook-add-a-built-in-plugin-feature-preview/3-register-the-internal-pair:
en: e79355a7184d82d3
zh: 92a0e188dee2717c
/cookbook-add-a-built-in-plugin-feature-preview/4-keep-the-page-contract:
en: 095cb97e16c2232e
zh: 2ed9b60710ed522c
/cookbook-add-a-built-in-plugin-feature-preview/5-validate-and-review:
en: 053b2105cc00b8d0
zh: d06467ded2ccac42
/cookbook-add-a-plugin-feature-preview:
en: c37096cd3cdb886c
zh: fc6850d68c638898
/cookbook-add-a-plugin-feature-preview/1-decide-whether-an-image-helps:
en: ee84f52d834cbccf
zh: 7bf3a425a231b5e6
/cookbook-add-a-plugin-feature-preview/2-draw-one-fixed-size-svg:
en: 3a28640aa605dce8
zh: c5336958f92b5a70
/cookbook-add-a-plugin-feature-preview/3-put-it-at-the-top-of-the-detail-document:
en: 17e2ace32a89c419
zh: c635a890e86a41ec
/cookbook-add-a-plugin-feature-preview/4-page-behavior:
en: 20ce358cb110104c
zh: abb8d72fed81f3c8
/cookbook-add-a-plugin-feature-preview/5-validate-and-review:
en: 2888c02bac267662
zh: 5b0e95611c8d5d94
+18 -25
View File
@@ -1,52 +1,45 @@
# Cookbook: Add a built-in plugin feature preview
# Cookbook: Add a plugin feature preview
English | [中文](adding-a-plugin-preview.zh.md)
Plugin Manager owns optional feature illustrations for built-in bundles. Follow this guide when editing its internal `BUNDLE_PREVIEWS` map. Packages cannot declare illustrations through their manifest or exports; third-party plugins have no preview interface.
A feature preview is an optional illustration at the top of a bundle's detail document, the long description that its page in **Plugins** shows first. Follow this guide when drawing one for an official bundle. The detail document itself is declared through [`meta.detail`](adding-a-package.md#plugin-display-metadata).
## 1. Decide whether an image helps
Show one concrete result or recognizable capability. Omit the map entry when the description is enough: no image is required, and omission reserves no space. Essential instructions stay in localized text or configuration outside the illustration.
Show one concrete result or recognizable capability. Omit the image when the description is enough: no image is required. Essential instructions stay in the document's text or in configuration outside the illustration.
## 2. Draw one fixed-size SVG
| Item | Requirement |
|---|---|
| Count | Zero or one logical feature image per bundle. English and Chinese files are alternatives for that image, not a gallery or carousel. |
| Format | SVG only: `width="640" height="300" viewBox="0 0 640 300"` (32:15). Raster preview files and other dimensions are unsupported. |
| Languages | Supply both `en` and `zh`. Translate headings, annotations, and example content; preserve official brand assets and names. No single strings, arrays, or extra languages. |
| Count | Zero or one feature image per bundle. English and Chinese files are alternatives for that image, not a gallery or carousel. |
| Format | SVG: `width="640" height="300" viewBox="0 0 640 300"` (32:15). |
| Languages | Supply both an English and a Chinese file. Translate headings, annotations, and example content; preserve official brand assets and names. |
| Composition | One main example, optionally with one close-up. Keep important content about 32 canvas units from the edges. Review text at the page's actual size. |
| Typography | Outline lettering into paths using a suitable font. Match the feature and neighboring UI; do not rely on browser font substitution. |
| Theme | Compare light and dark themes. One illustration serves both; avoid transparent dark text that disappears on dark backgrounds. |
The SVG is self-contained: no fonts, remote resources, scripts, handlers, animation, stylesheets, `text`, or `foreignObject`. An `image` element may embed an unchanged official PNG/JPEG/WebP brand asset as a base64 data URL. Small plugin icons remain geometry-only. This exception preserves brand artwork and does not permit raster preview files. Validation checks the data-URL envelope, not decoded pixels; the page renders the SVG through `<img>`. Both raw language files together must stay within 1 MiB. They are embedded as base64 data URLs in the Plugin Manager client bundle, so keep them small.
The SVG is self-contained: no fonts, remote resources, scripts, handlers, animation, stylesheets, `text`, or `foreignObject`. An `image` element may embed an unchanged official PNG/JPEG/WebP brand asset as a base64 data URL. Small plugin icons remain geometry-only. The page renders the SVG through `<img>`. Keep each file small; the Host refuses images above plugin-manager's `detailImageMaxBytes`, 5 MiB by default.
## 3. Register the internal pair
## 3. Put it at the top of the detail document
Put `<feature>-preview.svg` and `<feature>-preview.zh.svg` under [Plugin Manager assets](../../packages/client/ui-plugin-manager/src/client/assets/). Import both into [PluginManagerPage.tsx](../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx), then add one exact built-in package name to `BUNDLE_PREVIEWS`:
Save the pair as `docs/preview.svg` and `docs/preview.zh.svg` in the bundle package, beside `docs/DETAIL.md` and `docs/DETAIL.zh.md`, and make the image the first line of each document:
```ts ignore-check
import examplePreview from './assets/example-preview.svg'
import examplePreviewZh from './assets/example-preview.zh.svg'
// One entry in the existing Map<string, Readonly<Record<'en' | 'zh', string>>>.
['@deepseek-ai/dsh-example-bundle', { en: examplePreview, zh: examplePreviewZh }]
```markdown
![Example feature preview](preview.svg)
```
Keep the map's required `en` and `zh` type. Do not add package metadata, a Host read method, exported preview resources, or a third-party declaration path. Use the [badge skill](../../packages/client/ui-plugin-manager/src/client/assets/badge-skill-preview.svg) and [voice input](../../packages/client/ui-plugin-manager/src/client/assets/voice-input-preview.svg) illustrations as examples. The voice transcript is explanatory sample content, not a recording result.
The Chinese document references `preview.zh.svg` with Chinese alternative text. Use the [badge skill](../../packages/experimental/badge-skill-bundle/docs/DETAIL.md) and [voice input](../../packages/experimental/voice-input-bundle/docs/DETAIL.md) documents as examples. The voice transcript is explanatory sample content, not a recording result.
## 4. Keep the page contract
## 4. Page behavior
| Item | Current behavior |
|---|---|
| Position | Below the description and status notices, above components and source information; aligned with the description. |
| Spacing | 24 px above the image; 32 px before the following details section. |
| Display | One `<img>`, natural 640×300 canvas, at most the available width and 320 px high; proportional scaling. The page supplies border and 16 px corners. |
| Language | The current UI language selects one of the required files. Alternative text uses the localized plugin name. |
| Availability | Visible before activation. Decode failure shows a localized notice without blocking controls. Lists and component rows retain their small icons. |
| Position | First in the detail document, which is the first section of the bundle page, below the description and status notices. |
| Display | The detail document renders the image at most its own width, inside its height-bounded scroll area. |
| Language | The document for the UI language references its own file. |
| Availability | Visible before activation. Lists and component rows keep their small icons. |
## 5. Validate and review
Run `pnpm exec vitest run scripts/builtin-plugin-preview.spec.ts packages/client/ui-plugin-manager/tests/components.client.spec.tsx scripts/client-bundle-css.spec.ts`, then `pnpm run build`. The asset test checks every registered pair, fixed SVG dimensions, self-contained content, and combined size. Type checking requires both languages. These checks enforce format; visual quality and equivalent meaning in both languages still require review.
Open the real plugin details in English and Chinese before activation. Decode the image and confirm 640×300 natural dimensions, placement, light/dark readability, and no horizontal overflow at 390 px. Also check a built-in without artwork and a third-party plugin. Follow [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) for PR evidence; record from the final clean commit and label the demonstrated flow accurately.
`scripts/plugin-preview-assets.spec.ts` checks every preview pair in a bundle's `docs` directory: the fixed canvas, absent scripts, stylesheets, text elements, and external references, the 1 MiB pair limit, and English and Chinese files that differ. Then open the real plugin details in English and Chinese before activation and confirm the image loads, its placement, light/dark readability, and no horizontal overflow at 390 px; review owns visual quality and equivalent meaning in both languages. Follow [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) for PR evidence; record from the final clean commit and label the demonstrated flow accurately.
+18 -25
View File
@@ -1,52 +1,45 @@
# Cookbook: 添加内置插件功能配图
# Cookbook: 添加插件功能配图
[English](adding-a-plugin-preview.md) | 中文
插件管理拥有内置组合包的可选功能配图。编辑其内部 `BUNDLE_PREVIEWS` 映射时遵循本指引。包不能通过 manifest 或 exports 声明配图;第三方插件没有配图接口。
功能配图是位于组合包详细介绍文档顶部的可选插图;详细介绍文档是该组合包在**插件**中的页面最先显示的长描述。为官方组合包绘制配图时遵循本指引。详细介绍文档本身通过 [`meta.detail`](adding-a-package.zh.md#plugin-display-metadata) 声明。
## 1. 判断是否需要配图
展示一个具体结果或容易辨认的能力。文字说明足够时不添加映射项:配图不是必需的,省略时不占空间。必要操作说明仍放在图外的本地化文字或配置中。
展示一个具体结果或容易辨认的能力。文字说明足够时省略配图:配图不是必需的。必要操作说明仍放在文档文字或图外的配置中。
## 2. 绘制一张固定尺寸 SVG
| 项目 | 要求 |
|---|---|
| 数量 | 每个组合包零张或一张逻辑配图。英文和中文文件是同一张图的替代版本,不是图库或轮播。 |
| 格式 | 仅 SVG:`width="640" height="300" viewBox="0 0 640 300"`(32:15)。不支持位图配图文件或其他尺寸。 |
| 语言 | 同时提供 `en` 和 `zh`。翻译标题、标注和示例内容,保留官方品牌素材和名称。不使用单个字符串、数组或额外语言。 |
| 数量 | 每个组合包零张或一张配图。英文和中文文件是同一张图的替代版本,不是图库或轮播。 |
| 格式 | SVG:`width="640" height="300" viewBox="0 0 640 300"`(32:15)。 |
| 语言 | 同时提供英文和中文文件。翻译标题、标注和示例内容,保留官方品牌素材和名称。 |
| 构图 | 一个主体示例,可选一个局部特写。重要内容距边缘约 32 个画布单位;按页面实际显示尺寸检查文字。 |
| 字体 | 选择合适字体并把文字转成路径,匹配功能及相邻界面,不依赖浏览器字体替换。 |
| 主题 | 对比浅色和深色主题。同一张图服务两种主题,避免透明底深色文字在深色背景中消失。 |
SVG 必须自包含:不使用字体、远程资源、脚本、事件处理器、动画、样式表、`text` 或 `foreignObject`。`image` 元素可以通过 base64 data URL 嵌入未改动的官方 PNG/JPEG/WebP 品牌素材。小插件图标仍仅使用几何图形。此例外用于保留品牌素材,不允许直接使用位图配图文件。校验检查 data URL 结构,不解码像素;页面通过 `<img>` 渲染 SVG。两种语言的原始文件合计不超过 1 MiB。图片会以 base64 data URL 嵌入插件管理 client bundle,因此应控制大小。
SVG 必须自包含:不使用字体、远程资源、脚本、事件处理器、动画、样式表、`text` 或 `foreignObject`。`image` 元素可以通过 base64 data URL 嵌入未改动的官方 PNG/JPEG/WebP 品牌素材。小插件图标仍仅使用几何图形。页面通过 `<img>` 渲染 SVG。每个文件都应控制大小;Host 会拒绝超过 plugin-manager 的 `detailImageMaxBytes`(默认 5 MiB)的图片。
## 3. 注册内部双语配图
## 3. 放在详细介绍文档顶部
将 `<feature>-preview.svg` 和 `<feature>-preview.zh.svg` 放在[插件管理素材目录](../../packages/client/ui-plugin-manager/src/client/assets/)下。在 [PluginManagerPage.tsx](../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx) 中导入两者,再向 `BUNDLE_PREVIEWS` 添加一个精确的内置包名:
将这对文件保存为组合包中的 `docs/preview.svg` 和 `docs/preview.zh.svg`,与 `docs/DETAIL.md` 和 `docs/DETAIL.zh.md` 放在一起,并把图片作为每份文档的第一行:
```ts ignore-check
import examplePreview from './assets/example-preview.svg'
import examplePreviewZh from './assets/example-preview.zh.svg'
// One entry in the existing Map<string, Readonly<Record<'en' | 'zh', string>>>.
['@deepseek-ai/dsh-example-bundle', { en: examplePreview, zh: examplePreviewZh }]
```markdown
![Example feature preview](preview.svg)
```
保留映射中必需的 `en` 和 `zh` 类型。不添加包元数据、Host 读取方法、导出的配图资源或第三方声明入口。可参考[徽章技能](../../packages/client/ui-plugin-manager/src/client/assets/badge-skill-preview.svg)和[语音输入](../../packages/client/ui-plugin-manager/src/client/assets/voice-input-preview.svg)配图。语音图中的转写文字是说明性示例,不是录音结果。
中文文档引用 `preview.zh.svg`,并使用中文替代文字。可参考[徽章技能](../../packages/experimental/badge-skill-bundle/docs/DETAIL.md)和[语音输入](../../packages/experimental/voice-input-bundle/docs/DETAIL.md)文档。语音图中的转写文字是说明性示例,不是录音结果。
## 4. 保持页面契约
## 4. 页面行为
| 项目 | 当前行为 |
|---|---|
| 位置 | 在描述和状态提示下方、组件与来源信息上方,与说明左边缘对齐。 |
| 间距 | 图片上方 24 px,后续详情区前 32 px。 |
| 显示 | 一个 `<img>`,原始画布 640×300,最大宽度为可用宽度,最大高度 320 px,等比缩放;页面提供边框及 16 px 圆角。 |
| 语言 | 当前 UI 语言选择必需的两份文件之一;替代文字使用本地化插件名。 |
| 可用性 | 启用前可见;解码失败显示本地化提示,不阻塞操作;列表和组件行保留小图标。 |
| 位置 | 位于详细介绍文档开头;该文档是组合包页面的第一个区域,在描述和状态提示下方。 |
| 显示 | 详细介绍文档以不超过文档自身宽度的尺寸渲染图片,图片位于有高度上限的滚动区域内。 |
| 语言 | 当前 UI 语言对应的文档引用其自己的文件。 |
| 可用性 | 启用前可见;列表和组件行保留小图标。 |
## 5. 校验与验收
运行 `pnpm exec vitest run scripts/builtin-plugin-preview.spec.ts packages/client/ui-plugin-manager/tests/components.client.spec.tsx scripts/client-bundle-css.spec.ts`,再运行 `pnpm run build`。素材测试检查每个注册条目的双语文件、固定 SVG 尺寸、自包含内容和合计大小;类型检查要求同时提供两种语言。这些检查约束格式;视觉质量及双语含义一致仍需人工审阅。
在真实插件详情页分别使用英文和中文,启用前解码图片,确认原始尺寸 640×300、插入位置、浅色及深色主题的可读性,以及 390 px 宽度下没有横向溢出。同时检查一个无配图的内置插件和一个第三方插件。按 [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) 制作 PR 证据,从最终干净提交录制,并准确说明演示范围。
`scripts/plugin-preview-assets.spec.ts` 检查组合包 `docs` 目录中的每一对配图:固定画布;不使用脚本、样式表、`text` 元素或外部引用;每对文件合计不超过 1 MiB;英文和中文文件不相同。然后在真实插件详情页分别使用英文和中文,启用前确认图片能加载,并检查插入位置、浅色及深色主题的可读性,以及 390 px 宽度下没有横向溢出;审阅负责检查视觉质量及双语含义是否一致。按 [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) 制作 PR 证据,从最终干净提交录制,并准确说明演示范围。
+2 -1
View File
@@ -62,6 +62,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:412`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
| `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:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`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:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `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) |
@@ -81,7 +82,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.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/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../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:39`](../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) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
+2 -1
View File
@@ -64,6 +64,7 @@
| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:412`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
| `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:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`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:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `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) |
@@ -83,7 +84,7 @@
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.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/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../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:39`](../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) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
+2 -2
View File
@@ -12,8 +12,8 @@
en: 08386e96eebf06d9
zh: c6d2b41199b0c466
/bilingual-documentation/scope-and-exclusions:
en: db70e758813d339b
zh: 3dcea283a08c47cb
en: eeda71b7ce01c121
zh: a6361361c1e3d578
/bilingual-documentation/division-of-labor:
en: 88097c97cb72f3ba
zh: 78a18000626cc818
+2
View File
@@ -47,6 +47,8 @@ The gate's limit, stated plainly: **a green gate means the pair was confirmed co
**Scope**: the root `CONTRIBUTING.md`, `BRAND_GUIDELINES.md`, and `SAFETY.md` documents, every non-vendor README, and every active document under `.agents/notes/**`, `docs/**`, and `python/**`. README matching is case-insensitive on the basename and covers future directories without another manifest edit. Dependency and ignored build-output trees and the frozen `.agents/notes/archived/` tree are discovery exclusions, not evolving translation source.
**Outside scope**: a bundle's detail documents under `packages/<group>/<package>/docs/`, which locale `meta.detail` names for the plugin page. The page shows the file for the active UI language, so these documents carry no in-document language switcher and no consistency record; [`verify-md-links`](../../scripts/verify-md-links.ts) and [`verify-md-wrap`](../../scripts/verify-md-wrap.ts) still check both languages, and review keeps them in step.
Generated English references and graphs participate in pairing when a reviewed Chinese counterpart is available. A generator puts its generated data in generated regions and writes each region into both pages, so regeneration never requires a manual Chinese update or a new record; only the prose outside the regions is translated and recorded. Freshness and pairing gates enforce their respective invariants independently.
Shared generated regions, such as the Cordis subsystem regions and the per-package regions of the [config catalog](../config-catalog.md), keep every generated byte identical across languages except localized paired-document paths; labels inside a region are code identifiers rather than translated words. A full-page generator can also render source-owned paired prose, such as [persistence-catalog-text.ts](../../scripts/persistence-catalog-text.ts), from the same structural data with matching Markdown structure and identical code blocks. It regenerates both pages and their consistency record together; review still verifies the paired prose's meaning and terminology.
+2
View File
@@ -49,6 +49,8 @@
**范围**:根目录 `CONTRIBUTING.md`、`BRAND_GUIDELINES.md` 与 `SAFETY.md` 文档、除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。
**范围外**:组合包 `packages/<group>/<package>/docs/` 下由 locale `meta.detail` 指定给插件页的详情文档。插件页只显示当前界面语言对应的文件,因此这些文档不带文档内的语言切换行,也没有一致性记录;[`verify-md-links`](../../scripts/verify-md-links.ts) 与 [`verify-md-wrap`](../../scripts/verify-md-wrap.ts) 仍会检查两种语言,两侧是否同步由评审把关。
有经评审的中文对侧的生成英文参考文档和图文档遵循配对规则。生成器把生成数据放在生成区域中,并把每个区域写入两种语言的页面,因此重新生成不需要手工更新中文,也不需要重新记录;只有区域外的说明文字需要翻译并计入记录。新鲜度门禁与配对门禁各自独立强制其约束。
共享生成区域(例如 Cordis 子系统区域和[配置目录](../config-catalog.zh.md)中每个包的区域)在两种语言间保持每个生成字节相同,仅配对文档路径按语言本地化;区域内的标签使用代码标识符而非译文。整页生成器也可以从相同的结构数据渲染源码维护的双语说明,例如 [persistence-catalog-text.ts](../../scripts/persistence-catalog-text.ts),并保持 Markdown 结构对应、代码块完全相同。它同时重新生成两页及其一致性记录;评审仍须验证双语说明的含义与术语。
+17 -11
View File
@@ -240,6 +240,7 @@ flowchart TD
pkg_deepseek_account_platform["deepseek-account-platform"]
end
subgraph group_deliverables["packages/deliverables"]
pkg_deliverable_file_references["deliverable-file-references"]
pkg_tool_present["tool-present"]
pkg_workspace_changes["workspace-changes"]
end
@@ -504,6 +505,7 @@ flowchart TD
pkg_attachment_local --> pkg_home_paths
pkg_hmr --> pkg_app_boot
pkg_hmr --> pkg_cmdline
pkg_deliverable_file_references --> pkg_system_prompt
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
@@ -1367,6 +1369,7 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
pkg_sdk_protocol --> pkg_user_questions
pkg_workflow_ptc --> pkg_agent
pkg_workflow_ptc --> pkg_llm
pkg_workflow_ptc --> pkg_ptc_runtime
@@ -1398,15 +1401,6 @@ flowchart TD
pkg_sdk_client --> pkg_llm
pkg_sdk_client --> pkg_sdk_protocol
pkg_sdk_client --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_subagent
pkg_sdk_jsonrpc_server --> pkg_working_directory
pkg_client_test_runtime --> pkg_api_gateway
pkg_client_test_runtime --> pkg_api_session_controller
pkg_client_test_runtime --> pkg_api_workspace_controller
@@ -1436,6 +1430,17 @@ flowchart TD
pkg_tool_schedule --> pkg_schedule
pkg_tool_schedule --> pkg_subagent
pkg_tool_schedule --> pkg_tools
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_schedule
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_subagent
pkg_sdk_jsonrpc_server --> pkg_user_questions
pkg_sdk_jsonrpc_server --> pkg_working_directory
```
<!-- BEGIN GENERATED module-graph:packages -->
@@ -1604,6 +1609,7 @@ flowchart TD
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
@@ -1780,13 +1786,13 @@ flowchart TD
| [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | `experimental` | [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`working-directory`](../packages/session/working-directory) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) |
<!-- END GENERATED module-graph:packages -->
+17 -11
View File
@@ -242,6 +242,7 @@ flowchart TD
pkg_deepseek_account_platform["deepseek-account-platform"]
end
subgraph group_deliverables["packages/deliverables"]
pkg_deliverable_file_references["deliverable-file-references"]
pkg_tool_present["tool-present"]
pkg_workspace_changes["workspace-changes"]
end
@@ -506,6 +507,7 @@ flowchart TD
pkg_attachment_local --> pkg_home_paths
pkg_hmr --> pkg_app_boot
pkg_hmr --> pkg_cmdline
pkg_deliverable_file_references --> pkg_system_prompt
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
@@ -1369,6 +1371,7 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
pkg_sdk_protocol --> pkg_user_questions
pkg_workflow_ptc --> pkg_agent
pkg_workflow_ptc --> pkg_llm
pkg_workflow_ptc --> pkg_ptc_runtime
@@ -1400,15 +1403,6 @@ flowchart TD
pkg_sdk_client --> pkg_llm
pkg_sdk_client --> pkg_sdk_protocol
pkg_sdk_client --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_subagent
pkg_sdk_jsonrpc_server --> pkg_working_directory
pkg_client_test_runtime --> pkg_api_gateway
pkg_client_test_runtime --> pkg_api_session_controller
pkg_client_test_runtime --> pkg_api_workspace_controller
@@ -1438,6 +1432,17 @@ flowchart TD
pkg_tool_schedule --> pkg_schedule
pkg_tool_schedule --> pkg_subagent
pkg_tool_schedule --> pkg_tools
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_schedule
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_subagent
pkg_sdk_jsonrpc_server --> pkg_user_questions
pkg_sdk_jsonrpc_server --> pkg_working_directory
```
<!-- BEGIN GENERATED module-graph:packages -->
@@ -1606,6 +1611,7 @@ flowchart TD
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
@@ -1782,13 +1788,13 @@ flowchart TD
| [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | `experimental` | [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`working-directory`](../packages/session/working-directory) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) |
<!-- END GENERATED module-graph:packages -->
+2 -2
View File
@@ -36,5 +36,5 @@
en: 077aa6573d9ede1a
zh: 0acea592a6e3630a
/host-wide-schedule/host-delivery:
en: b6d522d24b9b97ba
zh: 84df8ea09bedf51c
en: d5fc5b853529b139
zh: 4cc5eba14f9660e6
+21 -1
View File
@@ -431,7 +431,7 @@ New tasks initialize an empty history. Older task rows without history expose on
## Host delivery
The Host reads persisted tasks at startup and sets its timer to the earliest active target. Inactive tasks remain queryable but never schedule another delivery. At delivery it resolves the original Session through the Session controller, including cold Sessions. After restoration it samples the wall clock again and excludes members that are no longer due after a rollback; those tasks retain their timer obligation. Neither a Goal nor a separate Run completion record participates in dispatch.
The Host reads persisted tasks at startup and sets its timer to the earliest active target. Inactive tasks remain queryable but never schedule another delivery. At delivery it resolves the original Session, including cold Sessions, through `schedule/resolve-agent`. The embedding owns activation: the SDK handler waits for initialization and retains its Session handle and model options; without a handler, the Web Session controller restores the Session. The composition waits for its activation provider before starting Schedule. After restoration it samples the wall clock again and excludes members that are no longer due after a rollback; those tasks retain their timer obligation. Neither a Goal nor a separate Run completion record participates in dispatch.
Plugin-sourced `followup()` synchronously appends the message to the Session inbox. After a successful Session flush, one task-record put appends a saved delivery with its instruction snapshot and stores the latest `ScheduleDeliveryReceipt` together with the one-shot's `inactive` status or the recurring task's next target and status. Recurring tasks in the same batch share the receipt's `messageId`; each receipt retains that task's delivered occurrence time. Delivery never steers or cancels the current turn and does not wait for model completion.
@@ -547,4 +547,24 @@ Durable task set changed; clients refetch global task and Session-active catalog
```
Source: [`packages/schedule/schedule/src/types.ts`](../../packages/schedule/schedule/src/types.ts)
<a id="scheduleresolve-agent--waterfall"></a>
#### `schedule/resolve-agent` — waterfall
Restore a reminder's Session through its embedding's owning controller. Call next() to use the Web Session controller.
```ts cordis-catalog
/**
* Restore a reminder's Session through its embedding's owning controller.
* Call next() to use the Web Session controller.
* @param sessionId - Original Session receiving the reminder.
* @mode waterfall
*/
'schedule/resolve-agent'(sessionId: SessionId, next: () => Promise<Agent>): Promise<Agent>
```
Types: [Agent](core.md) · [SessionId](core.md)
Source: [`packages/schedule/schedule/src/index.ts`](../../packages/schedule/schedule/src/index.ts)
<!-- END GENERATED cordis-surface -->
+21 -1
View File
@@ -431,7 +431,7 @@ type ScheduleDeliveryHistoryResult =
## 宿主投递
宿主启动时读取持久任务,并将定时器设为最早的活动目标。未运行任务仍可查询,但不会再次安排投递。投递时通过 Session controller 获取原 Session,包括尚未加载的 Session。恢复后重新采样实际时间,并排除时钟回拨后尚未到期的成员;这些任务仍保留定时义务。投递不涉及 Goal 或独立的 Run 完成记录。
宿主启动时读取持久任务,并将定时器设为最早的活动目标。未运行任务仍可查询,但不会再次安排投递。投递通过 `schedule/resolve-agent` 解析原 Session,包括尚未激活的会话。嵌入方拥有激活过程:SDK 处理方等待初始化,并保留其 Session handle 和模型选项;没有处理方时由 Web Session controller 恢复会话。组合等待其激活提供方就绪后再启动 Schedule。恢复后重新采样实际时间,并排除时钟回拨后尚未到期的成员;这些任务仍保留定时义务。投递不涉及 Goal 或独立的 Run 完成记录。
插件来源的 `followup()` 同步将消息追加到 Session 收件箱。Session flush 成功后,一次任务记录 put 追加带有指令快照的发送记录,并同时存储最新的 `ScheduleDeliveryReceipt`,以及一次性任务的 `inactive` 状态或重复任务的下一目标和状态。同一批次的重复任务共享回执的 `messageId`;每条回执保留该任务已投递的发生时点。投递不会中途引导或取消当前轮次,也不等待模型完成。
@@ -547,4 +547,24 @@ Durable task set changed; clients refetch global task and Session-active catalog
```
Source: [`packages/schedule/schedule/src/types.ts`](../../packages/schedule/schedule/src/types.ts)
<a id="scheduleresolve-agent--waterfall"></a>
#### `schedule/resolve-agent` — waterfall
Restore a reminder's Session through its embedding's owning controller. Call next() to use the Web Session controller.
```ts cordis-catalog
/**
* Restore a reminder's Session through its embedding's owning controller.
* Call next() to use the Web Session controller.
* @param sessionId - Original Session receiving the reminder.
* @mode waterfall
*/
'schedule/resolve-agent'(sessionId: SessionId, next: () => Promise<Agent>): Promise<Agent>
```
Types: [Agent](core.zh.md) · [SessionId](core.zh.md)
Source: [`packages/schedule/schedule/src/index.ts`](../../packages/schedule/schedule/src/index.ts)
<!-- END GENERATED cordis-surface -->
+2 -2
View File
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
/session-projections:
en: e9330151d3eb579a
zh: 53e47acd1d810da9
en: 45e5f8cb8ca18d6b
zh: 4ecfad40ec9e221c
/session-projections/the-unit:
en: d48814d3ab0e997f
zh: d0337d8eba097a63
+4 -2
View File
@@ -26,6 +26,8 @@ interface ProjectionHandle<S> {
}
```
The [compatibility rules](../../packages/session/session-projection/README.md#deprecated-plugin-compatibility) define legacy disposal, keyed reads, and their once-per-key deprecation warnings.
## The unit
`ProjectionDefinition<K, S>` carries each unit's host state type locally; registration infers `S` from `stateSchema` and returns `ProjectionHandle<S>`. 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:
@@ -314,7 +316,7 @@ Source: [`packages/session/session-projection-cache/src/index.ts`](../../package
* Retained handles remain readable after release.
* Callers must await registration before publishing consumers; registration may perform asynchronous work.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns a ready handle with synchronous state reads and the effect disposer.
* @returns an awaitable yielding a ready handle; the callable runtime value is deprecated.
*/
register<K extends keyof SessionProjectionMap, S>( definition: Omit<ProjectionDefinition<K, S>, 'wire'> & { wire: NonNullable<ProjectionDefinition<K, S>['wire']> }, ): Promise<ProjectionHandle<S>>
@@ -323,7 +325,7 @@ register<K extends keyof SessionProjectionMap, S>( definition: Omit<ProjectionDe
* and always checkpointed like every other unit.
* Callers must await registration before publishing consumers; registration may perform asynchronous work.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns a ready handle with synchronous state reads and the effect disposer.
* @returns an awaitable yielding a ready handle; the callable runtime value is deprecated.
*/
register<K extends string, S>( definition: ProjectionDefinition<K, S> & { key: Exclude<K, keyof SessionProjectionMap> }, ): Promise<ProjectionHandle<S>>
+4 -2
View File
@@ -26,6 +26,8 @@ interface ProjectionHandle<S> {
}
```
[兼容规则](../../packages/session/session-projection/README.zh.md#deprecated-plugin-compatibility)规定旧释放操作、按键读取及其每个键一次的弃用警告。
## 投影单元
`ProjectionDefinition<K, S>` 在各单元内部携带宿主状态类型;注册从 `stateSchema` 推导 `S`,并返回 `ProjectionHandle<S>`。只有客户端可见的 key 合并到 `SessionProjectionMap`,由它约束对应的全量值。`wire` 块使单元对客户端可见,渲染归 slot 体系管,永远不归本层:
@@ -314,7 +316,7 @@ Source: [`packages/session/session-projection-cache/src/index.ts`](../../package
* Retained handles remain readable after release.
* Callers must await registration before publishing consumers; registration may perform asynchronous work.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns a ready handle with synchronous state reads and the effect disposer.
* @returns an awaitable yielding a ready handle; the callable runtime value is deprecated.
*/
register<K extends keyof SessionProjectionMap, S>( definition: Omit<ProjectionDefinition<K, S>, 'wire'> & { wire: NonNullable<ProjectionDefinition<K, S>['wire']> }, ): Promise<ProjectionHandle<S>>
@@ -323,7 +325,7 @@ register<K extends keyof SessionProjectionMap, S>( definition: Omit<ProjectionDe
* and always checkpointed like every other unit.
* Callers must await registration before publishing consumers; registration may perform asynchronous work.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns a ready handle with synchronous state reads and the effect disposer.
* @returns an awaitable yielding a ready handle; the callable runtime value is deprecated.
*/
register<K extends string, S>( definition: ProjectionDefinition<K, S> & { key: Exclude<K, keyof SessionProjectionMap> }, ): Promise<ProjectionHandle<S>>
@@ -1,21 +0,0 @@
---
kind: upgrade-guide
description: "Session projection registration is asynchronous and returns a retained reader instead of a disposer."
---
# Session projection registration handles
English | [中文](guide.zh.md)
## Change
`@deepseek-ai/dsh-session-projection` changes `SessionProjectionRegistry.register(definition)` from a synchronous disposer to `Promise<ProjectionHandle<S>>`. Registry-level `stateOf(session, key)` and `SessionProjectionStateMap` are removed. Host state types stay local to `ProjectionDefinition<K, S>` and are inferred from `stateSchema` during registration. Registration errors now reject the returned promise. After the last registration is disposed, the unit leaves snapshots, checkpoints, and live notifications, but retained handles can still fold Session events on demand.
`@deepseek-ai/dsh-agent` and `@deepseek-ai/dsh-subagent` require `sessionProjections`. AgentRegistry owns turn and step state; consumers use `ctx.agents.turnBoundaryOf(session)` instead of importing `turnBoundaryProjectionDefinition`. Turn-state consumers, including the Claude Code mods bridge, require `agents`. Preset and context-pressure reads use `ctx.agentPresets.presetForSession(session)` and `ctx.tokenMeter.contextPressure(session)`. The API controller and mods bridge no longer register those definitions independently; their optional-service behavior applies when the owning service is absent. `@deepseek-ai/dsh-api-session-controller` also removes `installModelSelectionProjection`.
## Migration
1. Mount `SessionProjectionRegistry` before `AgentRegistry`, `SubagentRuntime`, and other projection owners in custom compositions. Include `@deepseek-ai/dsh-agent` wherever a plugin requires `agents`. Consumers of domain data inject the owning service.
2. Remove host-state map augmentations and imports; pass the local state type as the second `ProjectionDefinition<K, S>` argument. Keep client-value declarations in `SessionProjectionMap`. For a plugin's own definitions, await `ctx.sessionProjections.register(definition)` in asynchronous `apply()` or `[Service.init]()` and retain the handle privately before publishing dependent operations. Keep existing function-plugin namespace exports.
3. Replace `registry.stateOf(session, key)` with a business service method for another domain, or `handle.stateOf(session)` inside the owner. Replace stored unregister callbacks with `handle.dispose`, which always returns `Promise<void>`, and await explicit disposal; fiber disposal still releases registrations automatically. Await registration when handling initialization failures.
4. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`.
5. Typecheck custom plugins, then verify activation, a Session state read, and owner disposal. A released reader remains usable, while the released unit disappears from new snapshots once its final registration is gone.
@@ -1,21 +0,0 @@
---
kind: upgrade-guide
description: "Session 投影注册改为异步,并返回保留读取能力的句柄而不是释放函数。"
---
# Session 投影注册句柄
[English](guide.md) | 中文
## 变更
`@deepseek-ai/dsh-session-projection` 的 `SessionProjectionRegistry.register(definition)` 从同步释放函数改为返回 `Promise<ProjectionHandle<S>>`。注册表级 `stateOf(session, key)` 与 `SessionProjectionStateMap` 已移除。宿主状态类型保留在各自的 `ProjectionDefinition<K, S>` 中,注册时从 `stateSchema` 推导。注册错误现在使返回的 Promise 拒绝。最后一次注册释放后,该投影单元不再参与快照、检查点和实时通知,但保留的句柄仍可按需折叠 Session 事件。
`@deepseek-ai/dsh-agent` 与 `@deepseek-ai/dsh-subagent` 依赖 `sessionProjections`。AgentRegistry 持有轮次和步骤状态;消费方使用 `ctx.agents.turnBoundaryOf(session)`,不再导入 `turnBoundaryProjectionDefinition`。包括 Claude Code mods 桥接在内的轮次状态消费方依赖 `agents`。预设与上下文压力分别通过 `ctx.agentPresets.presetForSession(session)` 和 `ctx.tokenMeter.contextPressure(session)` 读取。API 控制器与 mods 桥接不再独立注册这些定义;所属服务缺席时采用各自的可选服务行为。`@deepseek-ai/dsh-api-session-controller` 同时移除了 `installModelSelectionProjection`。
## 迁移
1. 在自定义组合中,先挂载 `SessionProjectionRegistry`,再挂载 `AgentRegistry`、`SubagentRuntime` 及其他投影所有者。组合中的插件依赖 `agents` 时,加入 `@deepseek-ai/dsh-agent`。领域数据的消费方注入所属服务。
2. 删除宿主状态映射的声明合并与导入,将局部状态类型作为 `ProjectionDefinition<K, S>` 的第二个类型参数。保留 `SessionProjectionMap` 中的客户端值声明。对插件自己的定义,在异步 `apply()` 或 `[Service.init]()` 中等待 `ctx.sessionProjections.register(definition)`,并在提供依赖操作前于内部保存句柄。保留现有函数插件命名空间导出。
3. 将 `registry.stateOf(session, key)` 替换为其他领域的业务服务方法,或所有者内部的 `handle.stateOf(session)`。将保存的取消注册回调替换为始终返回 `Promise<void>` 的 `handle.dispose`,并等待显式释放完成;fiber 释放仍会自动释放注册。处理初始化失败时须等待注册。
4. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。
5. 对自定义插件执行类型检查,再验证激活、Session 状态读取与 owner 释放。已释放的读取句柄仍可使用,而投影单元在最后一次注册解除后会从新快照中消失。
@@ -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/sdk-desktop-context/guide.md
/:
en: d3cffa64b9c50498
zh: 515c03ce9ae6d031
/sdk-desktop-tools-and-context:
en: d202616bf6e03fa4
zh: cc276adda4b952be
/sdk-desktop-tools-and-context/change:
en: 85ac2825c52e2503
zh: fd6fdbdca131c778
/sdk-desktop-tools-and-context/migration:
en: 9caef4f8b153622a
zh: 1b4901f5b18b72fd
@@ -0,0 +1,18 @@
---
kind: upgrade-guide
description: The existing SDK profile includes native desktop tools and model context.
---
# SDK desktop tools and context
English | [中文](guide.zh.md)
## Change
The existing `sdk` profile now includes `present`, `ask_user_question`, schedule tools, file-reference guidance and time context. It does not start a Web server. Standard tool calling remains the default; `DSH_TOOLS_MODE=ptc` selects programmatic tool calling in the same profile. `sdk-minimal` is unchanged.
## Migration
1. Keep selecting `sdk`. Provide `onUserQuestion` (TypeScript) or `on_user_question` (Python) to answer built-in questions over the native protocol, or retain an in-process `user-questions/request` answerer; without one, the tool reports `NO_PROVIDER`.
2. Working directory and model still come from SDK initialization. The SDK profile does not mount the Web-specific checkout and GUI development context.
3. Custom compositions that need the previous model-visible tool/context roster can disable the `present`, `tool-ask-user`, `tool-schedule`, `file-reference-local`, `deliverable-file-references`, and `time-context` rows. Review any pinned prompts and tool schemas before upgrading.
@@ -0,0 +1,18 @@
---
kind: upgrade-guide
description: 现有 SDK profile 包含原生桌面工具和模型上下文。
---
# SDK 桌面工具与上下文
[English](guide.md) | 中文
## 变更
现有 `sdk` profile 现在包含 `present`、`ask_user_question`、定时任务工具、文件引用说明和时间上下文。它不启动 Web server。默认仍使用标准工具调用;`DSH_TOOLS_MODE=ptc` 在同一 profile 中选择程序化工具调用。`sdk-minimal` 保持不变。
## 迁移
1. 继续选择 `sdk`。通过 `onUserQuestion`(TypeScript)或 `on_user_question`(Python)使用原生协议回答内建问题,或保留进程内的 `user-questions/request` 回答方;没有回答方时,工具报告 `NO_PROVIDER`。
2. 工作目录和模型仍来自 SDK 初始化。SDK profile 不挂载 Web 专用的 checkout 和 GUI 开发上下文。
3. 需要保留之前模型可见工具/上下文组合的自定义配置,可以禁用 `present`、`tool-ask-user`、`tool-schedule`、`file-reference-local`、`deliverable-file-references` 和 `time-context` 配置项。升级前请检查固定的提示词和工具 schema。
@@ -1,16 +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-alpha.3/session-projection-handles/guide.md
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/session-projection-handles/guide.md
/:
en: b3f54a7eadb3398a
zh: 6815b996b2cebc6f
en: a7c158942ce2a575
zh: 7103da2fa7ca494a
/session-projection-registration-handles:
en: 5cf8cb0051860df5
zh: 2578c6c3e77bc1c7
en: 08af78abac6d3bcd
zh: 85ede5596cac9d5e
/session-projection-registration-handles/change:
en: 64c2492c43c7a263
zh: ba5a15b959df496c
en: d5e6ea606de638a0
zh: a041eee1ffd48ac3
/session-projection-registration-handles/migration:
en: bd4310bb946b6f08
zh: 2086e6e9c11bc4ed
en: 9c7d73f9f50c4bdd
zh: b1c597c63d52b444
@@ -0,0 +1,24 @@
---
kind: upgrade-guide
description: "Session projection registration resolves asynchronously to a retained reader; the callable result and registry-level state reads are deprecated."
---
# Session projection registration handles
English | [中文](guide.zh.md)
## Change
`@deepseek-ai/dsh-session-projection` changes `SessionProjectionRegistry.register(definition)` from a synchronous disposer to `Promise<ProjectionHandle<S>>`. The definition installs synchronously; awaiting the result yields a handle with `stateOf(session)` and `dispose()`, and registration errors reject it. The runtime value is a callable thenable, not a native Promise: calling it still releases the registration, but that call is deprecated. `typeof registration` is `"function"` and `registration instanceof Promise` is false, so code that branches on those checks or treats functions as callbacks is affected; Vitest's `.rejects` calls it instead of observing the rejection.
Registry-level `stateOf(session, key): unknown` is deprecated. It returns `undefined` whenever no plugin currently registers the key: its owner is not composed, has not reached registration, or has released its last registration. `SessionProjectionStateMap` is removed; host state types stay local to `ProjectionDefinition<K, S>` and are inferred from `stateSchema`. After the last registration is disposed, the unit leaves snapshots, checkpoints, and live notifications, but retained handles still fold Session events on demand. [Runtime compatibility](../../../../packages/session/session-projection/README.md#deprecated-plugin-compatibility) details the deprecated calls.
`@deepseek-ai/dsh-agent` and `@deepseek-ai/dsh-subagent` require `sessionProjections`. AgentRegistry owns turn and step state and provides `ctx.agents.turnBoundaryOf(session)`; `@deepseek-ai/dsh-agent-loop` no longer exports `turnBoundaryProjectionDefinition`. Turn-state consumers, including the Claude Code mods bridge, require `agents`. Preset and context-pressure reads use `ctx.agentPresets.presetForSession(session)` and `ctx.tokenMeter.contextPressure(session)`. The API controller and mods bridge no longer register those definitions independently; their optional-service behavior applies when the owning service is absent. `@deepseek-ai/dsh-api-session-controller` removes `installModelSelectionProjection`.
## Migration
1. Mount `SessionProjectionRegistry` before `AgentRegistry`, `SubagentRuntime`, and other projection owners in custom compositions. Include `@deepseek-ai/dsh-agent` wherever a plugin requires `agents`.
2. For a plugin's own definitions, await `ctx.sessionProjections.register(definition)` in asynchronous `apply()` or `[Service.init]()` and retain the handle before publishing dependent operations. Remove `SessionProjectionStateMap` augmentations and pass the state type as the second `ProjectionDefinition<K, S>` argument. Keep client-value declarations in `SessionProjectionMap`.
3. Replace `registry.stateOf(session, key)` with the owning service's method, such as `ctx.agents.turnBoundaryOf(session)`, or `handle.stateOf(session)` inside the owner; until then, handle its `undefined` result. Replace calls to the registration result with an awaited `handle.dispose()`; fiber disposal still releases registrations automatically.
4. Where a native Promise is required, pass `Promise.resolve(registration)`, including `await expect(Promise.resolve(registration)).rejects.toThrow(...)` in Vitest. Replace `typeof` or `instanceof Promise` checks on the result with awaiting.
5. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`.
6. Typecheck custom plugins, then verify activation, a Session state read, a rejected registration, and owner disposal.
@@ -0,0 +1,24 @@
---
kind: upgrade-guide
description: "Session 投影注册异步解析为保留读取能力的句柄;可调用的注册结果与注册表级状态读取已弃用。"
---
# Session 投影注册句柄
[English](guide.md) | 中文
## 变更
`@deepseek-ai/dsh-session-projection` 的 `SessionProjectionRegistry.register(definition)` 从同步释放函数改为 `Promise<ProjectionHandle<S>>`。定义同步安装;等待结果得到带 `stateOf(session)` 与 `dispose()` 的句柄,注册错误使结果拒绝。运行时返回值是可调用的 thenable,而非原生 Promise:调用它仍会释放注册,但该调用已弃用。`typeof registration` 为 `"function"`,`registration instanceof Promise` 为 false,因此按这些检查分支或把函数视为回调的代码会受影响;Vitest 的 `.rejects` 会调用它,而不是观察拒绝。
注册表级 `stateOf(session, key): unknown` 已弃用。当前没有任何插件注册该 key 时它返回 `undefined`:所属插件未组合、尚未执行到注册,或已释放最后一次注册。`SessionProjectionStateMap` 已移除;宿主状态类型保留在各自的 `ProjectionDefinition<K, S>` 中,并从 `stateSchema` 推导。最后一次注册释放后,该投影单元不再参与快照、检查点和实时通知,但保留的句柄仍可按需折叠 Session 事件。[运行时兼容说明](../../../../packages/session/session-projection/README.zh.md#deprecated-plugin-compatibility)详述这些已弃用调用。
`@deepseek-ai/dsh-agent` 与 `@deepseek-ai/dsh-subagent` 依赖 `sessionProjections`。AgentRegistry 持有轮次和步骤状态,并提供 `ctx.agents.turnBoundaryOf(session)`;`@deepseek-ai/dsh-agent-loop` 不再导出 `turnBoundaryProjectionDefinition`。包括 Claude Code mods 桥接在内的轮次状态消费方依赖 `agents`。预设与上下文压力分别通过 `ctx.agentPresets.presetForSession(session)` 和 `ctx.tokenMeter.contextPressure(session)` 读取。API 控制器与 mods 桥接不再独立注册这些定义;所属服务缺席时采用各自的可选服务行为。`@deepseek-ai/dsh-api-session-controller` 移除了 `installModelSelectionProjection`。
## 迁移
1. 在自定义组合中,先挂载 `SessionProjectionRegistry`,再挂载 `AgentRegistry`、`SubagentRuntime` 及其他投影所有者。组合中的插件依赖 `agents` 时,加入 `@deepseek-ai/dsh-agent`。
2. 对插件自己的定义,在异步 `apply()` 或 `[Service.init]()` 中等待 `ctx.sessionProjections.register(definition)`,并在提供依赖操作前保存句柄。删除 `SessionProjectionStateMap` 的声明合并,将状态类型作为 `ProjectionDefinition<K, S>` 的第二个类型参数。保留 `SessionProjectionMap` 中的客户端值声明。
3. 将 `registry.stateOf(session, key)` 替换为所属服务的方法(例如 `ctx.agents.turnBoundaryOf(session)`),或所有者内部的 `handle.stateOf(session)`;替换之前须处理其 `undefined` 结果。将对注册结果的调用替换为等待 `handle.dispose()`;fiber 释放仍会自动释放注册。
4. 需要原生 Promise 时传入 `Promise.resolve(registration)`,包括在 Vitest 中写 `await expect(Promise.resolve(registration)).rejects.toThrow(...)`。将对结果的 `typeof` 或 `instanceof Promise` 检查改为直接等待。
5. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。
6. 对自定义插件执行类型检查,再验证激活、Session 状态读取、注册被拒绝以及 owner 释放。
+10 -10
View File
@@ -15,8 +15,8 @@
en: 4e53164159b5ce41
zh: 89582dc0294d0776
/deepseek-ai-dsh-sdk-app/use-this-package:
en: 63573087d1db9aff
zh: 638612ac3f0da2c0
en: b76bd7ea9880f529
zh: e80c7b9fa331dd9b
/deepseek-ai-dsh-sdk-app/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
@@ -24,17 +24,17 @@
en: 4f88e647c6df4f42
zh: 2bd3e4b0bdfe9412
/deepseek-ai-dsh-sdk-app/model-experience/sdk-model-context/what-the-model-sees:
en: fc42eb5995eb2078
zh: 4e47602e46ba5123
en: 0cce2b88aae3a0e5
zh: fcb31314096ef15e
/deepseek-ai-dsh-sdk-app/model-experience/sdk-model-context/token-effect:
en: 4a0cdca06d358f53
zh: d27080dcb68c661c
en: 0d4a38b2448396b3
zh: e63408a146bf0100
/deepseek-ai-dsh-sdk-app/model-experience/sdk-model-context/kv-cache-effect:
en: 5b0e33295fb90e19
zh: 4d48ca75bb677202
en: 0503df0971e5f8d7
zh: 18cae8c6e25cda04
/deepseek-ai-dsh-sdk-app/known-limitations-and-deferred-work:
en: 416e2793f9786480
zh: 05cc4fd289d0185d
en: 451497677de7e6b9
zh: dcfdc671c38d54d4
/deepseek-ai-dsh-sdk-app/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: e88aeb617ffb74ea
+9 -3
View File
@@ -23,6 +23,8 @@ The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/RE
<a id="use-this-package"></a>
## Use this package
The `sdk` profile includes native desktop tools and model context. Standard tool calling is the default; set `DSH_TOOLS_MODE=ptc` for programmatic tool calling. Both modes use the same plugins.
The startup provider binds stdin EOF to the launcher's bounded successful shutdown. SDK protocol `shutdown`, SIGINT, and SIGTERM retain their owning server or launcher paths; disposal drains the root profile tree and persistence. Stdout is reserved for newline-delimited JSON-RPC frames. The bundle disables model-generated session titles because the SDK exposes no title surface; deterministic fallback titles remain durable without an auxiliary model request. The inherited projection cache checkpoints SDK-created sessions for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. A deployment selects a different complete composition through profile bundles and patch files, not another app bin.
| Config | Default | Behavior |
@@ -44,20 +46,24 @@ The Python runtime exposes explicitly downloaded authoring resources and Office
#### What the model sees
The profile inherits the base system prompt, including the `You are an AI agent powered by DeepSeek Harness.` identity, and adds the same model-interpolated coding persona as the Web and headless profiles. Each Session receives its current directory in required user-role context from [`dsh-working-directory`](../../session/working-directory/README.md). Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`.
The profile inherits the base system prompt, including the `You are an AI agent powered by DeepSeek Harness.` identity, and adds the same model-interpolated coding persona as the Web and headless profiles. Each Session receives its current directory in required user-role context from [`dsh-working-directory`](../../session/working-directory/README.md). Default file tool schemas include `read`, `write`, and `edit`; they omit `str_replace_editor`. The model also receives `present`, `ask_user_question`, schedule tools, shared file-reference guidance, and time context. Child agents cannot use schedule tools. The [`time-context`](../../context/time-context/README.md) row accepts a fallback time zone. Skills use the configured resources described above.
#### Token effect
The base prompt sections and selected tool schemas determine token usage; this profile adds one coding-persona sentence.
The profile adds a coding-persona sentence, the mounted tool definitions and guidance, and logged runtime-context and time messages.
#### KV Cache effect
Stable for a fixed profile, provider, model, and tool roster. Profile changes take effect on the next process because the shipped SDK profile uses startup-only patches.
The static prefix is stable for a fixed profile, provider, model, and tool roster. Runtime values append to logged user context. Profile changes take effect on the next process because the shipped SDK profile uses startup-only patches.
## Known Limitations and Deferred Work
<a id="known-limitations-and-deferred-work"></a>
- **No graphical client** — file cards, native file opening, and browser state require an embedding client; the SDK starts no Web server.
- **User time zone is unknown** — the SDK protocol does not carry a user time zone, so time context reports `Browser time zone for this request: unavailable.` The process or container time zone may differ from the user's. The `time-context.timeZone` setting only controls fallback timestamp formatting; it does not establish the user's time zone. Automatic detection and SDK time-zone input are deferred.
- **Questions require an answerer** — configure `onUserQuestion` in TypeScript or `on_user_question` in Python, or supply a native `user-questions/request` handler; otherwise `ask_user_question` returns `NO_PROVIDER`.
- **Schedules share the runtime lifetime** — delivery needs a running process and its persistent Session storage.
- **A profile can omit the SDK server** — a custom profile selected by the TypeScript client must retain this bundle or another `dsh-sdk-jsonrpc-server` row; client initialization fails when no peer answers.
- **User plugins can violate stdout purity** — profile and per-launch patches are trusted application composition. The shipped bundle writes no non-protocol stdout, but it cannot contain an arbitrary inserted plugin.
- **Configuration changes require restart** — the `sdk-app` bundle disables HMR in YAML so one stdio connection never observes a replacement server or Agent dependency.
+9 -3
View File
@@ -23,6 +23,8 @@ kind: "package-bundle"
<a id="use-this-package"></a>
## 使用本包
`sdk` profile 包含原生桌面工具和模型上下文。默认使用标准工具调用;设置 `DSH_TOOLS_MODE=ptc` 可使用程序化工具调用。两种模式使用相同插件。
启动提供方把 stdin EOF 接到启动器的有界成功关闭流程。SDK 协议 `shutdown`、SIGINT 与 SIGTERM 继续使用各自所属的 server 或启动器路径;dispose(资源释放)会排空根 profile 配置树与持久化。stdout 专用于按换行分隔的 JSON-RPC 帧。SDK 不提供 title 表层,因此本组合包禁用模型生成的会话标题;确定性的 fallback title 仍会持久化,但不发起辅助模型请求。继承的投影缓存会为 SDK 创建的会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 行。部署通过 profile 组合包与 patch 文件选择另一套完整组合,而不是使用另一个应用 bin。
| 配置 | 默认值 | 行为 |
@@ -44,20 +46,24 @@ Python 运行时向本 profile 提供显式下载的创作资源和 Office sidec
#### 模型看到什么
profile 继承 base 系统提示词,包括 `You are an AI agent powered by DeepSeek Harness.` 身份说明,并添加与 Web 和 headless profile 相同、按当前模型插值的 coding persona。每个 Session 通过 [`dsh-working-directory`](../../session/working-directory/README.zh.md) 提供的必需 user-role 上下文接收当前目录。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。
profile 继承 base 系统提示词,包括 `You are an AI agent powered by DeepSeek Harness.` 身份说明,并添加与 Web 和 headless profile 相同、按当前模型插值的 coding persona。每个 Session 通过 [`dsh-working-directory`](../../session/working-directory/README.zh.md) 提供的必需 user-role 上下文接收当前目录。默认文件工具 schema 包含 `read`、`write` 和 `edit`,不包含 `str_replace_editor`。模型还会收到 `present`、`ask_user_question`、定时任务工具、共用文件引用说明和时间上下文。子 agent 不能使用定时任务工具。[`time-context`](../../context/time-context/README.zh.md) 配置项接受回退时区。Skills 使用上述配置的资源。
#### Token 影响
base 提示词段落与所选工具 schema 决定 token 用量;此 profile 额外添加一句 coding persona。
此 profile 添加一句 coding persona、所挂载的工具定义与说明,以及记录在日志中的运行时上下文和时间消息。
#### KV Cache 影响
对固定 profile、提供方、模型与工具清单保持稳定。由于随附 SDK profile 使用仅启动时 patch,profile 变化会在下一个进程生效。
对固定 profile、提供方、模型与工具清单,静态前缀保持稳定。运行时值追加到已记录的 user 上下文中。由于随附 SDK profile 使用仅启动时 patch,profile 变化会在下一个进程生效。
## 已知限制与延期工作
<a id="known-limitations-and-deferred-work"></a>
- **没有图形客户端**:文件卡片、本地文件打开和浏览器状态需要嵌入客户端;SDK 不启动 Web server。
- **用户时区未知**:SDK 协议不携带用户时区,因此时间上下文显示 `Browser time zone for this request: unavailable.`。进程或容器的时区可能与用户时区不同。`time-context.timeZone` 配置只控制回退时间戳的显示格式,不能据此确定用户时区。自动检测和 SDK 时区传参暂缓支持。
- **提问需要回答方**:在 TypeScript 中配置 `onUserQuestion`、在 Python 中配置 `on_user_question`,或提供原生 `user-questions/request` handler;否则 `ask_user_question` 返回 `NO_PROVIDER`。
- **定时任务依赖运行时生命周期**:投递需要运行中的进程及其持久化 Session 存储。
- **profile 可能省略 SDK server**:TypeScript client 选择的自定义 profile 必须保留本组合包或另一个 `dsh-sdk-jsonrpc-server` 配置项;没有 peer 响应时,client 初始化会失败。
- **用户插件可以破坏 stdout 纯净性**:profile 与逐次启动 patch 属于受信任应用组合。随附组合包不会向 stdout 写入非协议内容,但无法约束任意插入插件。
- **配置变化需要重启**:`sdk-app` 组合包在 YAML 中禁用 HMR,因此一个 stdio 连接不会观察到 server 或 agent 依赖被替换。
+37
View File
@@ -39,3 +39,40 @@
assetRoot: !!js "process.env.DSH_BUNDLED_OFFICE_SKILLS ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')"
cli: !!js "process.env.DSH_BUNDLED_OFFICE_CLI === '' ? false : process.env.DSH_BUNDLED_OFFICE_CLI"
node: !!js "process.env.DSH_BUNDLED_OFFICE_NODE ?? process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', 'dependencies', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node')"
# Share native desktop tools and model context without starting a Web server.
- id: tools
config:
mode: !!js process.env.DSH_TOOLS_MODE
- insert:
- id: connection
name: '@deepseek-ai/dsh-client-connection'
- id: file-upload
name: '@deepseek-ai/dsh-client-file-upload'
- id: workspace
name: '@deepseek-ai/dsh-workspace'
- id: schedule
name: '@deepseek-ai/dsh-schedule'
inject: [sdkSessionActivation]
- id: time-context
name: '@deepseek-ai/dsh-time-context'
- id: tool-schedule
name: '@deepseek-ai/dsh-tool-schedule'
- id: tool-ask-user
name: '@deepseek-ai/dsh-tool-ask-user'
- id: present
name: '@deepseek-ai/dsh-tool-present'
- id: file-reference-local
name: '@deepseek-ai/dsh-file-reference-local'
- id: deliverable-file-references
name: '@deepseek-ai/dsh-deliverable-file-references'
+11 -1
View File
@@ -39,7 +39,17 @@
"@deepseek-ai/schemastery": "workspace:~",
"commander": "^15.0.0",
"@deepseek-ai/dsh-skill-office": "workspace:*",
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:*"
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:*",
"@deepseek-ai/dsh-client-connection": "workspace:*",
"@deepseek-ai/dsh-client-file-upload": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
"@deepseek-ai/dsh-schedule": "workspace:*",
"@deepseek-ai/dsh-time-context": "workspace:*",
"@deepseek-ai/dsh-tool-schedule": "workspace:*",
"@deepseek-ai/dsh-tool-ask-user": "workspace:*",
"@deepseek-ai/dsh-tool-present": "workspace:*",
"@deepseek-ai/dsh-file-reference-local": "workspace:*",
"@deepseek-ai/dsh-deliverable-file-references": "workspace:*"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~"
+3
View File
@@ -346,6 +346,9 @@
- id: ui-cordis
name: '@deepseek-ai/dsh-client-ui-cordis'
- id: deliverable-file-references
name: '@deepseek-ai/dsh-deliverable-file-references'
# Turn tail: the changed-files card and delivery cards under each closing
# assistant message. Remove this entry to turn the surface off; the tail
# hole renders empty.
+2 -1
View File
@@ -188,7 +188,8 @@
"@deepseek-ai/dsh-client-product-analytics": "workspace:*",
"@deepseek-ai/dsh-host-product-telemetry-otel": "workspace:*",
"@deepseek-ai/dsh-client-ui-settings-session-log": "workspace:*",
"@deepseek-ai/dsh-harness-dev-context": "workspace:*"
"@deepseek-ai/dsh-harness-dev-context": "workspace:*",
"@deepseek-ai/dsh-deliverable-file-references": "workspace:*"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
+2 -2
View File
@@ -33,8 +33,8 @@
en: b0d806604bc17006
zh: 18dabe5dc2d0d419
/deepseek-ai-dsh-client-ui-chat/grouped-rendering:
en: 408664597c1480ed
zh: 287a9b981337f288
en: f78cbc73be5dc3ac
zh: 893ba03d66b8ab65
/deepseek-ai-dsh-client-ui-chat/scroll-ownership:
en: 284737900cb75668
zh: 12e0844082ff79ce
+1 -1
View File
@@ -107,7 +107,7 @@ The Chat Builder publishes `bottomSource` per Node: true for visible Nodes belon
Live tool deltas share reasoning's frame-batched publication; durable calls and results publish immediately. Native and nested results retain event presentation metadata, so historical file and command labels use the operation’s location. Repeated deltas retain the Tool node when observed argument answers, the anchor, location, and visibility are unchanged. The Definition supplies the same lazy argument reader to tool rows and group detail, without tool-specific registration.
The process group uses a stable `div` layout box, a scroll body, and an uncapped content box that reports growth inside the body. Business styles must adapt spacing within and across groups, including hidden or empty members and the answer-spacing exception. The `conversation.chat.flow` slot adds a `display: contents` DOM anchor; row selectors match that anchor directly, without repeating its column ancestry. The running status shares the rows' parent, so its divider uses the same visible-sibling rules. The anchor contributes no spacing of its own. CSS variables do not belong in the Group Definition.
The process group uses a stable `div` layout box, a scroll body, and an uncapped content box that reports growth inside the body. Business styles must adapt spacing within and across groups, including hidden or empty members and the answer-spacing exception. The `conversation.chat.flow` slot adds a `display: contents` DOM anchor; row selectors match that anchor directly, without repeating its column ancestry. The running status shares the rows' parent, so its divider uses the same visible-sibling rules. The anchor contributes no spacing of its own. The earlier-history control directly precedes the flow anchor; their sibling relationship supplies the leading gap. CSS variables do not belong in the Group Definition.
Running group titles, reasoning previews, tool and command rows, the active retry line, and the bottom running status use the shared [TextShimmer](../ui-primitives/README.md#component-catalog). Each row’s text shares one left-to-right highlight, including collapsed group titles; icons and hover chevrons retain their base appearance. Tool and process rows sit 6px apart. An expanded group title has 8px before its content, Assistant responses have 12px from adjacent process rows, and completed-Turn duration/status controls have 16px from their input and from a directly following visible row. A reply that follows the control after folded rows keeps the 12px reply gap, so folding never changes its spacing. Hidden and empty rows, including retained empty slot anchors, add no gap. The running “Deep diving” text retains its blue base while its theme-specific highlight sweeps across it.
+1 -1
View File
@@ -107,7 +107,7 @@ Chat Builder 按节点发布 `bottomSource`:节点可见且属于已加载的
实时工具 delta 与推理共用按帧合并的发布节奏;持久调用和结果立即发布。原生与嵌套结果保留事件中的展示元数据,使历史文件与命令标签使用执行时的位置。当已读取参数的答案、锚点、位置及可见性均未变化时,重复 delta 保持 Tool 节点不变。Definition 向工具行和分组详情提供同一个懒计算参数读器,无需按工具注册。
过程组使用稳定的 `div` 布局盒子、滚动正文及不限高的内容盒子,后者报告正文内部的内容增长。业务样式必须适配组内及组边界间距,处理隐藏或空成员以及回答前的间距特例。`conversation.chat.flow` 插槽添加一个 `display: contents` DOM 锚点;行选择器直接匹配该锚点,不重复匹配其正文容器祖先。运行状态与各行共享父级,使分隔线使用同一套可见兄弟节点规则。锚点自身不产生间距。CSS 变量不属于 Group Definition。
过程组使用稳定的 `div` 布局盒子、滚动正文及不限高的内容盒子,后者报告正文内部的内容增长。业务样式必须适配组内及组边界间距,处理隐藏或空成员以及回答前的间距特例。`conversation.chat.flow` 插槽添加一个 `display: contents` DOM 锚点;行选择器直接匹配该锚点,不重复匹配其正文容器祖先。运行状态与各行共享父级,使分隔线使用同一套可见兄弟节点规则。锚点自身不产生间距。更早历史控件紧邻流锚点之前,两者的兄弟关系决定首行间距。CSS 变量不属于 Group Definition。
运行中的分组标题、思考预览、工具与命令行、当前重试行以及底部运行状态使用共享的 [TextShimmer](../ui-primitives/README.zh.md#component-catalog)。每行的文字共用一道从左到右的高光,包括折叠的分组标题;图标和悬停箭头保留底色。工具和过程行之间相隔 6px;展开的分组标题与内容之间相隔 8px;Assistant 回复与相邻过程行之间相隔 12px;每轮完成/耗时控件与其输入、以及紧随其后的可见行之间保留 16px。控件与回复之间隔着已收起的行时,回复保持 12px 回复间距,因此折叠不会改变它的间距。隐藏行与空行(包括保留的空 slot 锚点)不产生间距。“深度求索”运行文字保留蓝色底色,并使用主题专用高光扫过文字。
@@ -64,10 +64,11 @@
/* The flow slot's display:contents anchor preserves layout but not selector ancestry.
`hidden="until-found"` retains a searchable zero-height box; hidden and empty
Seats do not contribute spacing. */
Seats do not contribute spacing. The older-history control precedes the anchor;
an ancestor :has() here also invalidates the history on unrelated descendant edits. */
:is(.column, :global([data-slot="conversation.chat.flow"])) > :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))
~ :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))),
.column:has(> .older) > :global([data-slot="conversation.chat.flow"])
.column > .older + :global([data-slot="conversation.chat.flow"])
> :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))) {
margin-top: var(--dsh-chat-flow-gap, 6px);
}
@@ -18,8 +18,8 @@
en: 4a2fcdcf13276fef
zh: 8bde03bbbf15840c
/deepseek-ai-dsh-client-ui-deliverables/use-this-package/explicit-deliveries:
en: 0ee4bbe45bb5e473
zh: 9c99d829b25f59ba
en: 6ae1652a01354a6f
zh: baa44d415d7a9f0e
/deepseek-ai-dsh-client-ui-deliverables/use-this-package/the-changed-files-card:
en: 4b279e2bddddc083
zh: a3135393299c935b
@@ -30,26 +30,26 @@
en: ef13aac8ac36a5c3
zh: 1e2ae4b3eaf98818
/deepseek-ai-dsh-client-ui-deliverables/understand-the-implementation:
en: 13e06395a2c447bf
zh: c2c29bbfbfabe756
en: 7ee5ee783902662c
zh: fdf2c244fb22b8ab
/deepseek-ai-dsh-client-ui-deliverables/further-exploration:
en: 476b59e8671db982
zh: b3d0495a03e8940a
/deepseek-ai-dsh-client-ui-deliverables/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance:
en: eb244aa21a4e5210
zh: e3450addfa19cea5
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance/what-the-model-sees:
en: 78339e353ae47a53
zh: 582efa623a5dfd72
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance/token-effect:
en: 66bd094b37a29c22
zh: e657d66ac6f95a22
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance/kv-cache-effect:
en: 52626a05acc8b320
zh: 6a8e62b0d47d5297
/deepseek-ai-dsh-client-ui-deliverables/model-experience/desktop-file-references:
en: 18f9c56e10a44603
zh: b774e6e5f2a5db6d
/deepseek-ai-dsh-client-ui-deliverables/model-experience/desktop-file-references/what-the-model-sees:
en: 635d8304709cfbc9
zh: 8ee33c6046743fb7
/deepseek-ai-dsh-client-ui-deliverables/model-experience/desktop-file-references/token-effect:
en: 2f1bfc1df1d8d8f3
zh: 4af1b4599f3d6a6a
/deepseek-ai-dsh-client-ui-deliverables/model-experience/desktop-file-references/kv-cache-effect:
en: 4dc7674aa58029a5
zh: 8b605cd009e60775
/deepseek-ai-dsh-client-ui-deliverables/known-limitations-and-deferred-work:
en: dcebaf6fd7885452
zh: 017ef058d4dcdc25
+6 -6
View File
@@ -34,7 +34,7 @@ Changed-file and delivery entry cards and hover previews follow the [R16 preview
The `present` tool's preparing stage is one non-expandable document-icon row. It does not show filenames, create delivery cards, or enable file actions before the recorded call supplies arguments.
The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files accessible through the Session filesystem, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../deliverables/tool-present/README.md) owns file-count limits and Session declarations. A single delivery fills the row; multiple deliveries form a two-column grid, collapsed after four files. Cards show the basename, description or file type, and a shared native opening control. Delivery and changed-file icon tiles use white at 50% opacity in the light theme and 5% in the dark theme. Icon tiles and single-file changed cards use the same border as delivery cards. Clicking the card previews the file in the right Sidebar. The control opens the default application and lists associated applications, with file reveal last. Matching inline-code references also open Sidebar previews. Duplicate declarations use the last description before the closing reply.
The Web `standard`, `ptc`, and `cordis` presets expose `present` for final files accessible through the Session filesystem, including files created through Bash. Call it with `files: [{ path, description? }]` after creating the files. The [present tool](../../deliverables/deliverable-file-references/README.md) owns file-count limits and Session declarations. A single delivery fills the row; multiple deliveries form a two-column grid, collapsed after four files. Cards show the basename, description or file type, and a shared native opening control. Delivery and changed-file icon tiles use white at 50% opacity in the light theme and 5% in the dark theme. Icon tiles and single-file changed cards use the same border as delivery cards. Clicking the card previews the file in the right Sidebar. The control opens the default application and lists associated applications, with file reveal last. Matching inline-code references also open Sidebar previews. Duplicate declarations use the last description before the closing reply.
The `present` tool row retains its document icon and recorded result across progress, success, failure, and interruption. Native actions share pending state and publish progress or retryable errors on the card. Successful open or reveal feedback stays visible for five seconds, fades over 200ms, then restores the file description; failures remain visible until another attempt. Desktop metadata is loaded when cards mount and invalidated on connection replacement. Without a Host desktop, cards retain Sidebar preview but omit native controls; failed metadata reads offer Retry. Delivery cards and change review expose `deliverables.file.actions` and `deliverables.review.file.actions`, supplied by [ui-open-in-app](../ui-open-in-app/README.md). Association GET requests and native POST actions both validate the recorded file coordinates and Session filesystem mapping before using the Host desktop.
@@ -64,7 +64,7 @@ The closing prose links produced or delivered paths: an inline-code token resolv
<details>
<summary>Implementation internals — click to expand</summary>
The Node half registers the static `ui:deliverable-file-references` system-prompt section described under [Model Experience](#model-experience). Explicit Markdown links use the shared [Markdown renderer](../ui-primitives/README.md); inline-code matching remains limited to produced or delivered files. The browser half registers a wrapper around the changed-files card and explicit deliveries into the chat view's `conversation.chat.turnTail` list alongside other feature artifacts. `deliverablesDefinition` folds the sequence of each Turn's latest validated `workspace/changes` announcement into `DeliverablesTurnData.changes`, whose summary the card reads from the Host and caches until the connection is replaced, its `deliverables/presented` events into deliveries, and the successful first-party mutation calls of `write`, `edit`, and mutating `str_replace_editor` commands into recorded file targets and exact argument aliases after validating their raw arguments; the produced paths feed only the prose mention resolver. Successful absolute `meta.path` values identify those targets. Root results without that field retain the legacy argument fallback; malformed relative targets contribute no guessed path. Reads, deletes, unsupported tools, malformed calls, malformed events, and failed results contribute nothing. Each row opens `dsh-resource://changes-review/session/<sessionId>/<seq>/<turn>` through `ctx.sidebarRight.openResource` with the file's index as the `changes-review` navigation parameter; the package registers the `changes-review` tab type at the `builtin` band for that pattern and its body under the keyed `sidebar.right.pane.tab` seat with an exclusive store of per-tab choices, and the body reads the summary and the comparisons through the authenticated routes into stores cleared on connection replacement. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes every surface and leaves the view's empty list at zero cost.
The Node half serves authenticated native file opens; the shared guidance comes from the separately mounted file-reference plugin described under [Model Experience](#model-experience). Explicit Markdown links use the shared [Markdown renderer](../ui-primitives/README.md); inline-code matching remains limited to produced or delivered files. The browser half registers a wrapper around the changed-files card and explicit deliveries into the chat view's `conversation.chat.turnTail` list alongside other feature artifacts. `deliverablesDefinition` folds the sequence of each Turn's latest validated `workspace/changes` announcement into `DeliverablesTurnData.changes`, whose summary the card reads from the Host and caches until the connection is replaced, its `deliverables/presented` events into deliveries, and the successful first-party mutation calls of `write`, `edit`, and mutating `str_replace_editor` commands into recorded file targets and exact argument aliases after validating their raw arguments; the produced paths feed only the prose mention resolver. Successful absolute `meta.path` values identify those targets. Root results without that field retain the legacy argument fallback; malformed relative targets contribute no guessed path. Reads, deletes, unsupported tools, malformed calls, malformed events, and failed results contribute nothing. Each row opens `dsh-resource://changes-review/session/<sessionId>/<seq>/<turn>` through `ctx.sidebarRight.openResource` with the file's index as the `changes-review` navigation parameter; the package registers the `changes-review` tab type at the `builtin` band for that pattern and its body under the keyed `sidebar.right.pane.tab` seat with an exclusive store of per-tab choices, and the body reads the summary and the comparisons through the authenticated routes into stores cleared on connection replacement. The package also provides the `chatFileMentions` service the chat view consults per closing message; composing the plugin out removes every surface and leaves the view's empty list at zero cost.
Native opening uses an authenticated POST addressed by the viewed Session, event sequence, and original file index; the review tab's native open of a changed file uses the same coordinates. For a declaration the Host reads the viewed Session header with the event and passes its cwd, or the deployment workspace root when absent, to `workspaceFiles.stat`; for a changed file it passes the working directory the served summary carries. This uses the same composed filesystem as Sidebar previews and does not activate an Agent, including for child Sessions. Native actions require the canonical process path to map from a Host path back to that same process path. Providers without this mapping return 422, after which the review tab hides its native open; a same-named Host file is insufficient. The same configured desktop availability governs metadata and execution. Edits affect subsequent opens; deletion returns an error. No file-content copy or attachment is created. Plugin disposal cancels and awaits pending native-open requests.
@@ -89,19 +89,19 @@ Read these pages when the deliverables surface is not enough. They move from the
<a id="model-experience"></a>
## Model Experience
### Clickable file-reference guidance
### Desktop file references
#### What the model sees
The guidance prefers primary results in the final reply: inline images support explanations and comparisons, image links serve references and file lists, and image file cards serve separate deliverables. File destinations use angle brackets, including paths with spaces. It recommends separate `present` cards for complete file deliverables, especially Office files, usually selecting the most important one or two, with at most four files per call when more are needed. It discourages cards solely for code edits and extra commands to inspect diff visibility. This guidance applies even with Coding Tools disabled: it accepts fewer file surfaces to avoid duplicate presentation and unnecessary tool calls. It asks the model to link every existing-file mention outside commands, configuration expressions, and code blocks, including repeats and tables. Labels default to filenames or clear aliases, with only enough parent directories to distinguish files. Precise references display `filename:24` or `filename:24–30`; their destinations retain full relative or absolute paths with `#L24` or `#L24-L30` anchors. The display suffix contains neither `#` nor `L`.
The Web composition mounts the shared [`dsh-deliverable-file-references`](../../deliverables/deliverable-file-references/README.md#model-experience) guidance. This UI package renders the resulting file references and cards.
#### Token effect
One fixed paragraph containing output-selection, rendering, and file-reference guidance whenever this package is loaded. The [present tool](../../deliverables/tool-present/README.md#model-experience) owns the delivery schema and result text.
No direct tokens; the separate prompt plugin owns the guidance.
#### KV Cache effect
The section is static at first-party order 9000 for the lifetime of the package mount, so it remains in the reusable prompt prefix and does not change across Turns.
No direct effect on model requests.
## Known Limitations and Deferred Work
+6 -6
View File
@@ -34,7 +34,7 @@ kind: "package-reference"
`present` 工具的准备阶段是带文档图标的不可展开单行。记录的调用提供参数前,该行不显示文件名、不创建交付卡片,也不启用文件操作。
Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明会话文件系统可访问的最终文件,包括通过 Bash 创建的文件。创建后以 `files: [{ path, description? }]` 调用。[present 工具](../../deliverables/tool-present/README.zh.md)拥有文件数量限制和会话声明。单个交付占满一行,多个交付使用双列网格,超过四个时默认收起。卡片显示文件名、说明或文件类型,以及共享的原生打开控件。交付和改动文件卡片的图标底块在浅色主题下使用 50% 不透明度的白色,深色主题下为 5%。图标底块与单文件改动卡片的外框使用与交付卡片相同的描边。点击卡片会在右侧 Sidebar 预览文件。控件打开默认应用,并列出关联应用,文件定位始终放在最后。匹配的行内代码引用也打开 Sidebar 预览。重复声明使用收尾回复之前最近一次的说明。
Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明会话文件系统可访问的最终文件,包括通过 Bash 创建的文件。创建后以 `files: [{ path, description? }]` 调用。[present 工具](../../deliverables/deliverable-file-references/README.zh.md)拥有文件数量限制和会话声明。单个交付占满一行,多个交付使用双列网格,超过四个时默认收起。卡片显示文件名、说明或文件类型,以及共享的原生打开控件。交付和改动文件卡片的图标底块在浅色主题下使用 50% 不透明度的白色,深色主题下为 5%。图标底块与单文件改动卡片的外框使用与交付卡片相同的描边。点击卡片会在右侧 Sidebar 预览文件。控件打开默认应用,并列出关联应用,文件定位始终放在最后。匹配的行内代码引用也打开 Sidebar 预览。重复声明使用收尾回复之前最近一次的说明。
`present` 工具行在执行、成功、失败和中断状态下保留文档图标及记录的结果。原生操作共享等待状态,并在卡片显示进度或可重试错误。打开或定位成功的反馈显示五秒后,在 200ms 内淡出并恢复文件说明;失败信息保留到下次尝试。卡片挂载时读取桌面信息,连接更换时清除缓存。Host 没有桌面时,卡片保留 Sidebar 预览并隐藏原生控件;元数据读取失败时提供重试。交付卡片和变更对比页通过 `deliverables.file.actions` 和 `deliverables.review.file.actions` 使用 [ui-open-in-app](../ui-open-in-app/README.zh.md) 提供的控件。关联应用 GET 查询与原生 POST 操作都会先校验记录中的文件坐标及会话文件系统映射,再调用 Host 桌面。
@@ -64,7 +64,7 @@ Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明
<details>
<summary>实现细节——点击展开</summary>
Node 半部注册[模型体验](#model-experience)所述的静态 `ui:deliverable-file-references` 系统提示词段。显式 Markdown 链接使用共享的 [Markdown 渲染器](../ui-primitives/README.zh.md);行内代码匹配仍仅限产出或交付文件。浏览器半部把组合改动文件卡片与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 列表,与其他功能产物并列。`deliverablesDefinition` 把每个轮次最新且通过校验的 `workspace/changes` 宣告的序号折叠进 `DeliverablesTurnData.changes`,卡片按它向 Host 读取摘要并缓存到连接被替换为止,把 `deliverables/presented` 事件折叠为交付,并根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数与结果记录的绝对 `meta.path` 把成功的第一方修改调用折叠为产出路径及精确参数别名;产出路径只供正文提及解析器使用。不带该字段的成功根调用保留历史参数回退;格式错误的相对目标不产生猜测路径。读取、删除、不受支持的工具、格式错误的调用、格式错误的事件和失败结果不贡献任何条目。每一行通过 `ctx.sidebarRight.openResource` 打开 `dsh-resource://changes-review/session/<sessionId>/<seq>/<turn>`,并以文件下标作为 `changes-review` 的导航参数;本包在 `builtin` 档为该模式注册 `changes-review` tab 类型,把其 body 连同一个按 tab 保存选择的独占 store 注册到按键的 `sidebar.right.pane.tab` 座位下,body 通过经过认证的路由把摘要和对比读进连接更换时清空的 store。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会移除全部表面,视图的空列表以零成本留下。
Node 半部提供经过身份验证的原生文件打开;共用指导由[模型体验](#model-experience)所述的独立文件引用插件提供。显式 Markdown 链接使用共享的 [Markdown 渲染器](../ui-primitives/README.zh.md);行内代码匹配仍仅限产出或交付文件。浏览器半部把组合改动文件卡片与显式交付的包装组件注册进 chat 视图的 `conversation.chat.turnTail` 列表,与其他功能产物并列。`deliverablesDefinition` 把每个轮次最新且通过校验的 `workspace/changes` 宣告的序号折叠进 `DeliverablesTurnData.changes`,卡片按它向 Host 读取摘要并缓存到连接被替换为止,把 `deliverables/presented` 事件折叠为交付,并根据 `write`、`edit` 和有修改作用的 `str_replace_editor` 命令中经过校验的原始参数与结果记录的绝对 `meta.path` 把成功的第一方修改调用折叠为产出路径及精确参数别名;产出路径只供正文提及解析器使用。不带该字段的成功根调用保留历史参数回退;格式错误的相对目标不产生猜测路径。读取、删除、不受支持的工具、格式错误的调用、格式错误的事件和失败结果不贡献任何条目。每一行通过 `ctx.sidebarRight.openResource` 打开 `dsh-resource://changes-review/session/<sessionId>/<seq>/<turn>`,并以文件下标作为 `changes-review` 的导航参数;本包在 `builtin` 档为该模式注册 `changes-review` tab 类型,把其 body 连同一个按 tab 保存选择的独占 store 注册到按键的 `sidebar.right.pane.tab` 座位下,body 通过经过认证的路由把摘要和对比读进连接更换时清空的 store。本包还提供 chat 视图按收尾消息查询的 `chatFileMentions` 服务;把插件组合出去会移除全部表面,视图的空列表以零成本留下。
原生打开使用经过认证的 POST,通过当前查看的会话、事件序号和原始文件索引定位声明;review tab 对改动文件的原生打开使用同一组坐标。对声明,Host 读取事件及当前查看的会话 header,将其中的 cwd 传给 `workspaceFiles.stat`,未记录 cwd 时使用部署的工作目录;对改动文件,传的是所提供摘要携带的工作目录。它与侧栏预览使用同一组合文件系统,无需启动 Agent,子会话也适用。原生操作要求规范化的进程路径能从 Host 路径映射回同一进程路径。提供方没有这种映射时返回 422,之后 review tab 隐藏原生打开;Host 上存在同名文件并不足够。同一份桌面可用性配置同时约束信息查询和实际执行。编辑会影响后续打开的内容;删除后返回错误。不创建文件内容副本或附件。插件释放时取消并等待进行中的原生打开请求。
@@ -89,19 +89,19 @@ Node 半部注册[模型体验](#model-experience)所述的静态 `ui:deliverabl
<a id="model-experience"></a>
## 模型体验
### 可点击文件引用指引
### 桌面文件引用
#### 模型看到的内容
提示词建议在最终回复中展示主要结果:内嵌图片用于讲解与对比,图片链接用于引用和文件列表,图片文件卡片用于独立交付。文件目标路径使用尖括号包裹,包括含空格的路径。对于需要完整文件的交付,尤其是 Office 文件,建议使用独立的 `present` 卡片,通常选最重要的一两项,需要时可以更多,但每次调用最多四项。提示词建议避免仅为列出代码改动生成卡片,也不为确认 diff 是否显示额外运行命令。代码工作工具关闭时仍采用这条建议,接受较少的文件展示入口,以减少重复展示和不必要的工具调用。文件引用要求链接命令、配置表达式和代码块以外的每次现有文件提及,包括重复提及和表格。标签默认使用文件名或清楚的别名,仅添加足以区分文件的父目录。精确引用显示为 `filename:24` 或 `filename:24–30`;目标保留完整相对路径或绝对路径,以及 `#L24` 或 `#L24-L30` 锚点。显示后缀不含 `#` 或 `L`。
Web 组合挂载共用的 [`dsh-deliverable-file-references`](../../deliverables/deliverable-file-references/README.zh.md#model-experience) 指导。本 UI 包渲染相应的文件引用和卡片。
#### Token 影响
加载本包时增加一段包含产出选择、渲染效果和文件引用指导的固定提示词。[present 工具](../../deliverables/tool-present/README.zh.md#model-experience)拥有交付 schema 和结果文本。
不直接增加 token;指导由单独的 prompt 插件负责。
#### KV Cache 影响
该段落在本包挂载期间始终以 first-party 顺序 9000 保持静态,因此留在可复用的提示词前缀中,不会随轮次改变。
不直接影响模型请求。
## 已知限制与延期工作
+5 -21
View File
@@ -1,36 +1,20 @@
/**
* Deliverables plugin, node half. Registers Web file-reference guidance and
* serves authenticated native opens of declared files. The browser
* Deliverables plugin, node half. Serves authenticated native opens of declared files. The browser
* half ships via exports["./client"], discovered through the package.json
* dsh.client declaration.
*/
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-workspace-changes/types'
import { registerPresentOpen } from './present-open.ts'
/** Services required for file-reference guidance, change summaries, and authenticated native opens. */
export const inject = ['systemPrompt', 'connection', 'sessionQuery', 'sessionController', 'workspaceFiles', 'fs', 'sandboxPolicy', 'workspaceChanges']
/** Static Web guidance for primary outputs and existing-file references. */
const FILE_REFERENCE_PROMPT = 'Prefer showing the primary results within your final response alongside a brief explanation. '
+ 'Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. '
+ 'Do not call present just to list edited source files, or run commands to check whether a diff view will appear. '
+ 'Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. '
+ 'Avoid repeating results already shown inline unless the separate card adds useful access. '
+ 'Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. '
+ 'Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.'
/** Services required for change summaries and authenticated native opens. */
export const inject = ['connection', 'sessionQuery', 'sessionController', 'workspaceFiles', 'fs', 'sandboxPolicy', 'workspaceChanges']
/**
* Register Web file-reference guidance and native opens for declared files.
* @param ctx - host context carrying the system-prompt registry.
* Register native opens for declared files.
* @param ctx - host context carrying file and connection services.
*/
export function apply(ctx: Context): void {
registerPresentOpen(ctx)
ctx.systemPrompt.section({
name: 'ui:deliverable-file-references',
order: ctx.systemPrompt.getSectionOrder('DELIVERABLE_FILE_REFERENCES'),
text: FILE_REFERENCE_PROMPT,
})
}
@@ -12,7 +12,7 @@ import { SessionQueryError } from '@deepseek-ai/dsh-session-query'
import type { SessionEventReadRequest } from '@deepseek-ai/dsh-session-query'
import type { WorkspaceChangedFile, WorkspaceChangesSummary, WorkspaceFileDiff } from '@deepseek-ai/dsh-workspace-changes/types'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { registerPresentOpen } from '../src/present-open.ts'
import * as deliverables from '../src/index.ts'
import {
changedFileUrl, changesDiffUrl, changesSummaryUrl, CHANGES_DIFF_PATH, CHANGES_OPEN_PATH, CHANGED_FILES_PATH, isChangedFile, isChangesDiff,
isChangesEvent, isChangesSummary,
@@ -64,10 +64,7 @@ async function fixture() {
const applications = vi.fn(async () => [{ id: 'player', name: 'Player', default: true, icon: null }])
ctx.provide('sessionController', { workspacePathApplications: applications, openWorkspacePath: opener, workspaceDesktop: () => ({ name: 'desktop', available: true, fileManager: 'finder' }) } as never)
const connection = new HostConnectionService(ctx, [], {} as BrowserAuth)
await ctx.plugin({
inject: ['connection', 'sessionQuery', 'sessionController', 'workspaceFiles', 'fs', 'sandboxPolicy', 'workspaceChanges'],
apply: registerPresentOpen,
})
await ctx.plugin(deliverables)
const handler = connection.createSharedFetchHandler('/api')
const open = (query = '?sessionId=owner&seq=9&index=0') => handler.fetch(new Request(`http://localhost${CHANGES_OPEN_PATH}${query}`, { method: 'POST' }), connection.browserPeer)
const read = (query = '?sessionId=owner&seq=9') => handler.fetch(new Request(`http://localhost${CHANGED_FILES_PATH}${query}`), connection.browserPeer)
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package:
en: 0d126503459846d5
zh: c380b8e975a76913
en: ddd9e45e21755108
zh: 8af470315eecf8a4
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/installing-a-bundle:
en: fcb2e69c8b028c0a
zh: d505a075679974eb
@@ -36,8 +36,8 @@
en: 54d7666c630da471
zh: d7ded257d0fd5559
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/page-tabs:
en: 76e0908903ab8fde
zh: ce6ae1cdd6aea971
en: 25451392e1118869
zh: 8ba84f8fdbb47b3f
/deepseek-ai-dsh-client-ui-plugin-manager/understand-the-implementation:
en: 203168fc87e939bd
zh: 72d0ccd2c54f3f44
@@ -48,8 +48,8 @@
en: 21a0bbe8eb92229c
zh: 096ff63db61992bb
/deepseek-ai-dsh-client-ui-plugin-manager/understand-the-implementation/configuration-slots:
en: c1338ea1bf004954
zh: 8dba2a39e619d3c3
en: 928c1bcaace6ce0c
zh: 64d2aad4f01b324c
/deepseek-ai-dsh-client-ui-plugin-manager/further-exploration:
en: f9a42dfed0360d6a
zh: 455a70bcb6c69624
@@ -60,8 +60,8 @@
en: ca75c51c89c2c9b0
zh: 2064958b0754647e
/deepseek-ai-dsh-client-ui-plugin-manager/known-limitations-and-deferred-work:
en: 344ba1e802163bb4
zh: f3cc2e30ffb037cb
en: 149c0d23a56a4944
zh: e2feaadc871581ae
/deepseek-ai-dsh-client-ui-plugin-manager/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 3bb0c20c93f16f98
+2 -5
View File
@@ -27,7 +27,7 @@ Use the **Plugins** entry in the Web sidebar to manage the profile's installed b
<a id="use-this-package"></a>
## Use this package
Select **Plugins** in the sidebar. The information button uses an 11px caption-colored icon in a 20px square hover area, with a 4px gap after the subtitle. It explains that this page configures official plugins and installs or manages other plugins, while **Settings → Built-in plugins** shows the built-in inventory and runtime status. Hover or focus shows the shared tooltip; clicking keeps it open until another click, Escape, Tab, or an outside click. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Built-in plugin settings** lists official configuration pages by their configured order: Subagent, web search, agent loop, then shell. **Experimental plugins** shows the primary optional bundles, with **More** beside its heading opening a separate page of the same title. That page lists all offered official bundles, including those on the main list, with Codex and Claude Code subagents last. Its back button returns to the main list, where **Installed plugins** remains below Experimental plugins. Bundle details return to the list they were opened from. Catalog entries keep their position after installation or removal. **Installed plugins** lists other profile bundles. Installation-carried optional bundles have no uninstall action. Experimental packages retain the **Experimental** marker on both lists. Bundle details can show a feature preview below the description, also while the bundle is disabled; **DSH badge skill** shows one in English or Chinese following the UI language, and a decode failure shows a short notice without blocking management. Switching on a bundle that page holds back adds it to the main list in the position that page shows it, and switching it off leaves it on that page alone; other bundles keep their place on both. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Built-in plugins** section.
Select **Plugins** in the sidebar. The information button uses an 11px caption-colored icon in a 20px square hover area, with a 4px gap after the subtitle. It explains that this page configures official plugins and installs or manages other plugins, while **Settings → Built-in plugins** shows the built-in inventory and runtime status. Hover or focus shows the shared tooltip; clicking keeps it open until another click, Escape, Tab, or an outside click. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Built-in plugin settings** lists official configuration pages by their configured order: Subagent, web search, agent loop, then shell. **Experimental plugins** shows the primary optional bundles, with **More** beside its heading opening a separate page of the same title. That page lists all offered official bundles, including those on the main list, with Codex and Claude Code subagents last. Its back button returns to the main list, where **Installed plugins** remains below Experimental plugins. Bundle details return to the list they were opened from. Catalog entries keep their position after installation or removal. **Installed plugins** lists other profile bundles. Installation-carried optional bundles have no uninstall action. Experimental packages retain the **Experimental** marker on both lists. Switching on a bundle that page holds back adds it to the main list in the position that page shows it, and switching it off leaves it on that page alone; other bundles keep their place on both. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Built-in plugins** section.
The heading moves its complete action group to another line when space is insufficient. Action labels stay on one line, and the information button follows the subtitle text.
@@ -112,8 +112,6 @@ Another plugin adds a tab after the page's own **Installed** tab by registering
-----
Built-in illustrations follow the [feature preview authoring guide](../../../docs/cookbook/adding-a-plugin-preview.md): optional, one 640×300 SVG per language, with both English and Chinese required. The internal map and its asset test enforce the format; packages have no preview declaration interface.
<a id="understand-the-implementation"></a>
## Understand the implementation
@@ -138,7 +136,7 @@ The add control reuses `Button`, `Menu`, and `MenuItemButton`. Both installation
Custom item pages use the Host entry id as their registration id; row pages use the bundle package and row id. The page owner supplies `form.state` and `form.mutate(operations, expectedRevision)` when the entry exposes editable Config fields. A custom page owns its draft and validation display, and may reuse `ConfigField` from ui-primitives. Bundle-wide pages can contain several entries and have no single form.
The page's `main` registration declares `plugins.item`, `plugins.bundle.config`, and `plugins.row.config` as its children, so the slots exist while the page does and a registrant's `ctx.slots.inject` waits for them. `configLedgerSource` projects the three ledgers into one observable — the official items in ledger order with their labels resolved in the active locale, and the bundle and row keys — cached until a ledger or the locale moves; the page binds it as `useConfigLedger` beside the store and never names a configurable plugin itself. The registration-owned navigation store selects the main list, the full experimental plugin list, a bundle, an official plugin, or a bundle row; switching away from the Plugins panel resets it to the list, while React remounts preserve the selected target. A fresh registration starts on the main list. Installation highlighting scrolls to a card only when it is on the current page; it does not change the selected page. Other Client plugins inject `pluginNavigation` and call `ctx.pluginNavigation.openBundle(packageName)` to open bundle details without changing the current Session. The target survives the initial inventory load; an absent bundle displays the list. A registration lives with the browser half that made it. `dsh-client-modules` attaches a package's browser half to the Loader row whose specifier is the bare package name, so every page a bundle registers, for itself or for any of its rows, goes away when that row is switched off; a sub-plugin whose page must outlive the other rows ships as its own package. Official packages whose names begin with `@deepseek-ai/dsh-experimental-` display the Experimental marker. Feature previews are SVG files under `src/client/assets/`, keyed by package name in the page and embedded in the Client bundle; packages cannot declare their own ([rationale](../../../.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.md)).
The page's `main` registration declares `plugins.item`, `plugins.bundle.config`, and `plugins.row.config` as its children, so the slots exist while the page does and a registrant's `ctx.slots.inject` waits for them. `configLedgerSource` projects the three ledgers into one observable — the official items in ledger order with their labels resolved in the active locale, and the bundle and row keys — cached until a ledger or the locale moves; the page binds it as `useConfigLedger` beside the store and never names a configurable plugin itself. The registration-owned navigation store selects the main list, the full experimental plugin list, a bundle, an official plugin, or a bundle row; switching away from the Plugins panel resets it to the list, while React remounts preserve the selected target. A fresh registration starts on the main list. Installation highlighting scrolls to a card only when it is on the current page; it does not change the selected page. Other Client plugins inject `pluginNavigation` and call `ctx.pluginNavigation.openBundle(packageName)` to open bundle details without changing the current Session. The target survives the initial inventory load; an absent bundle displays the list. A registration lives with the browser half that made it. `dsh-client-modules` attaches a package's browser half to the Loader row whose specifier is the bare package name, so every page a bundle registers, for itself or for any of its rows, goes away when that row is switched off; a sub-plugin whose page must outlive the other rows ships as its own package. Official packages whose names begin with `@deepseek-ai/dsh-experimental-` display the Experimental marker.
`plugins.bundle.config` supplies bundle detail configuration, keyed by npm package name. `plugins.bundle.activation` offers bundle-owned guidance after explicit enablement from the list, with callbacks to dismiss it or open the bundle details. It does not appear merely because an enabled bundle was listed.
@@ -169,7 +167,6 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- Feature previews have one image per language and no dark-theme variant, so the badge skill preview keeps its light background in dark theme.
- Ping response time does not measure package-download throughput. Probes use the Host fetch route, which pnpm-specific proxy settings can differ from; users can always choose the registry manually, and installation retains its existing fallback rules.
<a id="known-limitations-and-deferred-work"></a>
@@ -27,7 +27,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
在侧栏选择**插件**。信息按钮使用 11px 的 caption 色图标和 20px 方形悬停区域,与副标题间隔 4px。它说明当前页用于配置官方插件、安装和管理其他插件,**设置 → 内置插件**则展示内置插件列表及运行状态。悬停或聚焦显示共享 tooltip;点击后保持展开,直至再次点击、Escape、Tab 或外部点击。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**内置插件配置**按配置的顺序列出官方配置页:子智能体、网页搜索、Agent 循环、终端。**实验性插件**展示主要的可选组合包,标题旁的**更多**打开同名的独立页面。该页列出全部官方组合包,也包含主列表已有的条目,Codex 和 Claude Code 子智能体排在最后。返回按钮回到主列表,**已安装插件**保持在实验性插件下方。组合包详情返回打开它的列表。目录条目在安装或移除后保持原位置。**已安装插件**列出其他 profile 组合包。安装随附的可选组合包没有卸载操作。实验性包在两个列表中均保留**实验性**标签。组合包详情可在说明下方展示功能配图,未启用时也可查看;**DSH 徽章技能**按界面语言展示中文或英文配图,解码失败时显示简短提示,不影响管理操作。开启仅在该页列出的组合包会按该页中的位置把它加入主列表,关闭后它只留在该页;其他组合包在两个列表中均保持原位。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**内置插件**分区中编辑。
在侧栏选择**插件**。信息按钮使用 11px 的 caption 色图标和 20px 方形悬停区域,与副标题间隔 4px。它说明当前页用于配置官方插件、安装和管理其他插件,**设置 → 内置插件**则展示内置插件列表及运行状态。悬停或聚焦显示共享 tooltip;点击后保持展开,直至再次点击、Escape、Tab 或外部点击。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**内置插件配置**按配置的顺序列出官方配置页:子智能体、网页搜索、Agent 循环、终端。**实验性插件**展示主要的可选组合包,标题旁的**更多**打开同名的独立页面。该页列出全部官方组合包,也包含主列表已有的条目,Codex 和 Claude Code 子智能体排在最后。返回按钮回到主列表,**已安装插件**保持在实验性插件下方。组合包详情返回打开它的列表。目录条目在安装或移除后保持原位置。**已安装插件**列出其他 profile 组合包。安装随附的可选组合包没有卸载操作。实验性包在两个列表中均保留**实验性**标签。开启仅在该页列出的组合包会按该页中的位置把它加入主列表,关闭后它只留在该页;其他组合包在两个列表中均保持原位。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**内置插件**分区中编辑。
空间不足时,页头将整个操作组移到下一行。操作文案保持单行,信息按钮紧随副标题文字。
@@ -112,8 +112,6 @@ ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({
-----
内置配图遵循[功能配图创作指引](../../../docs/cookbook/adding-a-plugin-preview.zh.md):可选,每种语言一张 640×300 SVG,必须同时提供英文和中文。内部映射及素材测试约束格式,包没有配图声明接口。
<a id="understand-the-implementation"></a>
## 理解实现
@@ -138,7 +136,7 @@ Host 入口通过生成的 Remote 接口暴露 `pluginRegistryProbe.fastest()`
自定义条目页以 Host 条目 id 作为注册 id;行页面使用 bundle 包名和行 id。当条目提供可编辑 Config 字段时,页面宿主传入 `form.state` 和 `form.mutate(operations, expectedRevision)`。自定义页面负责草稿和校验提示,并可复用 ui-primitives 的 `ConfigField`。整个 bundle 的页面可以包含多个条目,因此没有单一表单。
页面的 `main` 注册把 `plugins.item`、`plugins.bundle.config` 与 `plugins.row.config` 声明为子 slot,因此它们与页面同生,注册方的 `ctx.slots.inject` 会等到它们出现。`configLedgerSource` 把三份账本投影成一个可观察对象——按账本顺序排列、标签按当前语言解析的官方条目,以及组合包与行的键——在账本或语言变化前保持缓存;页面把它作为 `useConfigLedger` 绑在 store 旁边,自身从不点名任何可配置插件。注册拥有的导航 store 选择主列表、完整的实验性插件列表、某个组合包、某个官方插件或组合包的某一行;切换离开插件面板时重置为列表,React 重新挂载则保留所选目标。新的注册从主列表开始。安装后的高亮仅在当前页面有对应卡片时滚动定位,不改变所选页面。其他 Client 插件注入 `pluginNavigation`,调用 `ctx.pluginNavigation.openBundle(packageName)` 即可打开组合包详情,不改变当前 Session。首次读取清单期间保留导航目标;组合包不存在时显示列表。注册与做出它的浏览器半侧同生共死。`dsh-client-modules` 只把一个包的浏览器半侧挂在说明符恰为包名的那一行 Loader 行上,所以组合包为自己或任一行注册的页面,都会在那一行被关闭时一起消失;需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。 名称以 `@deepseek-ai/dsh-experimental-` 开头的官方包显示实验性标记。功能配图是 `src/client/assets/` 下的 SVG 文件,由页面按包名指定并嵌入 Client bundle;包不能自行声明配图([决策依据](../../../.agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.zh.md))。
页面的 `main` 注册把 `plugins.item`、`plugins.bundle.config` 与 `plugins.row.config` 声明为子 slot,因此它们与页面同生,注册方的 `ctx.slots.inject` 会等到它们出现。`configLedgerSource` 把三份账本投影成一个可观察对象——按账本顺序排列、标签按当前语言解析的官方条目,以及组合包与行的键——在账本或语言变化前保持缓存;页面把它作为 `useConfigLedger` 绑在 store 旁边,自身从不点名任何可配置插件。注册拥有的导航 store 选择主列表、完整的实验性插件列表、某个组合包、某个官方插件或组合包的某一行;切换离开插件面板时重置为列表,React 重新挂载则保留所选目标。新的注册从主列表开始。安装后的高亮仅在当前页面有对应卡片时滚动定位,不改变所选页面。其他 Client 插件注入 `pluginNavigation`,调用 `ctx.pluginNavigation.openBundle(packageName)` 即可打开组合包详情,不改变当前 Session。首次读取清单期间保留导航目标;组合包不存在时显示列表。注册与做出它的浏览器半侧同生共死。`dsh-client-modules` 只把一个包的浏览器半侧挂在说明符恰为包名的那一行 Loader 行上,所以组合包为自己或任一行注册的页面,都会在那一行被关闭时一起消失;需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。 名称以 `@deepseek-ai/dsh-experimental-` 开头的官方包显示实验性标记。
`plugins.bundle.config` 以 npm 包名为 key,提供 Bundle 详情配置。`plugins.bundle.activation` 在用户从列表显式启用后提供 Bundle 自有引导,并传入关闭引导和打开详情的回调。仅列出已启用的 Bundle 不会触发引导。
@@ -169,7 +167,6 @@ Host 入口通过生成的 Remote 接口暴露 `pluginRegistryProbe.fastest()`
## 已知限制与延期工作
- 功能配图每种语言只有一张,没有深色主题版本,因此徽章技能配图在深色主题中仍为浅色背景。
- Ping 响应时间不代表包下载吞吐量。探测使用 Host fetch 的网络路径,pnpm 专用代理设置可能使用不同路径;用户始终可以手动选择安装源,安装仍遵循现有回退规则。
<a id="known-limitations-and-deferred-work"></a>
@@ -1309,17 +1309,6 @@ label.registryOption {
margin-top: 12px;
}
.packagePreview {
display: block;
align-self: flex-start;
max-width: 100%;
max-height: 320px;
margin-top: 24px;
border: 0.5px solid var(--dsw-alias-border-l4);
border-radius: var(--dsw-radius-lg);
object-fit: contain;
}
.detailSections {
display: flex;
flex-direction: column;
@@ -35,10 +35,6 @@ import type { ConfigPageForm } from './slot-contract.ts'
import { UpdateDialog, VersionSection } from './versions.tsx'
import { BundleDetailSection } from './BundleDetailSection.tsx'
import css from './PluginManagerPage.module.css'
import badgeSkillPreview from './assets/badge-skill-preview.svg'
import badgeSkillPreviewZh from './assets/badge-skill-preview.zh.svg'
import voiceInputPreview from './assets/voice-input-preview.svg'
import voiceInputPreviewZh from './assets/voice-input-preview.zh.svg'
/** Full component props assembled by the main slot renderer. */
export type PluginManagerPageProps =
@@ -161,12 +157,6 @@ function orderOptionalBundles(packages: readonly PackageView[]): PackageView[] {
return [...packages].sort((a, b) => optionalBundleRank(a.name) - optionalBundleRank(b.name))
}
/** Feature previews of official bundles, shown on their detail pages and selected by UI language. */
const BUNDLE_PREVIEWS = new Map<string, Readonly<Record<'en' | 'zh', string>>>([
['@deepseek-ai/dsh-experimental-badge-skill-bundle', { en: badgeSkillPreview, zh: badgeSkillPreviewZh }],
['@deepseek-ai/dsh-experimental-voice-input-bundle', { en: voiceInputPreview, zh: voiceInputPreviewZh }],
])
/** How long a toast holds: long enough to read a failure that names what broke. */
function toastHoldMs(text: string): number {
return Math.min(8_000, Math.max(3_000, text.length * 80))
@@ -250,14 +240,6 @@ function PackageArtwork({ src, row = false, size = row ? ROW_ARTWORK_SIZE : CARD
: <img className={css.packageImage} src={src} width={size} height={size} alt="" onError={() => { setFailedSource(src) }} />
}
/** A bundle's feature preview; a failed decode is reported in place without blocking management. */
function PackagePreview({ src, title, t }: { readonly src: string; readonly title: string; readonly t: Translate }): ReactNode {
const [failedSource, setFailedSource] = useState<string>()
return src === failedSource
? <p className={css.reason} role="status">{t('previewUnavailable')}</p>
: <img className={css.packagePreview} src={src} alt={t('previewAlt', { name: title })} onError={() => { setFailedSource(src) }} />
}
/** A row's switch: locked, saying why, when the Host refuses to address the row through the profile patch. */
function RowSwitch({ row, title, t, busy, onChange }: {
readonly row: PackageRow
@@ -749,8 +731,6 @@ function PackageDetail({
const { title, description, beta } = packageText(pkg, resolveText)
const newer = versionUpdate(versions)
const status = packageStatus(pkg)
const previews = BUNDLE_PREVIEWS.get(pkg.name)
const preview = previews === undefined ? undefined : resolveText(previews)
const subject: PluginsSubject = { kind: 'bundle', pkg: packageRef(pkg) }
return (
<div className={css.detail} data-plugin-detail={pkg.name}>
@@ -809,7 +789,6 @@ function PackageDetail({
<MetadataError error={pkg.meta?.error} t={t} />
{pkg.error === undefined ? null : <p className={css.reason} role="status">{t('reasonLabel')}: {managementText(pkg.error, t)}</p>}
{pkg.readOnlyReason === undefined ? null : <p className={css.reason} role="status">{managementText({ code: pkg.readOnlyReason }, t)}</p>}
{preview === undefined ? null : <PackagePreview src={preview} title={title} t={t} />}
<div className={css.detailSections}>
{detail}
{configured
@@ -1,5 +0,0 @@
/** Illustration imports are embedded in the plugin manager client bundle. */
declare module '*.svg' {
const url: string
export default url
}
@@ -41,8 +41,6 @@ export const zh = {
statusBeta: '实验性',
reasonLabel: '原因',
metadataError: '包元信息错误:{error}',
previewAlt: '{name}功能预览',
previewUnavailable: '无法显示功能配图',
versionTag: 'v{version}',
partsLabel: '包含的组件',
sourceTitle: '来源信息',
@@ -334,8 +332,6 @@ export const en = {
statusBeta: 'Experimental',
reasonLabel: 'Reason',
metadataError: 'Package metadata error: {error}',
previewAlt: '{name} feature preview',
previewUnavailable: 'Could not display feature preview',
versionTag: 'v{version}',
partsLabel: 'Components',
sourceTitle: 'Source',
@@ -840,47 +840,6 @@ describe('PluginManagerPage', () => {
expect(detailImage.width).toBe(36)
})
it('shows a known bundle preview in its details before activation, follows the UI language, and survives decode failure', () => {
const name = '@deepseek-ai/dsh-experimental-badge-skill-bundle'
const { actions, setLanguage } = renderTab({ packages: [pkg({ name, enabled: false }), pkg()] })
expect(screen.queryByRole('img')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: `View ${name}` }))
const english = screen.getByRole('img', { name: en.previewAlt.replace('{name}', name) }).getAttribute('src')
expect(english).toMatch(/badge-skill-preview\.svg$/)
setLanguage(zh)
const chinese = screen.getByRole('img', { name: zh.previewAlt.replace('{name}', name) })
expect(chinese.getAttribute('src')).toMatch(/badge-skill-preview\.zh\.svg$/)
fireEvent.error(chinese)
expect(screen.queryByRole('img')).toBeNull()
expect(screen.getByText(zh.previewUnavailable).getAttribute('role')).toBe('status')
fireEvent.click(screen.getByRole('switch', { name: zh.enableToggle.replace('{name}', name) }))
expect(actions.setEnabled).toHaveBeenCalledWith(name, true)
setLanguage(en)
expect(screen.getByRole('img').getAttribute('src')).toBe(english)
expect(screen.queryByText(en.previewUnavailable)).toBeNull()
})
it('shows no preview for bundles without one', () => {
renderTab({ packages: [pkg()] })
fireEvent.click(screen.getByRole('button', { name: 'View dsh-better-sidebar' }))
expect(screen.queryByRole('img')).toBeNull()
})
it('places the new voice illustration after its description and switches the single image with the UI language', () => {
const name = '@deepseek-ai/dsh-experimental-voice-input-bundle'
const description = 'Turn speech into local notes.'
const { setLanguage } = renderTab({ packages: [pkg({ name, enabled: false, meta: { description }, rows: [row()] })] })
fireEvent.click(screen.getByRole('button', { name: `View ${name}` }))
const image = screen.getByRole('img')
expect(image.getAttribute('src')).toMatch(/voice-input-preview\.svg$/u)
expect(screen.getByText(description).compareDocumentPosition(image) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(image.compareDocumentPosition(screen.getByRole('heading', { name: en.partsLabel })) & Node.DOCUMENT_POSITION_FOLLOWING)
.toBeTruthy()
setLanguage(zh)
expect(screen.getAllByRole('img')).toHaveLength(1)
expect(screen.getByRole('img').getAttribute('src')).toMatch(/voice-input-preview\.zh\.svg$/u)
})
it('shows metadata diagnostics without blocking management or displaying legacy descriptions', () => {
const error = 'locale/zh.json: invalid title'
const { actions, set, setLanguage } = renderTab({
@@ -10,9 +10,11 @@
/* A long README scrolls inside the section instead of pushing the page's later sections out of reach. */
max-height: min(560px, 60vh);
overflow-y: auto;
padding: 4px 16px;
border: 0.5px solid var(--dsw-alias-border-l1);
border-radius: var(--dsw-radius-lg);
/* Horizontal rules separate the document from its neighbors; a full frame would clash with the images' own corners. */
border-block: 0.5px solid var(--dsw-alias-border-l1);
/* Keep the scrollbar off the text and the text width independent of overflow. */
scrollbar-gutter: stable;
padding: 12px 0;
overflow-wrap: anywhere;
}
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 405b30feaf48c087
/deepseek-ai-dsh-client-ui-schedule/use-this-package:
en: edba0e1573f3ce42
zh: 77875b469d94cc62
en: 7d44cd1640bebfad
zh: 1d6b6c28a3c27115
/deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source:
en: 6bd6882dbbbefcb0
zh: 03aa15f1a65d13dc
+1 -1
View File
@@ -25,7 +25,7 @@ Manage active and inactive tasks from the Automation tasks page and the right Si
<a id="use-this-package"></a>
## Use this package
The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state keeps the heading's New action beside the notice. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation.
The shipped Web composition mounts `ui-schedule` with the Host Schedule service. The clock context that supplies the model's current time is preset-owned: `standard`, `cordis`, and `ptc` declare it, and `minimal` declares neither it nor the reminder tools. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state shows a decorative document-and-clock illustration above its notice and New action, with colors that follow the light or dark theme. Selecting an active or inactive task opens a second detail level with Rules and Records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. When the strip cannot fit the tab list beside the labelled entry, the entry becomes an icon button with the same button style as More task actions, with the Session chat icon and a Linked session tooltip that also shows while the entry is disabled; the entry keeps focus when it switches layouts; the tab list scrolls without a visible scrollbar. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation.
The page header keeps the title at its natural width and moves New to another line when the two no longer fit together. The action label stays on one line.
+1 -1
View File
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
随发行版交付的 Web 组合把 `ui-schedule` 与 Host Schedule 服务一起挂载。为模型提供当前时间的时钟上下文归 preset 所有:`standard`、`cordis` 与 `ptc` 声明它,`minimal` 既不声明它,也不声明提醒工具。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示和“新建”操作上方显示装饰性的文档与时钟插画,配色跟随浅色或深色主题。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。页签条放不下页签列表和带文字的入口时,入口收为与“更多任务操作”同样式的图标按钮,使用会话对话图标,悬停提示“关联会话”,入口禁用时也会显示;切换形态时保留焦点;页签列表可滚动但不显示滚动条。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
页头按标题的自然宽度排布;标题和“新建”无法并排容纳时,将按钮移到下一行。操作文案保持单行。
@@ -0,0 +1,37 @@
/** Decorative document-and-clock artwork with theme-owned colors. */
interface ScheduleEmptyIllustrationProps {
readonly className?: string | undefined
}
/**
* Render the empty-state illustration without adding an accessible announcement.
* @param props - optional CSS class applied to the root SVG.
* @returns the decorative inline SVG element.
*/
export function ScheduleEmptyIllustration({ className }: ScheduleEmptyIllustrationProps) {
return (
<svg className={className} width="170" height="170" viewBox="0 0 170 170" fill="none" aria-hidden="true" focusable="false">
<path d="M163.819 97.9382C163.819 126.572 144.48 135.329 115.847 135.329C87.2129 135.329 60.1276 126.572 60.1276 97.9382C60.1276 69.3047 101.003 46.0927 129.637 46.0927C158.27 46.0927 163.819 69.3047 163.819 97.9382Z" fill="var(--dsw-alias-illustration-background)" />
<path d="M9.49521 88.6735C20.8278 111.218 39.5801 110.429 62.2137 99.0519C84.8474 87.6748 102.791 70.018 91.4589 47.4731C80.1263 24.9282 38.6291 22.8935 15.9955 34.2706C-6.63818 45.6478 -1.83735 66.1286 9.49521 88.6735Z" fill="var(--dsw-alias-illustration-background)" />
<path d="M25.2636 34.442C24.727 29.8132 28.0445 25.6258 32.6733 25.0893L101.373 17.126C106.002 16.5895 110.189 19.9069 110.726 24.5358L120.107 105.466C120.643 110.095 117.326 114.282 112.697 114.818L43.9972 122.782C39.3684 123.318 35.181 120.001 34.6445 115.372L25.2636 34.442Z" fill="var(--dsw-alias-illustration-backing)" />
<path d="M40.169 25.1368C40.169 20.477 43.9466 16.6995 48.6064 16.6995H117.766C122.426 16.6995 126.204 20.477 126.204 25.1368V106.609C126.204 111.268 122.426 115.046 117.766 115.046H48.6064C43.9466 115.046 40.169 111.268 40.169 106.609V25.1368Z" fill="var(--dsw-alias-illustration-surface)" />
<path d="M110.258 44.9883V50.3516H72.0831V44.9883H110.258Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M110.258 63.2803V68.6436H72.0831V63.2803H110.258Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M91.171 81.5713V86.9346H72.0831V81.5713H91.171Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M56.1124 43.4998H64.0964V51.4838H56.1124V43.4998Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M56.1124 61.97H64.0964V69.954H56.1124V61.97Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M56.1124 80.2607H64.0964V88.2448H56.1124V80.2607Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M126.205 148.002C126.205 149.615 105.321 150.923 79.5592 150.923C53.7973 150.923 32.9132 149.615 32.9132 148.002C32.9132 146.388 53.7973 145.08 79.5592 145.08C105.321 145.08 126.205 146.388 126.205 148.002Z" fill="var(--dsw-alias-illustration-shadow)" />
<path d="M13.1544 119.318C14.7101 119.318 16.2657 119.43 17.8214 119.656C17.9942 119.682 18.1671 119.709 18.3399 119.737V120.238C18.1671 120.266 17.9942 120.293 17.8214 120.319C16.2657 120.545 14.7101 120.657 13.1544 120.657C11.5986 120.657 10.0423 120.545 8.48643 120.319C8.31357 120.293 8.14073 120.266 7.96787 120.238V119.737C8.14073 119.709 8.31357 119.682 8.48643 119.656C10.0423 119.43 11.5986 119.318 13.1544 119.318Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M125.197 2.96338C126.287 2.96338 127.378 3.076 128.468 3.30225C128.589 3.32738 128.71 3.3544 128.831 3.38232V3.88428C128.71 3.9122 128.589 3.93922 128.468 3.96436C127.378 4.1906 126.287 4.30322 125.197 4.30322C124.108 4.30319 123.018 4.19057 121.928 3.96436C121.807 3.93922 121.686 3.91221 121.565 3.88428V3.38232C121.686 3.3544 121.807 3.32738 121.928 3.30225C123.018 3.07603 124.108 2.96341 125.197 2.96338Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M145.187 32.6399C146.742 32.6399 148.298 32.7525 149.854 32.9788C150.026 33.0039 150.199 33.0309 150.372 33.0588V33.5608C150.199 33.5887 150.026 33.6157 149.854 33.6409C148.298 33.8671 146.742 33.9797 145.187 33.9797C143.631 33.9797 142.074 33.8671 140.519 33.6409C140.346 33.6157 140.173 33.5887 140 33.5608V33.0588C140.173 33.0309 140.346 33.0039 140.519 32.9788C142.074 32.7525 143.631 32.6399 145.187 32.6399Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M12.4776 119.987C12.4777 118.431 12.5903 116.875 12.8165 115.32C12.8416 115.147 12.8687 114.974 12.8966 114.801H13.3985C13.4265 114.974 13.4535 115.147 13.4786 115.32C13.7048 116.875 13.8175 118.431 13.8175 119.987C13.8175 121.542 13.7049 123.099 13.4786 124.654C13.4535 124.827 13.4265 125 13.3985 125.173H12.8966C12.8687 125 12.8416 124.827 12.8165 124.654C12.5903 123.099 12.4776 121.542 12.4776 119.987Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M124.522 3.63379C124.522 2.54381 124.634 1.45326 124.86 0.363281C124.886 0.242199 124.913 0.121083 124.941 2.19411e-08L125.442 0C125.47 0.121082 125.497 0.242199 125.523 0.363281C125.749 1.45326 125.861 2.54381 125.861 3.63379C125.861 4.72362 125.749 5.81349 125.523 6.90332C125.497 7.02442 125.47 7.1455 125.442 7.2666H124.941C124.913 7.1455 124.886 7.02442 124.86 6.90332C124.634 5.81349 124.522 4.72362 124.522 3.63379Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M144.509 33.3101C144.509 31.7544 144.622 30.1987 144.848 28.6431C144.873 28.4702 144.9 28.2974 144.928 28.1245H145.43C145.458 28.2974 145.485 28.4702 145.51 28.6431C145.736 30.1987 145.849 31.7544 145.849 33.3101C145.849 34.8659 145.736 36.4222 145.51 37.978C145.485 38.1509 145.458 38.3237 145.43 38.4966H144.928C144.9 38.3237 144.873 38.1509 144.848 37.978C144.622 36.4222 144.509 34.8659 144.509 33.3101Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M149.05 98.8527C149.05 112.66 137.857 123.853 124.05 123.853C110.242 123.853 99.0496 112.66 99.0496 98.8527C99.0496 85.0455 110.242 73.8527 124.05 73.8527C137.857 73.8527 149.05 85.0455 149.05 98.8527Z" fill="var(--dsw-alias-illustration-detail)" />
<path d="M144.192 98.8529C144.192 109.978 135.174 118.997 124.049 118.997C112.923 118.997 103.905 109.978 103.905 98.8529C103.905 87.7278 112.923 78.709 124.049 78.709C135.174 78.709 144.192 87.7278 144.192 98.8529Z" fill="var(--dsw-alias-illustration-inset)" />
<path d="M125.124 86.7281L125.123 99.2824L134.923 104.132L132.824 108.375L122.76 103.395C121.309 102.677 120.39 101.197 120.39 99.5782L120.389 86.7275L125.124 86.7281Z" fill="var(--dsw-alias-illustration-detail)" />
</svg>
)
}
@@ -388,6 +388,14 @@
color: var(--dsw-alias-label-caption);
}
.emptyIllustration {
flex: none;
width: 110px;
max-width: 100%;
height: auto;
margin-bottom: 12px;
}
/* The mock's `.empty h3 + .btn`: the empty state's action keeps the mock's 16px
above. */
.emptyAction {
@@ -13,6 +13,7 @@ import { TaskDetail, useTaskDetail, type TaskDetailInjected } from './TaskDetail
import { useRelativeClock } from './relative-clock.ts'
import { formatScheduleFrequency, nextRunParts, taskName, zoneLabel } from './schedule-format.ts'
import type { FrequencyZone } from './schedule-format.ts'
import { ScheduleEmptyIllustration } from './ScheduleEmptyIllustration.tsx'
import css from './TaskManagerPage.module.css'
/** Injected catalog and task actions for the management page. */
@@ -151,7 +152,7 @@ export function TaskManagerPage(props: TaskManagerPageProps) {
<div className={css.list}>
{selected === undefined && <CatalogFeedback status={status} populated={rows.length > 0} onRetry={onRetry} t={t} />}
{status === 'ready' && rows.length === 0 && <div className={css.empty} role="status">
<IconClockOutlineRegular size={24} className={css.emptyGlyph} />
<ScheduleEmptyIllustration className={css.emptyIllustration} />
<h2>{t(emptyTitle)}</h2>
<Button variant="outline" className={css.emptyAction} onClick={onNewTask}>
{t('empty.action')}
@@ -891,6 +891,24 @@ describe('Task manager catalog', () => {
expect(screen.getByRole('button', { name: en['statusFilter.all'] }).getAttribute('aria-pressed')).toBe('true')
})
it('renders a decorative document-and-clock illustration in an empty catalog', () => {
mount({ records: [], status: 'ready' })
const illustration = screen.getByRole('status').querySelector('svg')
expect(illustration?.getAttribute('aria-hidden')).toBe('true')
expect(illustration?.getAttribute('focusable')).toBe('false')
expect(illustration?.getAttribute('viewBox')).toBe('0 0 170 170')
const paths = illustration?.querySelectorAll('path')
expect(paths).toHaveLength(20)
// Roles follow the approved artwork's paint order, including overlapping paper and clock layers.
expect(paths?.[0]?.getAttribute('fill')).toBe('var(--dsw-alias-illustration-background)')
expect(paths?.[2]?.getAttribute('fill')).toBe('var(--dsw-alias-illustration-backing)')
expect(paths?.[3]?.getAttribute('fill')).toBe('var(--dsw-alias-illustration-surface)')
expect(paths?.[10]?.getAttribute('fill')).toBe('var(--dsw-alias-illustration-shadow)')
expect(paths?.[17]?.getAttribute('fill')).toBe('var(--dsw-alias-illustration-detail)')
expect(paths?.[18]?.getAttribute('fill')).toBe('var(--dsw-alias-illustration-inset)')
expect(emptyNewTaskButton().textContent).toBe(en['empty.action'])
})
it('distinguishes an empty catalog, loading, and a failed read', () => {
const h = mount({ records: [], status: 'loading' })
expect(screen.getByRole('status', { name: en['list.loading'] }).textContent).toBe('')
@@ -3761,6 +3779,10 @@ describe('Mock detail metrics shared with the task list', () => {
expect(heading).toMatch(/font-size:\s*14px;/)
expect(heading).toMatch(/font-weight:\s*400;/)
expect(rule('emptyGlyph')).toMatch(/margin-bottom:\s*12px;/)
expect(rule('emptyIllustration')).toMatch(/width:\s*110px;/)
expect(rule('emptyIllustration')).toMatch(/max-width:\s*100%;/)
expect(rule('emptyIllustration')).toMatch(/height:\s*auto;/)
expect(rule('emptyIllustration')).toMatch(/margin-bottom:\s*12px;/)
// The mock's `.empty h3 + .btn`.
expect(rule('emptyAction')).toMatch(/margin-top:\s*16px;/)
})
+2 -2
View File
@@ -33,8 +33,8 @@
en: 3eb9b0e3365b2db3
zh: c73acc55dd897266
/deepseek-ai-dsh-client-ui-theme/understand-the-implementation/stylesheets:
en: 2da5b2af3a965090
zh: 883c91afc6bf105d
en: c47fd726c865cdd3
zh: 7c62b6e5857c29a7
/deepseek-ai-dsh-client-ui-theme/understand-the-implementation/scrollbar-rebinding:
en: bf76e7c34a331595
zh: 5accd0260ef7c61e
+2
View File
@@ -69,6 +69,8 @@ In pointer modality, `html[data-input-modality='pointer'] body :focus-visible:no
Menu icons use `--dsw-alias-menu-icon`: neutral-bluish 800 in light mode and `label-primary-dimmed` in dark mode.
The `--dsw-alias-illustration-*` tokens separate layered decorative artwork backgrounds, foreground surfaces, backing layers, shadows, inset fills, and details. Clock faces use the inset fill. The dark palette keeps foreground surfaces brighter than backing layers and backgrounds; inset fills and shadows share the backing tone.
`base.css` suppresses only the outline of focused elements marked `data-dsh-automatic-focus` by the [primitive focus helper](../ui-primitives/README.md); ordinary keyboard focus styling, borders, shadows, and error states remain intact.
System toasts use `--dsw-alias-toast-bg` and `--dsw-alias-toast-label` for a shared background and text color across callers. Document previews pair `--dsw-alias-bg-document-preview` with `--dsw-alias-label-document-preview` so the backdrop and status text follow the same theme. Tooltip keycaps use `--dsw-alias-tooltip-key-bg`, a lighter fill derived from the tooltip background in each palette. Switch thumbs read `--dsw-alias-switch-thumb`: white in light mode and neutral-bluish 400 in dark mode, so an off switch stays lighter than its track without the glare of pure white.
+2
View File
@@ -69,6 +69,8 @@ Desktop 的固定快捷键 `Cmd`/`Ctrl` + `+`(或 `=`)和 `Cmd`/`Ctrl` + `-`
菜单图标使用 `--dsw-alias-menu-icon`:浅色模式为 neutral-bluish 800,深色模式为 `label-primary-dimmed`。
`--dsw-alias-illustration-*` token 分别定义分层装饰插画的背景、前层表面、后层、投影、内嵌填充与细节配色。表盘使用内嵌填充。深色配色让前层表面比后层和背景更亮;内嵌填充与投影沿用后层色调。
`base.css` 仅抑制[基础控件焦点工具](../ui-primitives/README.zh.md)通过 `data-dsh-automatic-focus` 标记的聚焦元素外轮廓线;正常键盘焦点样式、边框、阴影及错误状态保持不变。
系统提示使用 `--dsw-alias-toast-bg` 和 `--dsw-alias-toast-label`,在各调用方之间统一背景与文字颜色。文档预览配对使用 `--dsw-alias-bg-document-preview` 与 `--dsw-alias-label-document-preview`,使底色与状态文字遵循相同主题。Tooltip 键帽使用 `--dsw-alias-tooltip-key-bg`,由各主题的 tooltip 背景派生稍浅的填充。开关滑块读取 `--dsw-alias-switch-thumb`:浅色模式为纯白,深色模式为 neutral-bluish 400,使关闭态滑块比轨道更亮,同时避免纯白在暗色下过亮。
@@ -59,9 +59,11 @@ body {
--dsw-static-neutral-bluish-150: rgb(233, 236, 242);
--dsw-static-neutral-bluish-200: rgb(225, 229, 238);
--dsw-static-neutral-bluish-300: rgb(207, 211, 214);
--dsw-static-neutral-bluish-310: rgb(202, 210, 219);
--dsw-static-neutral-bluish-400: rgb(173, 178, 184);
--dsw-static-neutral-bluish-500: rgb(151, 157, 166);
--dsw-static-neutral-bluish-50: rgb(249, 250, 251);
--dsw-static-neutral-bluish-55: rgb(245, 247, 250);
--dsw-static-neutral-bluish-600: rgb(129, 133, 140);
--dsw-static-neutral-bluish-60: rgb(245, 246, 247);
--dsw-static-neutral-bluish-700: rgb(97, 102, 107);
@@ -71,6 +73,7 @@ body {
--dsw-static-neutral-bluish-850: rgb(44, 44, 46);
--dsw-static-neutral-bluish-875: rgb(35, 35, 36);
--dsw-static-neutral-bluish-900: rgb(27, 27, 28);
--dsw-static-neutral-bluish-90: rgb(237, 240, 244);
--dsw-static-neutral-bluish-950: rgb(21, 21, 23);
--dsw-static-red-100: rgb(254, 226, 226);
--dsw-static-red-400: rgb(242, 90, 90);
@@ -140,9 +143,11 @@ body[data-ds-dark-theme] {
--dsw-static-neutral-bluish-150: rgb(233, 236, 242);
--dsw-static-neutral-bluish-200: rgb(225, 229, 238);
--dsw-static-neutral-bluish-300: rgb(207, 211, 214);
--dsw-static-neutral-bluish-310: rgb(202, 210, 219);
--dsw-static-neutral-bluish-400: rgb(173, 178, 184);
--dsw-static-neutral-bluish-500: rgb(151, 157, 166);
--dsw-static-neutral-bluish-50: rgb(249, 250, 251);
--dsw-static-neutral-bluish-55: rgb(245, 247, 250);
--dsw-static-neutral-bluish-600: rgb(129, 133, 140);
--dsw-static-neutral-bluish-60: rgb(249, 250, 251);
--dsw-static-neutral-bluish-700: rgb(97, 102, 107);
@@ -152,6 +157,7 @@ body[data-ds-dark-theme] {
--dsw-static-neutral-bluish-850: rgb(44, 44, 46);
--dsw-static-neutral-bluish-875: rgb(35, 35, 36);
--dsw-static-neutral-bluish-900: rgb(27, 27, 28);
--dsw-static-neutral-bluish-90: rgb(237, 240, 244);
--dsw-static-neutral-bluish-950: rgb(21, 21, 23);
--dsw-static-red-100: rgb(254, 226, 226);
--dsw-static-red-400: rgb(242, 90, 90);
@@ -206,6 +212,12 @@ body {
--dsw-alias-button-tool-bar-fill-invisible: rgba(31, 31, 31, 0.36);
--dsw-alias-button-tool-bar-fill: rgba(84, 85, 87, 0.5);
--dsw-alias-button-tool-bar-hover: rgba(84, 85, 87, 0.6);
--dsw-alias-illustration-background: var(--dsw-static-neutral-bluish-55);
--dsw-alias-illustration-backing: var(--dsw-static-neutral-bluish-310);
--dsw-alias-illustration-detail: var(--dsw-static-neutral-bluish-310);
--dsw-alias-illustration-inset: var(--dsw-alias-illustration-surface);
--dsw-alias-illustration-shadow: var(--dsw-alias-illustration-surface);
--dsw-alias-illustration-surface: var(--dsw-static-neutral-bluish-90);
--dsw-alias-interactive-bg-active: rgba(38, 49, 72, 0.1);
--dsw-alias-interactive-bg-hover-accent: rgba(38, 49, 72, 0.14);
--dsw-alias-interactive-bg-hover-danger: rgba(236, 19, 19, 0.05);
@@ -324,6 +336,12 @@ body[data-ds-dark-theme] {
--dsw-alias-button-tool-bar-fill-invisible: rgba(31, 31, 31, 0.36);
--dsw-alias-button-tool-bar-fill: rgba(84, 85, 87, 0.5);
--dsw-alias-button-tool-bar-hover: rgba(84, 85, 87, 0.6);
--dsw-alias-illustration-background: var(--dsw-static-neutral-bluish-850);
--dsw-alias-illustration-backing: var(--dsw-static-neutral-bluish-750);
--dsw-alias-illustration-detail: var(--dsw-static-neutral-bluish-600);
--dsw-alias-illustration-inset: var(--dsw-alias-illustration-backing);
--dsw-alias-illustration-shadow: var(--dsw-alias-illustration-backing);
--dsw-alias-illustration-surface: var(--dsw-static-neutral-bluish-700);
--dsw-alias-interactive-bg-active: rgba(255, 255, 255, 0.14);
--dsw-alias-interactive-bg-hover-accent: rgba(255, 255, 255, 0.24);
--dsw-alias-interactive-bg-hover-danger: rgba(242, 90, 90, 0.15);
@@ -0,0 +1,140 @@
/** Decorative artwork resolves theme tokens to distinct foreground, backing, and detail colors. */
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { expect, it } from 'vitest'
import { parseRules, varReferences } from './stylesheet-scan.ts'
const stylesheet = readFileSync(resolve(import.meta.dirname, '../src/styles/design-platform.css'), 'utf8')
const illustration = readFileSync(resolve(import.meta.dirname, '../../ui-schedule/src/client/ScheduleEmptyIllustration.tsx'), 'utf8')
const rules = parseRules(stylesheet)
const PREFIX = '--dsw-alias-illustration-'
const ROLES = ['background', 'backing', 'detail', 'inset', 'shadow', 'surface'] as const
type Rgb = [number, number, number]
function definitions(dark: boolean): Map<string, string> {
const selector = dark ? 'body[data-ds-dark-theme]' : 'body'
return new Map(rules.filter(rule => rule.selectors.includes(selector)).flatMap(rule => rule.declarations))
}
function resolveColor(palette: Map<string, string>, name: string): Rgb {
const seen = new Set<string>()
let current = name
while (palette.has(current) && !seen.has(current)) {
seen.add(current)
const value = palette.get(current)!
const [reference] = varReferences(value)
if (reference !== undefined) {
expect(value, current).toMatch(/^var\(\s*--[\w-]+\s*\)$/)
current = reference
continue
}
const rgb = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/.exec(value)
expect(rgb, `${name}: ${current} = ${value}`).not.toBeNull()
return [Number(rgb![1]), Number(rgb![2]), Number(rgb![3])]
}
throw new Error(`Unresolved color ${name}: ${current}`)
}
function illustrationColors(palette: Map<string, string>) {
return Object.fromEntries(ROLES.map(role => [role, resolveColor(palette, `${PREFIX}${role}`)]))
}
function luminance(rgb: Rgb): number {
const linear = rgb.map((channel) => {
const value = channel / 255
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4
})
return linear[0]! * 0.2126 + linear[1]! * 0.7152 + linear[2]! * 0.0722
}
function expectDarkLayers(palette: Map<string, string>): void {
const color = (role: string): Rgb => resolveColor(palette, `${PREFIX}${role}`)
expect(luminance(color('surface'))).toBeGreaterThan(luminance(color('backing')))
expect(luminance(color('backing'))).toBeGreaterThan(luminance(color('background')))
for (const role of ['surface', 'backing', 'background']) expect(color('detail')).not.toEqual(color(role))
expect(color('inset')).toEqual(color('backing'))
expect(color('shadow')).toEqual(color('backing'))
}
function expectConsumerColors(source: string, palette: Map<string, string>): void {
const consumed = new Set(varReferences(source))
const defined = [...palette.keys()].filter(name => name.startsWith(PREFIX)).sort()
expect([...consumed].sort()).toEqual(defined)
expect(consumed.size).toBe(ROLES.length)
for (const name of consumed) resolveColor(palette, name)
}
const lightColors = {
background: [245, 247, 250],
backing: [202, 210, 219],
detail: [202, 210, 219],
inset: [237, 240, 244],
shadow: [237, 240, 244],
surface: [237, 240, 244],
}
it('preserves the original light artwork colors, including surface-colored insets and shadows', () => {
expect(illustrationColors(definitions(false))).toEqual(lightColors)
})
it.each([false, true])('defines the approved light static steps in the dark=%s palette', (dark) => {
const palette = definitions(dark)
expect(resolveColor(palette, '--dsw-static-neutral-bluish-55')).toEqual([245, 247, 250])
expect(resolveColor(palette, '--dsw-static-neutral-bluish-90')).toEqual([237, 240, 244])
expect(resolveColor(palette, '--dsw-static-neutral-bluish-310')).toEqual([202, 210, 219])
})
it('resolves the dark artwork colors through the actual static palette', () => {
expect(illustrationColors(definitions(true))).toEqual({
background: [44, 44, 46],
backing: [67, 69, 74],
detail: [129, 133, 140],
inset: [67, 69, 74],
shadow: [67, 69, 74],
surface: [97, 102, 107],
})
})
it('keeps dark surfaces brighter than backing and background, with distinct details and backing-colored insets and shadows', () => {
expectDarkLayers(definitions(true))
})
it.each([false, true])('resolves the illustration consumer names in the dark=%s palette', (dark) => {
expectConsumerColors(illustration, definitions(dark))
})
it('rejects light static-color drift even when alias names stay unchanged', () => {
const palette = definitions(false)
palette.set('--dsw-static-neutral-bluish-90', 'rgb(235, 238, 242)')
expect(() => { expect(illustrationColors(palette)).toEqual(lightColors) }).toThrow()
})
it.each([
['surface', 'backing'],
['backing', 'surface'],
['background', 'backing'],
['detail', 'surface'],
['inset', 'surface'],
['shadow', 'surface'],
])('rejects a dark %s role changed to %s', (role, target) => {
const palette = definitions(true)
palette.set(`${PREFIX}${role}`, `var(${PREFIX}${target})`)
expect(() => { expectDarkLayers(palette) }).toThrow()
})
it.each([false, true])('rejects renamed consumers and missing definitions in the dark=%s palette', (dark) => {
const palette = definitions(dark)
const renamed = illustration.replaceAll(`${PREFIX}inset`, `${PREFIX}clock-face`)
expect(() => { expectConsumerColors(renamed, palette) }).toThrow()
const misspelled = illustration.replace(PREFIX, '--dsw-alias-illustation-')
expect(() => { expectConsumerColors(misspelled, palette) }).toThrow()
palette.delete(`${PREFIX}inset`)
expect(() => { expectConsumerColors(illustration, palette) }).toThrow()
})
it.each(['--dsw-static-missing', `${PREFIX}inset`])('rejects an unresolved or cyclic inset reference to %s', (target) => {
const palette = definitions(true)
palette.set(`${PREFIX}inset`, `var(${target})`)
expect(() => resolveColor(palette, `${PREFIX}inset`)).toThrow('Unresolved color')
})
+2 -2
View File
@@ -15,8 +15,8 @@
en: 1eb35b015a6c423f
zh: a8a3dceee2c9e943
/packages-deliverables/packages:
en: a421c20359e2a77b
zh: 70286affa23c2af4
en: a437bcc95b45e6bb
zh: 181a543289f1386a
/packages-deliverables/related-documentation:
en: f640329953d0787b
zh: 3d4065e793f426ee
+1
View File
@@ -24,6 +24,7 @@ The deliverables family records what a turn hands to the user as durable Session
| Package | Role | ctx key |
|---|---|---|
| [`deliverable-file-references`](deliverable-file-references/README.md) | Shared file-link and result-delivery guidance for Web and SDK hosts | registers on `ctx.systemPrompt` |
| [`tool-present`](tool-present/README.md) | Declares existing files as final deliverables through the `present` tool | registers on `ctx.tools` |
| [`workspace-changes`](workspace-changes/README.md) | Summarizes each top-level turn's changed files from git working-tree snapshots and whole-file captures, and serves their comparisons | provides `ctx.workspaceChanges`; listens to `session/event`, appends `workspace/changes` |
+1
View File
@@ -24,6 +24,7 @@ deliverables 系列把一轮交给用户的内容记录为只有客户端读取
| 包 | 角色 | ctx key |
|---|---|---|
| [`deliverable-file-references`](deliverable-file-references/README.zh.md) | Web 与 SDK 宿主共用的文件链接与结果交付指导 | 注册到 `ctx.systemPrompt` |
| [`tool-present`](tool-present/README.zh.md) | 通过 `present` 工具把已有文件声明为最终交付物 | 注册到 `ctx.tools` |
| [`workspace-changes`](workspace-changes/README.zh.md) | 用 git 工作树快照和整文件捕获汇总每个顶层轮次改动的文件,并提供其对比 | 提供 `ctx.workspaceChanges`;监听 `session/event`,追加 `workspace/changes` |
@@ -0,0 +1,49 @@
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/deliverables/deliverable-file-references/README.md
/:
en: be4edd7a1427808e
zh: 11cd071f23d83084
/deepseek-ai-dsh-deliverable-file-references:
en: 86c55674ad26ea6f
zh: 6c1e7fbd9808e727
/deepseek-ai-dsh-deliverable-file-references/summary:
en: c1d53b592282a502
zh: c9fe1ed8de02ea68
/deepseek-ai-dsh-deliverable-file-references/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-deliverable-file-references/use-this-package:
en: 582e00c45e6b82f2
zh: 516580c07a0d8abd
/deepseek-ai-dsh-deliverable-file-references/understand-the-implementation:
en: 4fe7c3a943dbfee1
zh: c9df999dcf922e37
/deepseek-ai-dsh-deliverable-file-references/further-exploration:
en: 87bee2f9ae099907
zh: c6906e52aae6e670
/deepseek-ai-dsh-deliverable-file-references/model-experience:
en: 215e7ba838619b7b
zh: a311da3843709f90
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance:
en: def6db96da0ca05a
zh: a0956e37e0a362c9
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance/what-the-model-sees:
en: 1dfb9b2c748c87fe
zh: 9b4308986ae22e3a
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance/what-the-model-sees/shared-guidance:
en: f6f9454c369cd5a2
zh: 127b54d86cdf0cd5
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance/token-effect:
en: f3c44754e5881a8d
zh: cf72c0d320ee4881
/deepseek-ai-dsh-deliverable-file-references/model-experience/file-reference-guidance/kv-cache-effect:
en: a1a94134f784ac31
zh: d763a15160ef4043
/deepseek-ai-dsh-deliverable-file-references/known-limitations-and-deferred-work:
en: 8cf1186304d4f10b
zh: b38c0eb9558b3bdc
/deepseek-ai-dsh-deliverable-file-references/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: e88aeb617ffb74ea
@@ -0,0 +1,95 @@
---
description: "Give Web and SDK agents shared instructions for linking files and delivering results."
kind: "package-reference"
---
# @deepseek-ai/dsh-deliverable-file-references
English | [中文](README.zh.md)
## Summary
Agents receive the same file-link and result-delivery instructions in Web and SDK sessions. Use this guidance when a host can expose links or delivered files. It adds one fixed system-prompt paragraph; hosts retain responsibility for file access and rendering.
## Table of Contents
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Model Experience](#model-experience)
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
The Web and `sdk` bundles mount this plugin independently of the `present` tool. Custom compositions need only `systemPrompt`:
```yaml
- id: deliverable-file-references
name: '@deepseek-ai/dsh-deliverable-file-references'
```
There are no configuration fields. Mount [`dsh-tool-present`](../tool-present/README.md) separately to execute file deliveries. Disable this row to omit its guidance.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
The [plugin entry](src/index.ts) registers `ui:deliverable-file-references` at the shared `DELIVERABLE_FILE_REFERENCES` order. Cordis removes the section on unload. It reads no files and starts no GUI or transport.
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
- [System prompts](../../core/system-prompt/README.md) — section assembly and ordering.
- [Present tool](../tool-present/README.md) — executable file-delivery declarations.
- [Web deliverables](../../client/ui-deliverables/README.md) — file cards and file opening.
<a id="model-experience"></a>
## Model Experience
### File-reference guidance
#### What the model sees
Each assembly includes the same static paragraph:
##### Shared guidance
```markdown
Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
```
#### Token effect
One fixed paragraph while mounted, with no tool schema or extra model request.
#### KV Cache effect
The paragraph stays at first-party order 9000 across turns. Mounting, removing, or changing it changes the assembled prompt prefix; it adds no per-turn values.
## Known Limitations and Deferred Work
<a id="known-limitations-and-deferred-work"></a>
- Guidance requires a host that supports file links or deliveries; this plugin does not implement rendering, file access, or the `present` tool.
<a id="dev-note"></a>
### Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
None.
</details>
@@ -0,0 +1,95 @@
---
description: "为 Web 和 SDK agent 提供共用的文件链接与结果交付说明。"
kind: "package-reference"
---
# @deepseek-ai/dsh-deliverable-file-references
[English](README.md) | 中文
## 概述
Agent 在 Web 和 SDK 会话中接收相同的文件链接与结果交付说明。宿主能够提供链接或已交付文件时,可使用这段指导。它增加一个固定的系统提示词段落;文件访问与渲染仍由宿主负责。
## 目录
- [使用本包](#use-this-package)
- [理解实现](#understand-the-implementation)
- [进一步探索](#further-exploration)
- [模型体验](#model-experience)
- [已知限制与延期工作](#known-limitations-and-deferred-work)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用本包
Web 和 `sdk` bundle 独立于 `present` 工具挂载本插件。自定义组合仅需提供 `systemPrompt`:
```yaml
- id: deliverable-file-references
name: '@deepseek-ai/dsh-deliverable-file-references'
```
本包没有配置字段。执行文件交付时,需单独挂载 [`dsh-tool-present`](../tool-present/README.zh.md)。禁用本行即可省略这段指导。
-----
<a id="understand-the-implementation"></a>
## 理解实现
<details>
<summary>实现细节——点击展开</summary>
[插件入口](src/index.ts)按共享的 `DELIVERABLE_FILE_REFERENCES` 顺序注册 `ui:deliverable-file-references`。卸载时 Cordis 移除该段落。本插件不读取文件,也不启动 GUI 或传输服务。
</details>
-----
<a id="further-exploration"></a>
## 进一步探索
- [系统提示词](../../core/system-prompt/README.zh.md)——段落组装与顺序。
- [Present 工具](../tool-present/README.zh.md)——可执行的文件交付声明。
- [Web 交付](../../client/ui-deliverables/README.zh.md)——文件卡片与文件打开。
<a id="model-experience"></a>
## 模型体验
### 文件引用指导
#### 模型看到的内容
每次组装均包含同一个静态段落:
##### 共用指导
```markdown
Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
```
#### Token 影响
挂载期间增加一个固定段落,不增加工具 schema 或额外模型请求。
#### KV Cache 影响
该段落跨轮次保持 first-party 顺序 9000。挂载、移除或修改它会改变组装后的提示词前缀;它不增加逐轮变化的值。
## 已知限制与延期工作
<a id="known-limitations-and-deferred-work"></a>
- 指导要求宿主支持文件链接或交付;本插件不实现渲染、文件访问或 `present` 工具。
<a id="dev-note"></a>
### 开发备注
<details>
<summary>维护者工作上下文——点击展开</summary>
无。
</details>
@@ -0,0 +1,37 @@
{
"name": "@deepseek-ai/dsh-deliverable-file-references",
"description": "Shared model guidance for file links and deliverables across DeepSeek Harness hosts",
"version": "0.2.1-rc.2",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/deliverables/deliverable-file-references"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts"
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-system-prompt": "workspace:*"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-system-prompt": "workspace:*"
}
}
@@ -0,0 +1,30 @@
/** Shared file-delivery guidance for clients that render desktop-style file references. */
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-system-prompt'
/** Stable Loader identity. */
export const name = 'deliverable-file-references'
/** Prompt registry shared by the Web and SDK desktop compositions. */
export const inject = ['systemPrompt']
/** Static Web guidance for primary outputs and existing-file references. */
const FILE_REFERENCE_PROMPT = 'Prefer showing the primary results within your final response alongside a brief explanation. '
+ 'Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. '
+ 'Do not call present just to list edited source files, or run commands to check whether a diff view will appear. '
+ 'Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. '
+ 'Avoid repeating results already shown inline unless the separate card adds useful access. '
+ 'Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. '
+ 'Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.'
/**
* Register the desktop file-reference guidance without mounting a GUI.
* @param ctx - context carrying the prompt registry.
*/
export function apply(ctx: Context): void {
ctx.systemPrompt.section({
name: 'ui:deliverable-file-references',
order: ctx.systemPrompt.getSectionOrder('DELIVERABLE_FILE_REFERENCES'),
text: FILE_REFERENCE_PROMPT,
})
}
@@ -13,23 +13,16 @@ afterEach(async () => {
ctx = undefined
})
describe('ui-deliverables node plugin', () => {
describe('desktop file-reference prompt', () => {
it('registers final-response file-reference guidance only while mounted', async () => {
ctx = new Context()
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
ctx.provide('connection', { fetch: { register: () => () => {} } } as never)
ctx.provide('sessionQuery', {} as never)
ctx.provide('sessionController', {} as never)
ctx.provide('workspaceFiles', {} as never)
ctx.provide('fs', {} as never)
ctx.provide('sandboxPolicy', {} as never)
ctx.provide('workspaceChanges', {} as never)
const mounted = ctx.plugin({ apply, inject })
await mounted.await()
const section = (await ctx.systemPrompt.assemble()).sections
.find(entry => entry.name === 'ui:deliverable-file-references')
expect(section?.text).toMatchInlineSnapshot('"Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix."')
expect(section?.text).toMatchInlineSnapshot('"Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix."')
// Only live request pins track current guidance; seeded sessions retain their recorded prompt.
for (const scenario of ['fresh-round-trip', 'ptc-round']) {
@@ -37,6 +30,11 @@ describe('ui-deliverables node plugin', () => {
expect(sidecar, scenario).toContain(section!.text)
}
for (const scenario of ['desktop-context', 'desktop-ptc-context']) {
const sidecar = await readFile(new URL(`../../../../snapshots/sdk/${scenario}/system-prompt.expected.md`, import.meta.url), 'utf8')
expect(sidecar, scenario).toContain(section!.text)
}
await mounted.dispose()
expect((await ctx.systemPrompt.assemble()).sections
.some(entry => entry.name === 'ui:deliverable-file-references')).toBe(false)
@@ -0,0 +1,18 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cordis"
},
{
"path": "../../core/system-prompt"
}
]
}
Loaded 100 of 222 files, more files were not shown because too many files have changed in this diff. Show more