Merge branch 'master' into fix/producer-owned-source-doc-examples

This commit is contained in:
Chinesezjc authored and GitHub committed 2026-09-24 18:20:16 +08:00
commit 72a9690fa8
210 files changed
+1182 -599

No files matched your search

@@ -18,8 +18,8 @@
en: a6d7884e4359af3a
zh: e72588705942000a
/agent-note-the-approval-seam-one-shot-permission-decisions-over-a-waterfall-of-answerers/decision/design-detail/the-seam-mechanism-and-policy-split:
en: 0aa6f3a90921d1ea
zh: c4a734f84800dae8
en: 53da7800d8744d9e
zh: 316fef884853ca5d
/agent-note-the-approval-seam-one-shot-permission-decisions-over-a-waterfall-of-answerers/decision/design-detail/ask-routing-in-dsh-tools:
en: 1d4478059f608fd2
zh: 4a1d12e4ccb766b5
@@ -55,7 +55,7 @@ After validation and a successful `approval/asked` append, the service resolves
Answerers are `approval/request` waterfall listeners. Zero listeners fall through to `unavailable`; a recognizing listener occupies the first-wins decision slot, while an unrecognized agent must delegate with `next()`. Listeners dispose with their fibers, so an unloaded channel fails closed. Because sibling registration order is not deterministic, a deployment composes one terminal answerer and reserves `prepend` for decide-or-delegate gates.
`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, and optional `signal`. It uses the `ToolCallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments.
`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, optional localized `displayReason`, and optional `signal`. It uses the `ToolCallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments.
#### Ask routing in dsh-tools
@@ -55,7 +55,7 @@ tool/result "escalated" — this one call ran under the wider mode; the gra
应答者是 `approval/request` waterfall 监听器。零监听器会直接落到 `unavailable`;识别该 agent 的监听器占用先到先得的决策槽,而不识别的监听器必须调用 `next()` 委派。监听器会随其 fiber 一同 dispose(资源释放),因此卸载通道后,请求会在故障时默认被拒绝。由于兄弟插件的注册顺序不确定,部署应组合一个终端应答者,并保留 `prepend` 给「决策或委派」门禁。
`ApprovalRequest` 携带发起请求的 `agent`、`toolName`、可选的精确 `callId`、人类可读的 `reason` 和可选的 `signal`。它使用 `ToolCallId` brand 而不导入依赖本 seam 的 `dsh-tools`。通道适配器可按 `callId` 关联任何更丰富的调用状态;审批请求本身不重复携带工具参数。
`ApprovalRequest` 携带发起请求的 `agent`、`toolName`、可选的精确 `callId`、人类可读的 `reason`、可选的本地化 `displayReason` 和可选的 `signal`。它使用 `ToolCallId` brand 而不导入依赖本 seam 的 `dsh-tools`。通道适配器可按 `callId` 关联任何更丰富的调用状态;审批请求本身不重复携带工具参数。
#### dsh-tools 中的 Ask 路由
@@ -0,0 +1,43 @@
# Bilingual-pair consistency record for 2026-09-19-timed-user-question-two-settlements.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/proposed/architecture/2026-09-19-timed-user-question-two-settlements.md
/agent-note-timed-user-questions-as-two-settlements:
en: 15c55d3b120b8fd4
zh: 6b86ac34f4a1e9cd
/agent-note-timed-user-questions-as-two-settlements/problem:
en: f665127a9b5b8225
zh: b19845f62310f3a2
/agent-note-timed-user-questions-as-two-settlements/proposal:
en: 56ca8c0f97976358
zh: 69163bb5f064e069
/agent-note-timed-user-questions-as-two-settlements/proposal/modes-and-scope:
en: d23e622ee4643b48
zh: 3732c10c12344755
/agent-note-timed-user-questions-as-two-settlements/proposal/ownership:
en: 0543678ad9d26fc5
zh: aa6ba207e661c567
/agent-note-timed-user-questions-as-two-settlements/proposal/foreground-wait-and-claim-lifetime:
en: 447713bbdf1ab4d8
zh: 2757678ae47822aa
/agent-note-timed-user-questions-as-two-settlements/proposal/durable-state-and-late-answers:
en: b8729d06ab7814ed
zh: acaa8f4174701248
/agent-note-timed-user-questions-as-two-settlements/proposal/card-state-and-delivery-races:
en: 8c55b693e7a5bca4
zh: aea6e966ce3667e3
/agent-note-timed-user-questions-as-two-settlements/proposal/local-countdown-and-panel-actions:
en: 4c0c9ec59fc51217
zh: 322870eeb49176c7
/agent-note-timed-user-questions-as-two-settlements/proposal/reply-node-and-grouping:
en: 10323b0e0340a40b
zh: a17fc3a4d4d820a7
/agent-note-timed-user-questions-as-two-settlements/alternatives-considered:
en: ce8ff9d5bc4b85b7
zh: 49024fe3a5b194b2
/agent-note-timed-user-questions-as-two-settlements/acceptance-criteria:
en: 5a993484c73befe9
zh: 38cd3209c68178c3
/agent-note-timed-user-questions-as-two-settlements/risks:
en: aae260b0cd95554f
zh: 3d7fbd6cbe9e2d37
@@ -0,0 +1,113 @@
# Agent Note: Timed user questions as two settlements
Status: proposed
English | [中文](2026-09-19-timed-user-question-two-settlements.zh.md)
## Problem
A question can block an agent even when useful work does not depend on the answer. Releasing the tool call after a wait must not discard the question: the user may answer after the agent has continued or after reopening the Session. A pending result is neither a skipped answer nor permission to proceed with work that requires approval.
The foreground tool result and the eventual answer have different lifetimes. A `user-questions/request` waterfall settles once; keeping it alive after returning pending creates a second answer path that outlives its caller. Countdown, focus, and editing also have different owners from durable question state.
## Proposal
Keep foreground answers on the existing Remote Event waterfall. A timeout ends that request and returns pending; a later answer uses a business RPC that steers a durable user message. The claim stream controls the foreground wait only. It neither transports answers nor replaces the Remote Event.
### Modes and scope
- The default `mode: legacy` retains the blocking schema and `ask()` path. Explicit `mode: timed` adds `timeout`, using the configured default of 120 seconds when omitted.
- A positive integer timeout selects `askTimed()`; `-1` selects blocking `ask()`. A blocking call made with the timed schema still participates in timed-question projection and review; it is not identical to a legacy call.
- Plan review retains `ask()`, its `plan-review` intent, and `BAD_INTENT` validation. This decision does not move plan review to another package.
- Human interaction requires the exact live runtime root when an Agent is supplied. A stale instance fails with `CALLER_NOT_LIVE`; an owned child fails with `DELEGATED_CALLER`. Durable Session lineage does not prevent a resumed runtime root from asking.
### Ownership
| Owner | Responsibility |
|---|---|
| `tool-ask-user` | Model schema, timeout validation, service selection, and pending-result text. |
| `UserQuestionService` | Foreground waits and Client claims, unattended deadlines, late-answer validation and steering, and the durable question projection. |
| Gateway and Remote runtime | Existing request delivery, settlement, cancellation, and Remote stream transport; no question-specific timer or event-name branch. |
| `ui-user-questions` | One card per Session and call, answer-channel selection, local countdown and drafts, and the reply Definition and renderer. |
| `ui-chat` | Message-id-based presentation aggregation and ordinary process/Turn folding. |
| Question-specific tool row | Pending/answered presentation and panel actions, composed in `QuestionToolRow` without adding `rowAction` to generic `ToolRow`. |
The [question service](../../../../packages/interaction/user-questions/README.md), [tool](../../../../packages/interaction/tool-ask-user/README.md), and [questions UI](../../../../packages/client/ui-user-questions/README.md) own their package contracts.
### Foreground wait and claim lifetime
`TimedQuestionWait` owns a private cancellation signal, the original Host deadline, and Client claims. The Host runs a timer only while no answer UI holds a claim. Delivery to a transport queue is not a claim. Losing the last claim restores the original deadline, with immediate expiry if it has passed.
A timed request carries `wait: { callId, timed: true }`. Before attaching its answer channel, the Client opens `attachWait`, a business Remote stream that yields one Host-computed `remainingMs` frame and stays open for the claim's lifetime. Focus, edits, and heartbeats do not travel through this stream.
The Client retains its claim through delivery of the waterfall outcome to the Host. Returning an answer locally is not delivery: releasing the claim then could let an overdue Host timer beat that answer. The Host closes the claim when the foreground request settles. Explicit delegation releases the claim before `next()`; plugin teardown delegates and cancels its stream.
Client countdown expiry rejects the waterfall with `ASK_TIMED_OUT`. Unattended Host expiry aborts only the private wait signal with the same business error. `askTimed()` maps only that error to pending, maps parent cancellation to `ASK_ABORTED`, and propagates other failures. `NO_PROVIDER` waits on the same bounded wait lifetime, not a permanently unresolved promise. Foreground completion releases timers, claims, and the wait registry entry.
### Durable state and late answers
The `userQuestions` projection reconstructs question state from existing Session events. It recognises the timed schema by the `timeout` parameter recorded in `request/header`; legacy calls are not tracked. A valid `tool/call` opens a question. Its `tool/result` makes it continued for pending or `TOOL_OUTCOME_UNKNOWN`, settles it with a valid answer batch, or removes it for another result or failure.
A continued question accepts `answer(agent, callId, answer)`. The method returns `false` for an unknown or non-continued call and throws `BAD_ANSWER` unless the batch names each question exactly once. Accepted answers use `agent.steer(createUserMessage(...))`, source `{ kind: 'user-question-reply', callId, outcome: 'answered' }`, and JSON text containing `answer_to_pending_question`, the call id, original questions, and answers. Remote Agent resolution from the Session id resumes a root when needed; the `answer()` body does not own that resume operation.
The inbox message is the durable reply. `agent/inbox/spliced` settles the question when the message is queued; the later admitted `user/message` is idempotent for that settlement. Settled answer batches remain in the projection so the original tool row can show late answers even though its own result remains pending. Unusable recorded reply text settles with an empty batch.
No new Session event type or wait-state log is needed. The message-source extension requires the repository's persistence-type acknowledgement. `dismissed` remains readable for historical records but has no producer: hiding a panel does not manufacture a reply.
### Card state and delivery races
`QuestionCards` shares one `PendingQuestion` between the live request and the continued projection, keyed by Session and `callId`. A blocking request without a call id has its own per-request card. An open card submits through its waterfall; without that channel it disables submission. A continued card uses the late-answer RPC.
A waterfall submission has no delivery acknowledgement. The Gateway can discard an outcome that loses a settlement race. The card therefore retains its draft and stays busy until projection reconciliation. If the question becomes continued during submission, the controls re-arm with a resubmit hint; the user can submit the retained draft through RPC. This is not automatic retry or a guarantee that every local submission reaches the model.
A tool-call-keyed card is removed when its call is absent from the active projection and no waterfall remains. Removal closes the card and clears its draft. Neither a local submission nor a transport cancel frame alone decides that the question is answered. An unkeyed blocking card ends with its waterfall; closing that blocking panel rejects with `ASK_CANCELLED`.
### Local countdown and panel actions
The Client computes `Date.now() + remainingMs`. Manual focus on a pristine answer surface pauses its countdown, and blur resumes the preserved remainder. Focus before the claim handshake remains effective when the deadline arrives; blur before the handshake does not create a timer. First edit or Take time freezes the countdown, with that disposition saved beside the browser-local draft.
Autofocus applies only when the answer channel is ready, no countdown is active, and the card is not locked. Hiding a timed-call panel leaves its claim and countdown running; its tool row can reopen it. A settled call opens a read-only panel from recorded questions and answers, not another answer channel.
### Reply node and grouping
The questions UI identifies a late reply from `source.kind`, then parses its recorded JSON for question-and-answer presentation. Pasting identical JSON in an ordinary user message does not identify it as a reply. Unreadable recorded payloads retain their raw-text presentation.
The ordinary message Definition and the question-reply Definition both match the appended message and retain separate Node Store projections with the same message id. Chat business grouping omits the `turn-trigger` presentation when a `question-reply` with that id is present. The duplicate trigger neither renders nor splits a group; the reply renders once. The same aggregation applies to unscoped historical input.
The reply participates in ordinary process grouping and whole-Turn folding. It is not independently visible: reading it can require expanding both its Turn and process group. Turn disclosure stays before the group it controls, including a reply that opens the Turn.
This decision adds no process-role field, independent-layout flag, user-message structure change, or target-local fallback dispatch. The independent-kind rules stay in Chat's existing grouping logic. A node-data-owned declaration of grouping and overlap semantics is deferred infrastructure, not part of the question feature.
## Alternatives considered
**Unconditional Host deadline or Gateway-owned question timers.** An unconditional deadline overrides local editing and Take time. Putting the exception into pending Remote Events or branching on the question event in Remote dispatch makes generic transport own business policy. Claims keep that policy in the question service.
**Keep the waterfall alive after returning pending.** The request outlives the tool caller, and late waterfall answers compete with the RPC. Ending the foreground request gives each settlement one answer path.
**Delegate every timed request immediately.** Dispatching to an intentionally empty answerer set wastes the existing blocking-answer path and requires special handling of `NO_PROVIDER`. Delegation is for an unavailable answerer, not the timed mode itself.
**Persist a whole-value wait/focus/edit stream or use Host focus leases.** Durable question status is derivable from tool and inbox events. Focus and editing are local UI state; logging them or synchronising focus leases adds Host state without making multiple Clients share one editing session.
**Reclassify JSON inside generic `MessageItem`, borrow the relay form, or use the default context row.** Parsing text to identify the business message confuses its source with its content; the relay form describes another agent's message, and a generic context row exposes JSON. A dedicated source, Definition, and renderer own the question presentation.
**Independent reply layout through generic node metadata.** It broadens a question feature into a conversation-infrastructure change. Ordinary folding and Chat-owned aggregation are sufficient for this decision; independently declared node-data grouping semantics remain a separate design task.
**A log-only close event or a manufactured dismissal message on panel close.** A log-only close hides the user's decision from the model and adds another durable mechanism. Manufacturing a reply treats hiding the panel as an answer. Panel close therefore leaves a timed question answerable.
**A dedicated late-reply Session event beside the inbox message.** The inbox splice and admitted user message already record the reply. A third event duplicates the same fact and adds another persistence type.
## Acceptance criteria
- Service tests cover claimed and unclaimed waits, last-claim disconnect, cancellation, timeout error mapping, continued-only answers, and exact answer-id validation.
- Projection tests distinguish legacy and timed schemas and cover pending, answered, other failures, resume repair, and duplicate inbox/message settlement.
- Client tests cover claim retention through Host acceptance, focus before handshake, indefinite waits, draft retention, RPC resubmission, and panel hiding/reopening.
- Grouping and browser replay cover one rendered reply with both projections retained, ordinary collapsed placement, disclosure ordering, and the original tool row's read-only late-answer panel.
- Legacy question round trips, cancellation, plan-review intent, and Session remounts retain their existing behavior. Live multi-Client delivery races and real Host restart remain integration coverage gaps beyond isolated lifecycle and projection tests.
## Risks
- Take time affects only one Client. Another Client can answer or expire the shared waterfall; local focus does not establish a global hold.
- A terminal claim-stream failure can reject the foreground question rather than produce pending. No unattended-wait fallback is provided for that failure.
- Host and Client clocks need not align, but Client system-clock changes can move expiry and transport latency can delay the local countdown's start.
- The duplicate projections remain stored. Aggregation adds linear scans during structural grouping rebuilds; ordinary text-only streaming updates retain their incremental path. This is an algorithmic bound, not a measured latency claim.
- Unanswered continued questions can remain after reopening a Session. Panel close records nothing, and visibility follows normal folding rather than an always-visible reply guarantee.
@@ -0,0 +1,113 @@
# Agent Note: 计时用户提问的两次结算
Status: proposed
[English](2026-09-19-timed-user-question-two-settlements.md) | 中文
## Problem
即使还有不依赖回答的工作可做,问题仍可能阻塞 agent(智能体)。等待后放行工具调用不能丢弃问题:用户可能在 agent 继续执行后,或重新打开 Session 后才回答。pending 结果既不是跳过回答,也不是开展需要批准的工作的授权。
前台工具结果与最终回答的生命周期不同。`user-questions/request` waterfall(瀑布式事件)只结算一次;返回 pending 后仍保留它,会产生一条活过调用方的第二回答路径。倒计时、聚焦和编辑的归属也不同于持久问题状态。
## Proposal
前台回答继续使用现有 Remote Event waterfall。超时结束该请求并返回 pending;迟到回答通过业务 RPC steer 一条持久用户消息。接手 stream 只控制前台等待,既不传递回答,也不替代 Remote Event。
### 模式与范围
- 默认 `mode: legacy` 保留阻塞式 schema 和 `ask()` 路径。显式 `mode: timed` 增加 `timeout`,省略时使用配置默认值 120 秒。
- 正整数超时选择 `askTimed()`;`-1` 选择阻塞式 `ask()`。timed schema 发出的阻塞调用仍参与计时问题投影和答案查看,并不等同于 legacy 调用。
- 计划评审保留 `ask()`、`plan-review` 意图及 `BAD_INTENT` 校验。本决策不把计划评审迁移到其他包。
- 提供 Agent 时,人机交互要求它是运行时中确切的存活根实例。陈旧实例以 `CALLER_NOT_LIVE` 失败;被其他 agent 拥有的子实例以 `DELEGATED_CALLER` 失败。持久 Session 的血缘关系不妨碍恢复后的运行时根实例提问。
### 职责归属
| 归属方 | 职责 |
|---|---|
| `tool-ask-user` | 模型 schema、超时校验、服务选择和 pending 结果文本。 |
| `UserQuestionService` | 前台等待与 Client 接手、无人接手时的 deadline、迟到回答校验与 steering(中途引导),以及持久问题投影。 |
| Gateway 与 Remote 运行时 | 现有请求投递、结算、取消及 Remote stream 传输;不包含问题专属计时器或按事件名分支。 |
| `ui-user-questions` | 每个 Session 和调用的一张卡片、回答通道选择、本地倒计时与草稿,以及回复 Definition 和渲染器。 |
| `ui-chat` | 基于消息 id 的呈现聚合,以及普通过程/轮次折叠。 |
| 问题专用工具行 | pending/已回答呈现与面板操作,在 `QuestionToolRow` 中组合,不向通用 `ToolRow` 添加 `rowAction`。 |
[问题服务](../../../../packages/interaction/user-questions/README.zh.md)、[工具](../../../../packages/interaction/tool-ask-user/README.zh.md)和[问题 UI](../../../../packages/client/ui-user-questions/README.zh.md)分别拥有各包的约定。
### 前台等待与接手生命周期
`TimedQuestionWait` 拥有专属取消 signal、原始 Host deadline 和 Client 接手记录。Host 只在没有回答 UI 接手时运行计时器。投递到传输队列不算接手。最后一个接手方离开后恢复原 deadline,已经过期则立即到期。
计时请求携带 `wait: { callId, timed: true }`。Client 在挂接回答通道前打开业务 Remote stream `attachWait`,接收一帧由 Host 计算的 `remainingMs`,并在接手期间保持 stream 打开。聚焦、编辑和心跳不经过该 stream。
Client 将接手保留到 waterfall 结果交付给 Host。本地返回回答不等于交付:此时释放接手会让已经到期的 Host 计时器抢在回答之前结算。Host 在前台请求结算时关闭接手。显式委托在 `next()` 前释放接手;插件拆卸会委托请求并取消自己的 stream。
Client 倒计时到期以 `ASK_TIMED_OUT` 拒绝 waterfall。无人接手时,Host 到期只以同一业务错误中止等待专属 signal。`askTimed()` 只把该错误映射为 pending,把父级取消映射为 `ASK_ABORTED`,其余失败继续传播。`NO_PROVIDER` 等待同一个有界等待生命周期,而不是永不结算的 Promise。前台完成时释放计时器、接手记录及等待注册项。
### 持久状态与迟到回答
`userQuestions` 投影从现有 Session 事件重建问题状态。它通过 `request/header` 记录的 `timeout` 参数识别 timed schema,不跟踪 legacy 调用。有效的 `tool/call` 打开问题。其 `tool/result` 在 pending 或 `TOOL_OUTCOME_UNKNOWN` 时将问题标为已继续,在有效回答批次时结算并保存答案,其他结果或失败则移除问题。
已继续的问题接受 `answer(agent, callId, answer)`。未知或非已继续调用返回 `false`;回答批次没有恰好包含每个问题一次时抛出 `BAD_ANSWER`。接受的回答通过 `agent.steer(createUserMessage(...))` 送入 agent,来源为 `{ kind: 'user-question-reply', callId, outcome: 'answered' }`,JSON 文本包含 `answer_to_pending_question`、调用 id、原问题及答案。Remote 根据 Session id 解析 Agent 时按需恢复根实例;恢复操作不属于 `answer()` 方法体。
inbox 消息就是持久回复。`agent/inbox/spliced` 在消息入队时结算问题;随后接纳的 `user/message` 对该结算是幂等的。已结算答案批次保留在投影中,让原工具行即使自身结果仍是 pending,也能显示迟到回答。无法使用的已记录回复文本以空答案批次结算。
无需新增 Session 事件类型或等待状态日志。消息来源扩展需要仓库的持久化类型变更确认。`dismissed` 为历史记录保留可读性,但没有生产者:隐藏面板不会伪造回复。
### 卡片状态与投递竞争
`QuestionCards` 按 Session 和 `callId`,让实时请求与已继续投影共享一个 `PendingQuestion`。没有调用 id 的阻塞请求拥有独立的逐请求卡片。开放卡片通过 waterfall 提交;缺少该通道时禁用提交。已继续卡片使用迟到回答 RPC。
waterfall 提交没有送达确认。Gateway 可能丢弃输掉结算竞争的结果,因此卡片保留草稿并保持忙碌,等待投影同步。若提交期间问题变为已继续,控件带重提提示重新可用,用户可以通过 RPC 提交保留的草稿。这不是自动重试,也不保证每次本地提交都能到达模型。
按工具调用标识的卡片在调用不属于活动投影且没有 waterfall 时移除。移除会关闭卡片并清理草稿。本地提交和传输取消帧都不能单独决定问题已经回答。无调用标识的阻塞卡片随 waterfall 结束;关闭这种阻塞面板以 `ASK_CANCELLED` 拒绝请求。
### 本地倒计时与面板操作
Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会暂停倒计时,失焦后从保留的剩余时间继续。接手握手前的聚焦在 deadline 到达时仍生效;握手前失焦不会制造计时器。首次编辑或「慢慢回答」冻结倒计时,该状态与浏览器本地草稿一起保存。
只有回答通道就绪、没有活动倒计时且卡片未锁定时才自动聚焦。隐藏计时调用的面板会保留接手和倒计时,工具行可以重新打开面板。已结算调用依据记录的问题与答案打开只读面板,不产生另一条回答通道。
### 回复节点与分组
问题 UI 通过 `source.kind` 识别迟到回复,再解析已记录的 JSON 来呈现问答。普通用户消息粘贴相同 JSON 不会被识别为回复。无法读取的已记录载荷保留原始文本呈现。
普通消息 Definition 和问答回复 Definition 都匹配追加的消息,在 Node Store 中保留消息 id 相同的独立投影。存在同 id 的 `question-reply` 时,Chat 业务分组省略 `turn-trigger` 呈现。重复触发节点既不渲染,也不切开分组,回复只显示一次。相同聚合也适用于无轮次归属的历史输入。
回复参与普通过程分组和整轮折叠,不保证独立可见:阅读时可能需要展开所属轮次与过程分组。轮次折叠控件始终在所控制的分组之前,包括由回复开启轮次的情况。
本决策不添加 process-role 字段、独立布局标记、用户消息结构变更或目标内 fallback 派发。独立 kind 规则保留在 Chat 现有分组逻辑中。由节点 data 声明分组与重叠语义的机制是留待拆分的基础设施,不属于问题功能。
## Alternatives considered
**无条件 Host deadline 或 Gateway 拥有问题计时器。** 无条件 deadline 会覆盖本地编辑和「慢慢回答」。把例外放入 pending Remote Events,或在 Remote 派发中按问题事件分支,会让通用传输拥有业务策略。接手机制将策略留在问题服务中。
**返回 pending 后继续保留 waterfall。** 请求活过工具调用方,迟到的 waterfall 回答与 RPC 竞争。结束前台请求让每次结算各有一条回答路径。
**立即委托所有计时请求。** 向故意留空的回答方集合派发请求,浪费现有阻塞回答路径,还要求特殊处理 `NO_PROVIDER`。委托表示回答方不可用,不代表计时模式本身。
**持久化整值等待/聚焦/编辑流,或使用 Host 聚焦租约。** 持久问题状态可从工具与 inbox 事件推导。聚焦和编辑属于本地 UI 状态;记录它们或同步聚焦租约会增加 Host 状态,却不能让多个 Client 共享同一编辑会话。
**在通用 `MessageItem` 内重新分类 JSON、借用 relay form,或使用默认 context 行。** 解析文本来识别业务消息混淆了来源与内容;relay form 表示其他 agent 的消息,通用 context 行则会暴露 JSON。专用来源、Definition 和渲染器拥有问题呈现。
**通过通用节点元数据让回复独立布局。** 这会把问题功能扩大为对话基础设施变更。普通折叠与 Chat 内聚合足以满足本决策;由节点 data 独立声明分组语义仍是单独的设计任务。
**面板关闭时写仅入日志的关闭事件,或伪造放弃消息。** 仅入日志会让模型看不到用户决定,还增加另一套持久机制。伪造回复则把隐藏面板当成回答。因此关闭面板仍让计时问题保持可回答。
**在 inbox 消息之外增加专用迟到回复 Session 事件。** inbox 插入与接纳的用户消息已经记录回复。第三个事件重复同一事实,并增加另一种持久化类型。
## Acceptance criteria
- 服务测试覆盖有人与无人接手、最后接手方断开、取消、超时错误映射、仅接受已继续问题的回答,以及答案 id 的精确校验。
- 投影测试区分 legacy 与 timed schema,并覆盖 pending、已回答、其他失败、恢复修复及 inbox/消息重复结算。
- Client 测试覆盖接手保留到 Host 接受结果、握手前聚焦、无限期等待、草稿保留、RPC 重提及面板隐藏/重开。
- 分组与浏览器回放覆盖保留两个投影但只渲染一次回复、普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。
- legacy 问答往返、取消、计划评审意图及 Session 重新挂载保留既有行为。真实多 Client 投递竞争与真实 Host 重启仍是独立生命周期和投影测试之外的集成覆盖缺口。
## Risks
- 「慢慢回答」只影响一个 Client。其他 Client 可以回答或让共享 waterfall 到期,本地聚焦不构成全局暂停。
- 接手 stream 的终止性失败可能让前台问题失败,而不是产生 pending。不为该失败提供无人接手等待的 fallback。
- Host 与 Client 时钟不必对齐,但 Client 系统时钟变化可能移动到期时间,传输延迟可能推迟本地倒计时的开始。
- 重复投影仍保留在存储中。聚合在结构性分组重建时增加线性扫描;普通纯文本 stream 更新保留增量路径。这是算法复杂度界限,不是实测延迟结论。
- 无人回答的已继续问题可以在重新打开 Session 后继续存在。关闭面板不写记录,可见性遵循普通折叠,而不是保证回复始终可见。
+1 -1
View File
@@ -912,7 +912,7 @@ describe('a profile patch stored before Developer tools owned preset selection',
setup: agentCtx => legacy.agentPresets.mount(agentCtx).then(() => undefined),
})
try {
expect(toolNames(legacy, handle.agent)).toEqual(['bash'])
expect(toolNames(legacy, handle.agent)).toEqual(['bash', ...SCHEDULE_TOOLS])
} finally {
await handle.dispose()
}
+2 -2
View File
@@ -354,7 +354,7 @@ describe('web e2e: agent-preset selection', () => {
await dialog.getByRole('button', { name: 'New task default: Minimal mode' }).waitFor({ timeout: 10_000 })
await expect.poll(() => livePreset(scaffold), { timeout: 15_000 }).toBe('minimal')
await dialog.getByRole('button', { name: 'General', exact: true }).click()
const developerTools = dialog.getByRole('switch', { name: 'Developer tools' })
const developerTools = dialog.getByRole('switch', { name: 'Coding Tools' })
await developerTools.click()
await expect.poll(() => developerTools.getAttribute('aria-checked')).toBe('false')
await dialog.getByRole('button', { name: 'Close' }).last().click()
@@ -366,7 +366,7 @@ describe('web e2e: agent-preset selection', () => {
await openSettings(page, 'en')
const reopened = page.getByRole('dialog', { name: 'Settings' })
await reopened.getByRole('button', { name: 'General', exact: true }).click()
const reopenedDeveloperTools = reopened.getByRole('switch', { name: 'Developer tools' })
const reopenedDeveloperTools = reopened.getByRole('switch', { name: 'Coding Tools' })
await reopenedDeveloperTools.click()
await expect.poll(() => reopenedDeveloperTools.getAttribute('aria-checked')).toBe('true')
await reopened.getByRole('button', { name: 'Agent presets' }).click()
+2 -2
View File
@@ -175,8 +175,8 @@ describe('web e2e: a git workspace turn ends with its changed files', () => {
expect(compactHeight).toBeLessThan(60)
await openSettings(page, 'zh')
const settings = page.getByRole('dialog', { name: '设置' })
await settings.getByRole('switch', { name: '开发者工具' }).click()
await expect.poll(() => settings.getByRole('switch', { name: '开发者工具' }).getAttribute('aria-checked')).toBe('true')
await settings.getByRole('switch', { name: '代码工作工具' }).click()
await expect.poll(() => settings.getByRole('switch', { name: '代码工作工具' }).getAttribute('aria-checked')).toBe('true')
await settings.getByRole('button', { name: '关闭', exact: true }).click()
await header.getByRole('tablist').waitFor({ state: 'visible' })
expect(await header.evaluate(element => element.getBoundingClientRect().height)).toBeGreaterThan(compactHeight)
@@ -14,7 +14,7 @@ it('persists developer tools in the Host settings document and restores the acce
const page = await newEnglishPage(browser)
await page.goto(scaffold.authenticatedUrl)
await openSettings(page, 'en')
const toggle = page.getByRole('switch', { name: 'Developer tools' })
const toggle = page.getByRole('switch', { name: 'Coding Tools' })
expect(await toggle.getAttribute('aria-checked')).toBe('true')
expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).not.toMatch(/id: ui-settings(?:\r?\n|$)/)
await toggle.click()
@@ -29,7 +29,7 @@ it('persists developer tools in the Host settings document and restores the acce
// The page owns no selection switch: only the General one gates the choice.
const section = page.locator('section')
.filter({ has: page.getByRole('heading', { name: 'Agent presets', exact: true }) })
const refused = section.getByRole('button', { name: /^Turn on Developer tools in General settings to choose a default: / })
const refused = section.getByRole('button', { name: /^Turn on Coding Tools in General settings to choose a default: / })
expect(await section.getByRole('switch').count()).toBe(0)
await expect.poll(() => refused.count()).toBeGreaterThan(0)
await scaffold.ctx.settings.update('ui-settings', { enabled: true })
+2 -2
View File
@@ -837,8 +837,8 @@ else process.exit(1);
].join('\n'))
await openSettings(page, 'en')
const settings = page.getByRole('dialog', { name: 'Settings' })
await settings.getByRole('switch', { name: 'Developer tools' }).click()
await expect.poll(() => settings.getByRole('switch', { name: 'Developer tools' }).getAttribute('aria-checked')).toBe('true')
await settings.getByRole('switch', { name: 'Coding Tools' }).click()
await expect.poll(() => settings.getByRole('switch', { name: 'Coding Tools' }).getAttribute('aria-checked')).toBe('true')
await successShot(page, 'developer-tools-setting')
await settings.getByRole('button', { name: 'Close', exact: true }).click()
await expect.poll(() => iframe.getAttribute('sandbox')).toBe('allow-scripts')
@@ -22,8 +22,8 @@
- button "标准"
- text: 性能与用量 选择性能与用量信息展示的详细程度
- button "详细"
- text: 开发者工具 显示用于调试和排查问题的工具与信息
- switch "开发者工具" [checked]
- text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换
- switch "代码工作工具" [checked]
- text: 快捷键
- paragraph: 查看当前可用的快捷键和输入操作
- button "查看快捷键"
@@ -22,8 +22,8 @@
- button "Standard"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Developer tools Show tools and information for debugging and troubleshooting
- switch "Developer tools" [checked]
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools" [checked]
- text: Keyboard shortcuts
- paragraph: Browse available shortcuts and input actions
- button "View shortcuts"
@@ -24,8 +24,8 @@
- button "查看 Agent 循环": Agent 循环
- text: 控制 Agent 派发工具调用的方式。
- listitem:
- button "查看 Subagent": Subagent
- text: 设置 Subagent 的递归层级、数量和模型。
- button "查看 子智能体": 子智能体
- text: 设置子智能体的递归层级、数量和模型。
- listitem:
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
@@ -1,6 +1,6 @@
- button "返回插件列表": 插件列表
- heading "Subagent" [level=3]
- paragraph: 设置 Subagent 的递归层级、数量和模型。
- heading "子智能体" [level=3]
- paragraph: 设置子智能体的递归层级、数量和模型。
- region "运行限制":
- heading "运行限制" [level=3]
- text: 最大递归深度
@@ -10,16 +10,16 @@
- textbox "最大递归深度":
- /placeholder: ""
- text: "2"
- text: Subagent 并行数量上限
- button "Subagent 并行数量上限说明"
- text: 子智能体并行数量上限
- button "子智能体并行数量上限说明"
- text: 已覆盖
- button "恢复默认"
- textbox "Subagent 并行数量上限":
- textbox "子智能体并行数量上限":
- /placeholder: ""
- text: "12"
- region "模型选择":
- heading "模型选择" [level=3]
- text: 允许 Agent 为 Subagent 选择模型
- switch "允许 Agent 为 Subagent 选择模型"
- paragraph: 关闭后,Subagent 使用配置的默认模型或继承父 Agent 的模型;已选模型会保留。
- text: 允许 Agent 为子智能体选择模型
- switch "允许 Agent 为子智能体选择模型"
- paragraph: 关闭后,子智能体使用配置的默认模型或继承父 Agent 的模型;已选模型会保留。
- button "保存" [disabled]
@@ -24,8 +24,8 @@
- button "查看 Agent 循环": Agent 循环
- text: 控制 Agent 派发工具调用的方式。
- listitem:
- button "查看 Subagent": Subagent
- text: 设置 Subagent 的递归层级、数量和模型。
- button "查看 子智能体": 子智能体
- text: 设置子智能体的递归层级、数量和模型。
- listitem:
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
@@ -24,8 +24,8 @@
- button "查看 Agent 循环": Agent 循环
- text: 控制 Agent 派发工具调用的方式。
- listitem:
- button "查看 Subagent": Subagent
- text: 设置 Subagent 的递归层级、数量和模型。
- button "查看 子智能体": 子智能体
- text: 设置子智能体的递归层级、数量和模型。
- listitem:
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
@@ -22,8 +22,8 @@
- button "Standard"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Developer tools Show tools and information for debugging and troubleshooting
- switch "Developer tools"
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools"
- text: Keyboard shortcuts
- paragraph: Browse available shortcuts and input actions
- button "View shortcuts"
@@ -22,8 +22,8 @@
- button "Standard"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Developer tools Show tools and information for debugging and troubleshooting
- switch "Developer tools"
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools"
- text: Keyboard shortcuts
- paragraph: Browse available shortcuts and input actions
- button "View shortcuts"
@@ -22,8 +22,8 @@
- button "标准"
- text: 性能与用量 选择性能与用量信息展示的详细程度
- button "详细"
- text: 开发者工具 显示用于调试和排查问题的工具与信息
- switch "开发者工具"
- text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换
- switch "代码工作工具"
- text: 快捷键
- paragraph: 查看当前可用的快捷键和输入操作
- button "查看快捷键"
@@ -0,0 +1,4 @@
# Browser-zone assertions need a fresh reading after the non-browser preparation turn.
- id: time-context
config:
refreshIntervalMs: 0
+15 -15
View File
@@ -94,7 +94,7 @@ describe('web e2e: plugin configuration pages', () => {
const official = panel.locator('[data-plugin-group="official"]')
expect(await official.locator('[data-plugin-package]').count()).toBe(OPTIONAL_BUNDLES.length)
expect(await official.locator('[data-plugin-item]').count()).toBe(4)
for (const title of ['终端', 'Agent 循环', 'Subagent', '网页搜索']) {
for (const title of ['终端', 'Agent 循环', '子智能体', '网页搜索']) {
expect(await official.getByRole('button', { name: `查看 ${title}`, exact: true }).count()).toBe(1)
}
// A card carries the one-liner; the fields wait for the page.
@@ -108,9 +108,9 @@ describe('web e2e: plugin configuration pages', () => {
it('saves subagent limits and resets them to the deployment defaults', async () => {
const panel = await openPlugins()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
const depth = panel.getByLabel('最大递归深度', { exact: true })
const capacity = panel.getByLabel('Subagent 并行数量上限', { exact: true })
const capacity = panel.getByLabel('子智能体并行数量上限', { exact: true })
expect(await depth.inputValue()).toBe('1')
expect(await capacity.inputValue()).toBe('8')
await depth.fill('2')
@@ -120,7 +120,7 @@ describe('web e2e: plugin configuration pages', () => {
await expect.poll(settingsDocument).toContain('maxActiveSubagents: 12')
await expect.poll(settingsDocument).toContain('maxDepth: 2')
await openPlugins()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
const snapshot = await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd)
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'subagent.expected.md'), snapshot, MODE)
const controlHeight = await depth.evaluate(element => element.getBoundingClientRect().height)
@@ -133,7 +133,7 @@ describe('web e2e: plugin configuration pages', () => {
await panel.getByRole('button', { name: '保存', exact: true }).click()
await expect.poll(() => panel.getByRole('button', { name: '保存', exact: true }).isDisabled()).toBe(true)
await openPlugins()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
expect(await depth.inputValue()).toBe('1')
expect(await capacity.inputValue()).toBe('8')
await panel.getByRole('button', { name: '返回插件列表', exact: true }).click()
@@ -141,7 +141,7 @@ describe('web e2e: plugin configuration pages', () => {
it('opens field explanations with the keyboard and retains unsaved edits', async () => {
const panel = await openPlugins()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
const depth = panel.getByLabel('最大递归深度', { exact: true })
await depth.fill('2')
const depthHelp = panel.getByRole('button', { name: '最大递归深度说明', exact: true })
@@ -149,27 +149,27 @@ describe('web e2e: plugin configuration pages', () => {
await depthHelp.press('Enter')
const depthRules = panel.getByRole('region', { name: '最大递归深度说明', exact: true })
await depthRules.waitFor()
expect(await depthRules.getByText('限制 Agent 创建 Subagent 的递归层级。', { exact: true }).count()).toBe(1)
expect(await depthRules.getByText('限制 Agent 创建子智能体的递归层级。', { exact: true }).count()).toBe(1)
const depthTable = depthRules.getByRole('table', { name: '最大递归深度说明', exact: true })
expect(await depthTable.getByRole('row', { name: '0 禁用 Subagent', exact: true }).count()).toBe(1)
expect(await depthTable.getByRole('row', { name: '1 仅允许主 Agent 创建 Subagent', exact: true }).count()).toBe(1)
expect(await depthTable.getByRole('row', { name: '0 禁用子智能体', exact: true }).count()).toBe(1)
expect(await depthTable.getByRole('row', { name: '1 仅允许主 Agent 创建子智能体', exact: true }).count()).toBe(1)
expect(await depthRules.getByText('如果某个工具单独设置了最大递归深度,以该工具的设置为准。', { exact: true }).count()).toBe(1)
await depthHelp.press('Enter')
expect(await depthRules.count()).toBe(0)
expect(await depth.inputValue()).toBe('2')
await panel.getByRole('button', { name: 'Subagent 并行数量上限说明', exact: true }).click()
const capacityRules = panel.getByRole('region', { name: 'Subagent 并行数量上限说明', exact: true })
expect(await capacityRules.getByText('同一主 Agent 下,所有递归层级同时存活的 Subagent 总数,主 Agent 不计入。达到上限时,新的启动请求会被拒绝。', { exact: true }).count()).toBe(1)
await panel.getByRole('button', { name: '子智能体并行数量上限说明', exact: true }).click()
const capacityRules = panel.getByRole('region', { name: '子智能体并行数量上限说明', exact: true })
expect(await capacityRules.getByText('同一主 Agent 下,所有递归层级同时存活的子智能体总数,主 Agent 不计入。达到上限时,新的启动请求会被拒绝。', { exact: true }).count()).toBe(1)
await panel.getByRole('button', { name: '返回插件列表', exact: true }).click()
await openPage(panel, 'Subagent')
await openPage(panel, '子智能体')
expect(await depth.inputValue()).toBe('1')
})
it('saves limits and the model allowlist together from the shared card', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-subagent-model-selection'))
const panel = await openPlugins()
await openPage(panel, 'Subagent')
const toggle = panel.getByRole('switch', { name: '允许 Agent 为 Subagent 选择模型' })
await openPage(panel, '子智能体')
const toggle = panel.getByRole('switch', { name: '允许 Agent 为子智能体选择模型' })
await panel.getByLabel('最大递归深度', { exact: true }).fill('2')
await toggle.click()
+3 -1
View File
@@ -271,7 +271,9 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
const everyAdapter = new EveryReminderAdapter()
beforeAll(async () => {
scaffold = await launchWebScaffold()
scaffold = await launchWebScaffold({
extraOverlayPath: fileURLToPath(new URL('./fixtures/time-context-every-step.patch.yml', import.meta.url)),
})
scaffold.ctx.effect(
() => scaffold.ctx.llm.registerAdapter([AFTER_PROVIDER], afterAdapter),
'Schedule Web After adapter',
+1 -1
View File
@@ -3381,7 +3381,7 @@ export type ShellDialect = 'bash' | 'pwsh'
export interface Config {
/** Fallback display zone when the open turn has no unique browser zone. Omit to use the process zone. */
timeZone?: string
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject at every eligible step. */
/** Minimum milliseconds between durable injections in one session. Defaults to 600000 (10 minutes); 0 injects at every eligible step. */
refreshIntervalMs?: number
}
```
+1 -1
View File
@@ -3383,7 +3383,7 @@ export type ShellDialect = 'bash' | 'pwsh'
export interface Config {
/** Fallback display zone when the open turn has no unique browser zone. Omit to use the process zone. */
timeZone?: string
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject at every eligible step. */
/** Minimum milliseconds between durable injections in one session. Defaults to 600000 (10 minutes); 0 injects at every eligible step. */
refreshIntervalMs?: number
}
```
+1 -1
View File
@@ -28,7 +28,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:52`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:106`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) |
+1 -1
View File
@@ -30,7 +30,7 @@
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:52`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
| `compaction/summary-error` | `waterfall` | [`packages/compaction/compaction/src/index.ts:106`](../packages/compaction/compaction/src/index.ts) | [`compaction-basic`](../packages/compaction/compaction-basic) (`waterfall`) | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) |
+2 -2
View File
@@ -12,8 +12,8 @@
en: 5e51c6e37c5e30c3
zh: 84bad36357332539
/user-approval/approval-request:
en: dc3647525133ff2f
zh: 8365b809a93ec81d
en: 044ddfc382eb745c
zh: c0010b23c94ab3af
/user-approval/dispatch-and-audit:
en: bd2c2cc1eaaaf2d6
zh: 67bfbed1b7499ffb
+1 -1
View File
@@ -50,7 +50,7 @@ Both policies contribute their complete current meaning to the cache-safe runtim
## Approval request
`ApprovalRequest` identifies the agent and tool action closely enough to route and audit the question. It deliberately omits tool arguments: an answerer attaches the prompt to the already-streamed tool call through `callId` instead of rendering a second copy that could drift.
`ApprovalRequest` identifies the agent and tool action closely enough to route and audit the question. It deliberately omits tool arguments: an answerer attaches the prompt to the already-streamed tool call through `callId` instead of rendering a second copy that could drift. Optional `displayReason` carries requester-owned locale strings to presentation; `reason` remains the audit text.
```ts type-equiv
/**
+1 -1
View File
@@ -50,7 +50,7 @@ type ApprovalPolicy = 'ask' | 'never'
## 审批请求
`ApprovalRequest` 以足够精确的方式标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而非渲染另一份可能漂移的副本。
`ApprovalRequest` 以足够精确的方式标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而非渲染另一份可能漂移的副本。可选的 `displayReason` 向界面传递请求方提供的本地化字符串;`reason` 仍是审计文本。
```ts type-equiv
/**
+2 -2
View File
@@ -24,8 +24,8 @@
en: c063711f8247c240
zh: 5e54758bbaf1670c
/web-client-architecture/conversation-and-presentation:
en: a5fac31f9a3e6e97
zh: 01d7c4e286921360
en: 9e0d6a3dfdbc4c12
zh: 0eea67d515ec56ef
/web-client-architecture/data-paths:
en: a254278432416473
zh: 567c2d3c6a20b304
+1 -1
View File
@@ -55,7 +55,7 @@ This pairing is not a second source of business truth. Host controllers decide d
## Conversation and presentation
Web and desktop share the [developer-tool preference](../../packages/client/ui-settings/README.md#use-this-package). It controls diagnostic Views, new-session preset selection, changed-file cards, and the builtin HTML preview policy without changing Session records.
Web and desktop share the [Coding Tools preference](../../packages/client/ui-settings/README.md#use-this-package). It controls diagnostic Views, new-session preset selection, changed-file cards, and the builtin HTML preview policy without changing Session records.
`ui-session` installs the Session scope adapter and publishes `useSessions`, `useSessionStatus`, `useSessionRetainInfo`, `useSession`, `sessionId`, and `useProjection`. `SessionProvider` inherits an outer binding or binds an explicit `SessionReference`, so concurrent subtrees can target different Sessions. Domain adapters add further standard sources without putting React hooks on the model objects.
+1 -1
View File
@@ -55,7 +55,7 @@ Connection 拥有请求 URL 解析、request correlation、`/api` carrier、trus
## Conversation 与 presentation
Web 和桌面端共享[开发者工具偏好](../../packages/client/ui-settings/README.zh.md#use-this-package)。它控制诊断 View、新会话预设选择、改动文件卡片和内置 HTML 预览策略,不改变 Session 记录。
Web 和桌面端共享[代码工作工具偏好](../../packages/client/ui-settings/README.zh.md#use-this-package)。它控制诊断 View、新会话预设选择、改动文件卡片和内置 HTML 预览策略,不改变 Session 记录。
`ui-session` 安装 Session scope adapter,并提供 `useSessions`、`useSessionStatus`、`useSessionRetainInfo`、`useSession`、`sessionId` 和 `useProjection`。`SessionProvider` 可以继承外围 binding,也可以绑定显式 `SessionReference`,因此并存子树可以指向不同 Session。领域 adapter 可以继续添加标准 source,但不会把 React hook 放进 model object。
+1 -1
View File
@@ -551,7 +551,7 @@ Execute a TypeScript program against the available tools. Takes two required arg
},
"justification": {
"type": "string",
"description": "Reason this complete program needs wider access, shown to the user for approval."
"description": "Reason this complete program needs wider access, shown to the user for approval. Use the language of the user’s current request."
}
},
"required": [
+1 -1
View File
@@ -555,7 +555,7 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类
},
"justification": {
"type": "string",
"description": "Reason this complete program needs wider access, shown to the user for approval."
"description": "Reason this complete program needs wider access, shown to the user for approval. Use the language of the user’s current request."
}
},
"required": [
+1 -1
View File
@@ -16,7 +16,7 @@
zh: a8a3dceee2c9e943
/client-web-gui-browser-half/packages:
en: 2a964f7daf3586d5
zh: 726fe18e86157978
zh: 2ca0adf9479a1ac5
/client-web-gui-browser-half/related-documentation:
en: 598d07b07449128f
zh: e8c40708ac0e603e
+1 -1
View File
@@ -77,7 +77,7 @@ kind: "package-group"
| [`ui-settings-models/`](ui-settings-models/README.zh.md) | 提供模型提供方配置与 DeepSeek 引导 | — |
| [`ui-settings-shell/`](ui-settings-shell/README.zh.md) | 在插件页提供终端设置页 | — |
| [`ui-settings-agent-loop/`](ui-settings-agent-loop/README.zh.md) | 在插件页提供 Agent 循环设置页 | — |
| [`ui-settings-subagent/`](ui-settings-subagent/README.zh.md) | 在插件页提供 Subagent 设置页 | — |
| [`ui-settings-subagent/`](ui-settings-subagent/README.zh.md) | 在插件页提供子智能体设置页 | — |
| [`ui-settings-web-search/`](ui-settings-web-search/README.zh.md) | 在插件页提供网页搜索设置页 | — |
| [`ui-plugin-manager/`](ui-plugin-manager/README.zh.md) | 贡献侧栏的“插件”面板:安装、启用、停用、重试与组合已安装的包 | — |
| [`ui-settings-plugin-inventory/`](ui-settings-plugin-inventory/README.zh.md) | 向「插件」设置贡献只读的 Host Loader 清单标签页 | — |
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: b3ad7867f23dad1c
/deepseek-ai-dsh-client-ui-agent-preset/use-this-package:
en: 1b1af8f5a2ac6b01
zh: 5ad3daeead8040df
en: cf381c4f00b42c3f
zh: 1b226687dcbd4875
/deepseek-ai-dsh-client-ui-agent-preset/understand-the-implementation:
en: 7813f0063c15e241
zh: 6eabc242edfc45ec
+1 -1
View File
@@ -27,7 +27,7 @@ Choose Agent presets and the new-task default in Web, read what each mode does a
Settings shows the built-in and custom card groups with default highlighting and card-body selection; a group without presets is omitted, except the custom group, which keeps its Creator entry on screen. Every card offers “View configuration”, which opens the preset's declared plugin list as read-only YAML in the Loader's own dialect (`!!js` conditions included); a failed preset stays readable because its diagnostic points into that YAML. Escape closes only the viewer and returns focus to its card; leaving Settings clears the viewer, and a late read does not reopen it. The page edits nothing: the Creator entry starts a Creator-mode task that authors or overrides a preset as a bundle, offered while the `cordis` preset is on the roster and a conversation flow exists.
Developer tools in General Settings decide whether a mode can be chosen at all: with them off the new-session picker disappears and the cards refuse selection, while the saved default keeps composing new tasks. Choosing a healthy default also synchronizes the blank session on the current new-task surface. Creator starts a new task using the `cordis` preset.
Coding Tools in General Settings decide whether a mode can be chosen at all: with them off the new-session picker disappears and the cards refuse selection, while the saved default keeps composing new tasks. Choosing a healthy default also synchronizes the blank session on the current new-task surface. Creator starts a new task using the `cordis` preset.
Known shipped presets offer mode details and usage examples in a read-only dialog. Its tabs preserve each page's scroll position; closing returns focus to the opening action. Help does not change the new-task default. The default badge replaces the card's group badge, and the preset id appears beside the title. Guide copy and examples belong to this package.
+1 -1
View File
@@ -27,7 +27,7 @@ kind: "package-reference"
设置页显示内置与自定义卡片分组、默认项高亮和点击卡片选择;没有 preset 的分组不显示,但自定义分组会保留其创造入口。每张卡片都提供「查看配置」,以只读 YAML 打开该 preset 声明的插件列表,使用 Loader 自己的方言(含 `!!js` 条件);加载失败的 preset 同样可读,因为其诊断信息正指向这份 YAML。Escape 只关闭查看器并将焦点还给卡片;离开设置页会清空查看器,迟到的读取结果不会重新打开它。本页不编辑任何内容:创造入口启动一个创造模式任务,以 bundle 形式创建或覆盖 preset;当 `cordis` preset 在列表中且存在会话流程时提供。
通用设置中的开发者工具决定能否选择模式:关闭后新会话选择器消失、卡片拒绝选择,而已保存的默认值继续用于新任务。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。
通用设置中的代码工作工具决定能否选择模式:关闭后新会话选择器消失、卡片拒绝选择,而已保存的默认值继续用于新任务。选择健康定义作为默认值也会同步当前新任务页面的空白会话。Creator 入口开启一个使用 `cordis` preset 的新任务。
已知的内置预设提供只读的模式说明与使用示例对话框。各页签保留各自的滚动位置;关闭后焦点回到打开它的操作。帮助不会改变新任务默认值。默认徽标取代卡片的分组徽标,预设 id 显示在标题旁。指南文案与示例归本包所有。
@@ -67,8 +67,8 @@ export const en: Record<AgentPresetSettingsKey, string> = {
creatorDraft: 'Let the agent help me create a preset',
enableDevToolsToSetDefault: 'Turn on Developer tools in General settings to choose a default',
enableDevToolsToCreate: 'Turn on Developer tools in General settings to start Creator mode',
enableDevToolsToSetDefault: 'Turn on Coding Tools in General settings to choose a default',
enableDevToolsToCreate: 'Turn on Coding Tools in General settings to start Creator mode',
}
/** Simplified Chinese copy. */
@@ -104,8 +104,8 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
creatorDraft: '让 Agent 帮我创建预设模式',
enableDevToolsToSetDefault: '请先在通用设置中开启开发者工具,再设置默认值',
enableDevToolsToCreate: '请先在通用设置中开启开发者工具,再启动创造模式',
enableDevToolsToSetDefault: '请先在通用设置中开启代码工作工具,再设置默认值',
enableDevToolsToCreate: '请先在通用设置中开启代码工作工具,再启动创造模式',
}
// The resolution itself is the shared fold in `dsh-agent-preset-registry/display`,
+2 -2
View File
@@ -21,8 +21,8 @@
en: adf7112e0691b89e
zh: 14eeddcff985119f
/deepseek-ai-dsh-client-ui-approval/known-limitations-and-deferred-work:
en: 158f435625e41ca9
zh: 9d58169aaf6c7ed7
en: ae644102ed52f7ee
zh: e109de1d517a3a94
/deepseek-ai-dsh-client-ui-approval/known-limitations-and-deferred-work/dev-note:
en: 240c92ce5e544116
zh: 5bfae598cc20a3af
+1 -1
View File
@@ -33,7 +33,7 @@ None; approval request and response rendering does not alter a model request.
<a id="known-limitations-and-deferred-work"></a>
- **The panel exposes transient decisions only** — it supports allow-once and reject; persistent permission policy remains owned by Host-side approval packages.
- **The panel exposes transient decisions only** — it supports allow-once and reject; persistent permission policy remains owned by Host-side approval packages. Requester-supplied localized presentation copy follows the UI language without changing the audit reason or translating model-generated text.
<a id="dev-note"></a>
+1 -1
View File
@@ -33,7 +33,7 @@ kind: "package-reference"
<a id="known-limitations-and-deferred-work"></a>
- **面板只提供临时决定**——它支持仅本次允许和拒绝;持久权限策略仍由 Host 侧审批包拥有。
- **面板只提供临时决定**——它支持仅本次允许和拒绝;持久权限策略仍由 Host 侧审批包拥有。请求方提供的本地化展示文案跟随界面语言,不改写审计原因,也不翻译模型生成的文本。
<a id="dev-note"></a>
@@ -14,11 +14,13 @@ export function ApprovalPanel(props: ApprovalComposerProps) {
const detail = approval.callId === undefined
? null
: props.renderSlot('conversation.approval.detail', { callId: approval.callId })
return <ApprovalFlow key={approval.key} pending={approval} detail={detail} t={props.t} />
const reason = approval.displayReason === undefined ? approval.reason : props.resolveReason(approval.displayReason)
return <ApprovalFlow key={approval.key} pending={approval} reason={reason} detail={detail} t={props.t} />
}
function ApprovalFlow({ pending, detail, t }: {
function ApprovalFlow({ pending, reason, detail, t }: {
pending: PendingApproval
reason: string | undefined
detail: ReactNode
t: ApprovalComposerProps['t']
}) {
@@ -69,7 +71,7 @@ function ApprovalFlow({ pending, detail, t }: {
role="group"
aria-label={t('detail.aria')}
>
<div className={css.headline}>{pending.reason ?? t('escalation', { toolName: pending.toolName })}</div>
<div className={css.headline}>{reason ?? t('escalation', { toolName: pending.toolName })}</div>
{detail !== null && <div className={css.command}>{detail}</div>}
</div>
<div className={css.actionRow}>
@@ -56,6 +56,8 @@ export interface ApprovalPresentationRequest {
readonly callId?: ToolCallId
/** Human-readable reason supplied by the requester. */
readonly reason?: string
/** Localized presentation copy; the audit reason remains unchanged. */
readonly displayReason?: { readonly en: string; readonly [locale: string]: string }
/** Cancellation projected from the Host waterfall. */
readonly signal?: AbortSignal
}
@@ -80,6 +82,8 @@ export class PendingApproval {
readonly callId: ToolCallId | undefined
/** Human-readable reason supplied by the asker. */
readonly reason: string | undefined
/** Localized presentation copy, when supplied by the asker. */
readonly displayReason: ApprovalPresentationRequest['displayReason']
/** Result returned by the Remote Event listener to the Host waterfall. */
readonly result: Promise<ApprovalDecision>
@@ -101,6 +105,7 @@ export class PendingApproval {
this.toolName = request.toolName
this.callId = request.callId
this.reason = request.reason
this.displayReason = request.displayReason
const completion = Promise.withResolvers<ApprovalDecision>()
this.result = completion.promise
this.#resolve = completion.resolve
@@ -170,9 +175,20 @@ export class PendingApproval {
}
}
/** Locale lookup supplied by the approval registration. */
export interface ApprovalInjected {
/**
* Resolve requester-owned copy in the current UI language.
* @param reason - localized presentation text with an English fallback.
* @returns text for the active UI language.
*/
resolveReason(reason: NonNullable<ApprovalPresentationRequest['displayReason']>): string
}
/** Full props of the approval composer takeover. */
export type ApprovalComposerProps =
PropsRuntime<'conversation.composer'>
& PropsRenderSlots<'conversation.approval.detail'>
& { matched: PendingApproval }
& PropsLocale<'approval'>
& ApprovalInjected
@@ -48,6 +48,7 @@ async function answerApproval(
? {}
: { callId: request.callId }),
...(request.reason === undefined ? {} : { reason: request.reason }),
...(request.displayReason === undefined ? {} : { displayReason: request.displayReason }),
...(request.signal === undefined ? {} : { signal: request.signal }),
})
const completed = Promise.withResolvers<void>()
@@ -93,6 +94,9 @@ export function apply(ctx: ClientContext): void {
select: ({ pendingInteraction }: ComposerChainProps): PendingApproval | null =>
pendingInteraction instanceof PendingApproval ? pendingInteraction : null,
locale: NS,
inject: () => ({
resolveReason: (reason: NonNullable<PendingApproval['displayReason']>) => ctx.locale.resolveText(reason),
}),
children: {
'conversation.approval.detail': { kind: 'single', scope: 'session' },
},
@@ -6,7 +6,7 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ApprovalPanel } from '../src/client/ApprovalPanel.tsx'
import type { ApprovalComposerProps } from '../src/client/contract/slots.ts'
import type { ApprovalComposerProps, ApprovalInjected } from '../src/client/contract/slots.ts'
import { PendingApproval } from '../src/client/contract/slots.ts'
import { apply, inject } from '../src/client/index.ts'
import { apply as nodeApply } from '../src/index.ts'
@@ -17,6 +17,7 @@ type ApprovalListener = (
toolName: string
callId?: string
reason?: string
displayReason?: PendingApproval['displayReason']
signal?: AbortSignal
},
next: () => Promise<'unavailable'>,
@@ -35,6 +36,7 @@ interface PluginBench {
registration(): {
options: {
select(props: { pendingInteraction: PendingApproval | undefined }): PendingApproval | null
inject(): ApprovalInjected
}
component: unknown
}
@@ -46,6 +48,7 @@ async function setupPlugin(): Promise<PluginBench> {
let registration: {
options: {
select(props: { pendingInteraction: PendingApproval | undefined }): PendingApproval | null
inject(): ApprovalInjected
}
component: unknown
} | undefined
@@ -82,6 +85,7 @@ async function setupPlugin(): Promise<PluginBench> {
ctx.provide('slots', { inject: injectSlot, register } as never)
ctx.provide('locale', {
register: vi.fn(() => disposeLocale),
resolveText: (reason: NonNullable<PendingApproval['displayReason']>) => reason.en,
} as never)
const fiber = ctx.plugin({ apply })
@@ -219,12 +223,15 @@ describe('approval Remote Event consumer', () => {
toolName: 'bash',
callId: 'call-1',
reason: 'needs access',
displayReason: { en: 'Display reason', zh: '展示原因' },
signal: controller.signal,
}, next)
const pending = bench.pending.getSnapshot()[0]!
const { options, component } = bench.registration()
expect(component).toBe(ApprovalPanel)
expect(pending.displayReason).toEqual({ en: 'Display reason', zh: '展示原因' })
expect(options.inject().resolveReason(pending.displayReason!)).toBe('Display reason')
expect(options.select({ pendingInteraction: undefined })).toBeNull()
expect(options.select({ pendingInteraction: pending })).toBe(pending)
expect(pending).toMatchObject({
@@ -319,8 +326,9 @@ function panelProps(
return {
matched: pending,
renderSlot,
resolveReason: (reason: NonNullable<PendingApproval['displayReason']>) => reason.en,
t: (key: string) => messages[key] ?? key,
} as unknown as ApprovalComposerProps
} as ApprovalComposerProps
}
describe('ApprovalPanel', () => {
@@ -362,6 +370,21 @@ describe('ApprovalPanel', () => {
await expect(pending.result).resolves.toBe('allowed-once')
})
it('keeps the audit reason intact and follows the UI language for presentation copy', () => {
const pending = new PendingApproval(id('s1'), {
toolName: 'bash',
reason: 'audit reason',
displayReason: { en: 'English explanation', zh: '中文说明' },
})
const props = panelProps(pending)
const view = render(<ApprovalPanel {...props} />)
expect(screen.getByText('English explanation')).toBeTruthy()
expect(screen.queryByText('audit reason')).toBeNull()
view.rerender(<ApprovalPanel {...props} resolveReason={reason => reason['zh']!} />)
expect(screen.getByText('中文说明')).toBeTruthy()
expect(pending.reason).toBe('audit reason')
})
it.each([['Enter', 'allowed-once'], ['Escape', 'rejected']] as const)(
'answers the focused container with %s through the pending request', async (key, outcome) => {
const pending = new PendingApproval(id('s1'), { toolName: 'bash' })
@@ -18,8 +18,8 @@
en: 13ae68865fa315d7
zh: c152f020abf20f35
/deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props:
en: 8de4fbd9df016e06
zh: 06b15ab7855120cb
en: 72a6f8429c1ab3d0
zh: ae76012917c96dcf
/deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries:
en: bb1244310f5c0382
zh: 824b84077432b559
+1 -1
View File
@@ -48,7 +48,7 @@ The composer measures its expanded control groups after size, content, visibilit
The context-occupancy button shows a ring and percentage below the input card, after the Session statistics. Clicking it opens the token breakdown in a panel kept inside the viewport, including when no statistics are shown; the button stays hidden until context usage and capacity are available.
With Developer tools off, the shell hides only Trajectory; other plugin-contributed Views remain available. The View tab bar is hidden when fewer than two Views are available. Turning Developer tools off while Trajectory is active returns to Chat; stored View preferences and recorded Session data remain intact. Turning it on makes Trajectory available again. View owners receive the available roster so their navigation actions can follow the same visibility.
With Coding Tools off, the shell hides only Trajectory; other plugin-contributed Views remain available. The View tab bar is hidden when fewer than two Views are available. Turning Coding Tools off while Trajectory is active returns to Chat; stored View preferences and recorded Session data remain intact. Turning it on makes Trajectory available again. View owners receive the available roster so their navigation actions can follow the same visibility.
The composer registers the File command action and owns its label, availability, and native file-dialog callback. Menu availability and invocation both consult the mounted composer's current attachment-intake policy. Unmounting or locking the composer disables that action; disposing the plugin removes its registration. The callback binding stays inside the input module.
+1 -1
View File
@@ -48,7 +48,7 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map,
上下文占用按钮在输入卡片下方、会话统计右侧显示圆环和百分比。点击按钮可在视口内的面板查看 token 构成,没有统计项时面板也不会越界;上下文用量和容量尚不可用时,按钮保持隐藏。
关闭开发者工具时,外壳仅隐藏 Trajectory;其他插件贡献的 View 仍然可用。可用 View 少于两个时隐藏 View 标签栏。在 Trajectory 激活时关闭开发者工具会返回 Chat;已保存的 View 偏好和 Session 记录保持不变。开启后,Trajectory 恢复可用。View 所有者接收可用列表,使导航操作跟随相同的可见性。
关闭代码工作工具时,外壳仅隐藏 Trajectory;其他插件贡献的 View 仍然可用。可用 View 少于两个时隐藏 View 标签栏。在 Trajectory 激活时关闭代码工作工具会返回 Chat;已保存的 View 偏好和 Session 记录保持不变。开启后,Trajectory 恢复可用。View 所有者接收可用列表,使导航操作跟随相同的可见性。
输入框注册「文件」命令动作,负责其标题、可用性和原生文件选择器回调。菜单可用性与实际调用都读取已挂载输入框当前的附件接收策略。输入框卸载或锁定后该动作不可用,插件 dispose(资源释放)时移除注册。回调绑定留在输入模块内部。
@@ -21,8 +21,8 @@
en: 0ee4bbe45bb5e473
zh: 9c99d829b25f59ba
/deepseek-ai-dsh-client-ui-deliverables/use-this-package/the-changed-files-card:
en: 18b37ee7d57ffbaa
zh: 639d09873f292904
en: 4b279e2bddddc083
zh: a3135393299c935b
/deepseek-ai-dsh-client-ui-deliverables/use-this-package/the-review-tab:
en: 231547659321f3a8
zh: bc5935cdd46ada56
@@ -42,8 +42,8 @@
en: eb244aa21a4e5210
zh: e3450addfa19cea5
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance/what-the-model-sees:
en: 627b3e8b57458c7a
zh: 8300f74182ff4508
en: 78339e353ae47a53
zh: 582efa623a5dfd72
/deepseek-ai-dsh-client-ui-deliverables/model-experience/clickable-file-reference-guidance/token-effect:
en: 66bd094b37a29c22
zh: e657d66ac6f95a22
+2 -2
View File
@@ -40,7 +40,7 @@ The `present` tool row retains its document icon and recorded result across prog
### The changed-files card
Developer tools must be enabled to show this card or request its summary. Turning the preference off removes the card immediately while explicit delivery cards, inline file links, and recorded workspace changes remain available.
Coding Tools must be enabled to show this card or request its summary. Turning the preference off removes the card immediately while explicit delivery cards, inline file links, and recorded workspace changes remain available.
The card renders the summary the Host serves for the turn's latest `workspace/changes` announcement, read once per announcement through the authenticated summary route; while the read is pending, once the Host answers that the summary is gone, or when it lists no file, there is no card. A single changed file shows “Edited” and its basename with its file-type icon, without a file list; its header retains the counts or binary/oversized status and the file’s accessible full path. Multiple files use a code-file icon and a header naming the complete changed-file count and summed added and deleted lines; hovering or focusing it replaces the counts with “Preview in sidebar”. Each row shows one file's display path in the regular UI font with its own monospace counts, “binary” for a binary file, or “too large” for a file the Host did not capture. Rows appear in the recorded display order, so repository files above the working directory and files outside it sort first. Four rows show before a fold; the control reveals every recorded file and, once expanded, collapses the list again from the bottom. Each row opens the turn's review in the right Sidebar on that file, and the header opens it on the first file. Hovering a file row or the single-file header for 500ms opens a scrollable, single-column preview of the comparison, independent of the Sidebar’s view. Its full path uses muted monospace text and scrolls horizontally when needed. File-status notes accompanying code and hunk headers are hidden in the preview; comparisons without hunks retain their status text. Each row exposes the full path as its accessible description. The preview is 48px narrower than the card, inset 24px on each side, and at most 420px high, further constrained by the viewport. The whole preview fades in and out over 100ms. Moving into the preview keeps it open; leaving it, pressing Escape, or activating the row by pointer or keyboard dismisses it. Comparisons load only when the preview opens and share the review tab’s cache and retry states.
@@ -93,7 +93,7 @@ Read these pages when the deliverables surface is not enough. They move from the
#### 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 developer 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 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`.
#### Token effect
+2 -2
View File
@@ -40,7 +40,7 @@ Web 的 `standard`、`ptc` 与 `cordis` preset 提供 `present`,用于声明
### 改动文件卡片
只有开启开发者工具才展示此卡片并请求其摘要。关闭该偏好会立即隐藏卡片,同时保留显式交付卡片、行内文件链接和已记录的工作区改动。
只有开启代码工作工具才展示此卡片并请求其摘要。关闭该偏好会立即隐藏卡片,同时保留显式交付卡片、行内文件链接和已记录的工作区改动。
卡片渲染 Host 为本轮最新一条 `workspace/changes` 宣告提供的摘要,每条宣告通过经过认证的摘要路由读取一次;读取尚未完成、Host 答复摘要已不存在,或摘要没有列出任何文件时,没有卡片。仅改动一个文件时,标题显示“已编辑”和文件名,配合对应的文件类型图标,不展示文件列表;标题保留增删行数或二进制/过大状态,并通过无障碍描述提供完整路径。多个文件使用代码文件图标,标题给出改动文件总数与增删行数合计;鼠标悬停或键盘聚焦时,统计信息替换为“在侧边栏预览”。每一行使用普通界面字体显示文件路径,增删行数保留等宽字体,二进制文件显示“二进制”,Host 没有捕获的文件显示“过大”。行按记录的展示顺序排列,因此仓库内位于工作目录之上的文件与工作区外的文件排在最前。折叠前显示四行;控件展开全部记录文件,展开后从底部收起列表。每一行在右侧 Sidebar 中打开本轮的 review 并选中该文件,标题则在第一个文件上打开它。 在文件行或单文件标题上悬停 500ms 后,会出现可滚动的单列改动浮层,不受 Sidebar 视图设置影响。顶部完整路径使用较浅的等宽字体,超出宽度时可横向滚动。浮层不显示代码旁的文件状态说明和 hunk 头;没有 hunk 的对比仍显示状态文字。每行通过无障碍描述提供完整路径。浮层比卡片窄 48px,左右各内缩 24px,高度最多 420px,并受视口可用空间限制。整个浮层的淡入和淡出各持续 100ms。鼠标移入浮层后保持打开;移出、按 Escape 或通过鼠标和键盘激活文件行会关闭浮层。对比只在浮层打开时加载,与 review tab 共用缓存及重试状态。
@@ -93,7 +93,7 @@ Node 半部注册[模型体验](#model-experience)所述的静态 `ui:deliverabl
#### 模型看到的内容
提示词建议在最终回复中展示主要结果:内嵌图片用于讲解与对比,图片链接用于引用和文件列表,图片文件卡片用于独立交付。文件目标路径使用尖括号包裹,包括含空格的路径。对于需要完整文件的交付,尤其是 Office 文件,建议使用独立的 `present` 卡片,通常选最重要的一两项,需要时可以更多,但每次调用最多四项。提示词建议避免仅为列出代码改动生成卡片,也不为确认 diff 是否显示额外运行命令。开发者工具关闭时仍采用这条建议,接受较少的文件展示入口,以减少重复展示和不必要的工具调用。文件引用要求链接命令、配置表达式和代码块以外的每次现有文件提及,包括重复提及和表格。标签默认使用文件名或清楚的别名,仅添加足以区分文件的父目录。精确引用显示为 `filename:24` 或 `filename:24–30`;目标保留完整相对路径或绝对路径,以及 `#L24` 或 `#L24-L30` 锚点。显示后缀不含 `#` 或 `L`。
提示词建议在最终回复中展示主要结果:内嵌图片用于讲解与对比,图片链接用于引用和文件列表,图片文件卡片用于独立交付。文件目标路径使用尖括号包裹,包括含空格的路径。对于需要完整文件的交付,尤其是 Office 文件,建议使用独立的 `present` 卡片,通常选最重要的一两项,需要时可以更多,但每次调用最多四项。提示词建议避免仅为列出代码改动生成卡片,也不为确认 diff 是否显示额外运行命令。代码工作工具关闭时仍采用这条建议,接受较少的文件展示入口,以减少重复展示和不必要的工具调用。文件引用要求链接命令、配置表达式和代码块以外的每次现有文件提及,包括重复提及和表格。标签默认使用文件名或清楚的别名,仅添加足以区分文件的父目录。精确引用显示为 `filename:24` 或 `filename:24–30`;目标保留完整相对路径或绝对路径,以及 `#L24` 或 `#L24-L30` 锚点。显示后缀不含 `#` 或 `L`。
#### Token 影响
@@ -28,7 +28,7 @@
zh: a9429f047a12bb4c
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/configuration-pages:
en: c32019330fd9586d
zh: 98f146f33f2638ed
zh: dd22110abd10159c
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/detail-page-extension-points:
en: 87f6d96467dc2383
zh: e0fb91aa4c4d5a12
@@ -51,7 +51,7 @@ Host 将网络失败或超时归因于 GitHub 地址,且提供 npmmirror 时
### 配置页
自带配置的插件把配置渲染在本页而不是设置里,通过本页声明的三个 slot:`plugins.item`(list)用于官方插件,按其 `label` 列在官方分组里;`plugins.bundle.config`(以组合包的包名为键)用于组合包自己的配置,显示在组合包页面的描述与行之间;`plugins.row.config`(以 `<包名>#<行 id>` 为键)用于某一行的配置,这一行由此多出一个**配置**控件,打开该行自己的页面。页面用 `view: 'page'` 渲染带自己保存控件的表单。官方插件卡片还在标题下渲染 `view: 'summary'`;行详情页只在缺少包描述时使用该视图。只有保存才写入:页面负责画标题、图标与面包屑,条目的表单在离开页面时丢弃暂存的修改。安装随附的四个宿主平面配置页——shell 执行器、agent loop、Subagent、DeepSeek 搜索提供方——各来自一个伴生包:[ui-settings-shell](../ui-settings-shell/README.zh.md)、[ui-settings-agent-loop](../ui-settings-agent-loop/README.zh.md)、[ui-settings-subagent](../ui-settings-subagent/README.zh.md) 与 [ui-settings-web-search](../ui-settings-web-search/README.zh.md),在 Host 服务其命名空间期间注册。组合包的浏览器半侧用同样的方式注册:
自带配置的插件把配置渲染在本页而不是设置里,通过本页声明的三个 slot:`plugins.item`(list)用于官方插件,按其 `label` 列在官方分组里;`plugins.bundle.config`(以组合包的包名为键)用于组合包自己的配置,显示在组合包页面的描述与行之间;`plugins.row.config`(以 `<包名>#<行 id>` 为键)用于某一行的配置,这一行由此多出一个**配置**控件,打开该行自己的页面。页面用 `view: 'page'` 渲染带自己保存控件的表单。官方插件卡片还在标题下渲染 `view: 'summary'`;行详情页只在缺少包描述时使用该视图。只有保存才写入:页面负责画标题、图标与面包屑,条目的表单在离开页面时丢弃暂存的修改。安装随附的四个宿主平面配置页——shell 执行器、agent loop、子智能体、DeepSeek 搜索提供方——各来自一个伴生包:[ui-settings-shell](../ui-settings-shell/README.zh.md)、[ui-settings-agent-loop](../ui-settings-agent-loop/README.zh.md)、[ui-settings-subagent](../ui-settings-subagent/README.zh.md) 与 [ui-settings-web-search](../ui-settings-web-search/README.zh.md),在 Host 服务其命名空间期间注册。组合包的浏览器半侧用同样的方式注册:
```tsx ignore-check
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 405b30feaf48c087
/deepseek-ai-dsh-client-ui-schedule/use-this-package:
en: bfe2c9f38a9803c0
zh: 4d237730e2b7f748
en: 3594ab6704cbd7b9
zh: 99a965f43187501b
/deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source:
en: 6bd6882dbbbefcb0
zh: 03aa15f1a65d13dc
+1 -1
View File
@@ -33,7 +33,7 @@ Rules shows the rule in a Run time card whose rows edit a local draft: Repeat is
The rows are disabled while a save is in flight, while the catalog is loading, during a deletion, and for an inactive task. A save bar appears only while the local draft differs from the stored task: it shows Unsaved changes with Cancel and Save changes, and Save changes becomes Saving… while the save is in flight. Cancel restores the stored values. A catalog refresh becomes the shown values only while the draft has no unsaved edit, so an unsaved draft survives an authoritative refresh. A failed save keeps the draft and shows a localized message; after a conflict, retry against the refreshed record. If the task disappears from the catalog, Delete is disabled until the task returns. A successful update refreshes the authoritative catalog. A rejected or unconfirmed update is distinct from a catalog-read failure: Retry reloads the catalog without presenting the update as failed. If the update itself cannot be confirmed, check the task before retrying. Late responses after switching tasks or closing details do not replace the current view, and no failure rolls back a Host write already begun.
Opening Delivery records lazily requests the newest 20 saved records. Load older records adds another page in newest-first append order, independent of wall-clock rollback. The tab strip is the whole header of that view: the editable name and next-run line belong to Rules. Each Saved delivery record leads with a clock glyph and its occurrence time as the locale's month name and clock field pair in the task's zone, or the browser's zone for a one-shot record, and shows the actual saved prompt when present. A legacy receipt without a prompt never substitutes the current instruction.
Opening Delivery records lazily requests the newest 20 saved records. Load older records adds another page in newest-first append order, independent of wall-clock rollback. The tab strip is the whole header of that view: the editable name and next-run line belong to Rules. Each Saved delivery record leads with a clock glyph and its occurrence time as the locale's month name and clock field pair in the task's zone, or the browser's zone for a one-shot record, and shows the actual saved prompt when present. A prompt longer than two lines shows its first two lines and an Expand toggle, which Collapse reverses; a collapsed prompt rechecks its length when the panel width changes. A legacy receipt without a prompt never substitutes the current instruction.
After the last saved page loads, a nonempty history with confirmed pruning shows a borderless notice at the bottom left of the detail panel. Its info button expands or collapses the current Host retention rules. Empty history, legacy unavailability without confirmed pruning, incomplete pagination, and failed reads show no cleanup notice.
+1 -1
View File
@@ -33,7 +33,7 @@ kind: "package-reference"
行在保存进行中、目录加载中、删除期间以及任务未运行时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。
打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。旧回执没有提示文本时,绝不以当前指令代替。
打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。超过两行的提示文本只显示前两行并提供“展开”,展开后可“收起”;面板宽度变化时,收起状态的提示文本会重新判断是否超过两行。旧回执没有提示文本时,绝不以当前指令代替。
最后一页加载完成后,非空且确认裁剪过的历史在详情面板左下角显示无分隔线的清理提示,信息图标可展开或收起当前宿主的保留规则。空历史、未确认裁剪的旧格式历史、尚未加载完的分页和读取失败均不显示清理提示。
@@ -1,8 +1,11 @@
/** Lazy saved delivery pages owned by the selected task's mounted records view. */
import { useCallback, useEffect, useId, useRef, useState } from 'react'
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import { Button, IconClockOutlineRegular, IconInfoOutlineRegular, IconWarningOutlineRegular, StateDot, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
import {
Button, IconChevronDownOutlineRegular, IconChevronUpOutlineRegular, IconClockOutlineRegular, IconInfoOutlineRegular,
IconWarningOutlineRegular, StateDot, Tooltip,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { ScheduleDeliveryHistoryRequest, ScheduleDeliveryHistoryResult } from '@deepseek-ai/dsh-schedule/client'
import { formatScheduleNextRun } from './schedule-format.ts'
import css from './TaskManagerPage.module.css'
@@ -28,6 +31,36 @@ type Props = DeliveryHistoryInjected & PropsLocale<'schedule.manager'> & {
timeZone?: string | undefined
}
/**
* Render one saved prompt clamped to two lines; the toggle appears only while the clamp hides text.
* Width changes re-measure a collapsed prompt; an expanded prompt keeps its toggle until collapsed.
* @param props - Saved prompt text and locale.
* @returns The prompt paragraph and, when its text exceeds two lines, the expand or collapse toggle.
*/
function SavedPrompt({ prompt, t }: { prompt: string } & PropsLocale<'schedule.manager'>) {
const ref = useRef<HTMLParagraphElement>(null)
const id = useId()
const [expanded, setExpanded] = useState(false)
const [clamped, setClamped] = useState(false)
useLayoutEffect(() => {
if (expanded) return
const paragraph = ref.current as HTMLParagraphElement
const measure = () => { setClamped(paragraph.scrollHeight > paragraph.clientHeight) }
measure()
const observer = new ResizeObserver(measure)
observer.observe(paragraph)
return () => { observer.disconnect() }
}, [expanded, prompt])
return <>
<p ref={ref} id={id} className={css.savedPrompt} data-expanded={expanded || undefined}>{prompt}</p>
{clamped && <button type="button" className={css.savedPromptToggle} aria-expanded={expanded} aria-controls={id}
onClick={() => { setExpanded(open => !open) }}>
{t(expanded ? 'delivery.collapse' : 'delivery.expand')}
{expanded ? <IconChevronUpOutlineRegular size={14} /> : <IconChevronDownOutlineRegular size={14} />}
</button>}
</>
}
/**
* Render immutable saved deliveries; the parent keys this view by task and Session.
* Refreshes supersede pending older pages, and unmount ignores both late outcomes.
@@ -121,7 +154,7 @@ export function DeliveryHistory({ id, sessionId, latestMessageId, timeZone, load
<div className={css.deliveryHead}>
<time className={css.deliveryTime} dateTime={record.scheduledAt}>{formatOccurrence(record.scheduledAt)}</time>
</div>
{record.prompt !== undefined && <p className={css.savedPrompt}>{record.prompt}</p>}
{record.prompt !== undefined && <SavedPrompt prompt={record.prompt} t={t} />}
</div>
</section>)}
{page?.nextBefore !== undefined && failure === undefined && <Button variant="outline" disabled={loading}
@@ -1183,7 +1183,6 @@
padding: 2px 0;
}
/* The occurrence time and its message-id info anchor share one line. */
.deliveryHead {
display: flex;
align-items: center;
@@ -1204,8 +1203,14 @@
overflow-wrap: anywhere;
}
/* Instruction text sits in the row's text column at the mock's 13px/22px. */
/* Instruction text sits in the row's text column at the mock's 13px/22px and
shows two lines until its toggle expands it. */
.savedPrompt {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
margin: 6px 0 0;
color: var(--dsw-alias-label-tertiary);
font-size: 13px;
@@ -1214,6 +1219,39 @@
overflow-wrap: anywhere;
}
.savedPrompt[data-expanded] {
display: block;
-webkit-line-clamp: none;
line-clamp: none;
}
/* The negative inline margin offsets the toggle's own padding, so its label
starts at the text column's left edge. */
.savedPromptToggle {
display: inline-flex;
align-items: center;
gap: 2px;
margin: 2px 0 0 -6px;
padding: 1px 6px;
border: 0;
border-radius: var(--dsw-radius-sm);
background: transparent;
color: var(--dsw-alias-label-secondary);
font: inherit;
font-size: 12px;
line-height: 20px;
cursor: pointer;
}
.savedPromptToggle:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.savedPromptToggle:focus-visible {
outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
outline-offset: 2px;
}
.detailActions,
.confirmActions {
display: flex;
@@ -37,6 +37,8 @@ export const en = {
'delivery.retry': 'Retry',
'delivery.refresh': 'Refresh',
'delivery.loadMore': 'Load more',
'delivery.expand': 'Expand',
'delivery.collapse': 'Collapse',
'delivery.pruned': 'Earlier delivery records have been cleared',
'delivery.retention': 'Retention rules',
'delivery.retentionBounds': 'Each task keeps up to {records} delivery records from the last {days} days.',
@@ -178,6 +180,8 @@ export const zh: Record<TaskManagerKey, string> = {
'delivery.retry': '重试',
'delivery.refresh': '刷新',
'delivery.loadMore': '加载更多',
'delivery.expand': '展开',
'delivery.collapse': '收起',
'delivery.pruned': '更早的运行记录已清理',
'delivery.retention': '保留规则',
'delivery.retentionBounds': '每个任务最多保留近 {days} 天内的 {records} 条运行记录。',
@@ -156,6 +156,49 @@ describe.each([en, zh])('saved delivery records', (dictionary) => {
expect(instructionRule).toMatch(/color:\s*var\(--dsw-alias-label-tertiary\);/)
expect(instructionRule).toMatch(/white-space:\s*pre-wrap;/)
expect(instructionRule).toMatch(/overflow-wrap:\s*anywhere;/)
// A collapsed instruction shows two lines; the expanded state lifts the clamp.
expect(instructionRule).toMatch(/display:\s*-webkit-box;/)
expect(instructionRule).toMatch(/-webkit-box-orient:\s*vertical;/)
expect(instructionRule).toMatch(/-webkit-line-clamp:\s*2;/)
expect(instructionRule).toMatch(/[;\s]line-clamp:\s*2;/)
expect(instructionRule).toMatch(/overflow:\s*hidden;/)
const expandedRule = ruleBlock(stylesheet, 'savedPrompt\\[data-expanded\\]')
expect(expandedRule).toMatch(/display:\s*block;/)
expect(expandedRule).toMatch(/-webkit-line-clamp:\s*none;/)
expect(expandedRule).toMatch(/[;\s]line-clamp:\s*none;/)
// The toggle's negative margin cancels its inline padding, so its label
// starts at the instruction's left edge.
const toggleRule = ruleBlock(stylesheet, 'savedPromptToggle')
expect(toggleRule).toMatch(/margin:\s*2px 0 0 -6px;/)
expect(toggleRule).toMatch(/padding:\s*1px 6px;/)
})
it('clamps each overflowing saved instruction behind its own expand toggle', async () => {
// Two 22px lines are visible of a six-line instruction.
vi.spyOn(Element.prototype, 'clientHeight', 'get').mockReturnValue(44)
vi.spyOn(Element.prototype, 'scrollHeight', 'get').mockReturnValue(132)
try {
mount(dictionary)
const [first, second] = await screen.findAllByRole('region', { name: dictionary['delivery.label'] })
const prompt = first!.querySelector<HTMLElement>(`.${css.savedPrompt}`)!
const toggle = within(first!).getByRole('button', { name: dictionary['delivery.expand'] })
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(toggle.getAttribute('aria-controls')).toBe(prompt.id)
expect(prompt.hasAttribute('data-expanded')).toBe(false)
expect(prompt.textContent).toBe(records[0]!.prompt)
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('true')
expect(toggle.textContent).toBe(dictionary['delivery.collapse'])
expect(prompt.hasAttribute('data-expanded')).toBe(true)
const other = within(second!).getByRole('button', { name: dictionary['delivery.expand'] })
expect(other.getAttribute('aria-expanded')).toBe('false')
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(toggle.textContent).toBe(dictionary['delivery.expand'])
expect(prompt.hasAttribute('data-expanded')).toBe(false)
} finally {
vi.restoreAllMocks()
}
})
it('reads legacy unavailability without inventing a prompt or a notice', async () => {
@@ -299,6 +342,53 @@ it('keeps the Rules and the Delivery records views on one type scale', () => {
expect(declared('savedPrompt', 'color')).toBe('var(--dsw-alias-label-tertiary)')
})
it('offers the instruction toggle only while the collapsed instruction overflows at the current width', async () => {
class FakeResizeObserver implements ResizeObserver {
static readonly made: FakeResizeObserver[] = []
readonly observe = vi.fn()
readonly unobserve = vi.fn()
readonly disconnect = vi.fn()
constructor(private readonly callback: ResizeObserverCallback) {
FakeResizeObserver.made.push(this)
}
fire(): void {
this.callback([], this)
}
}
vi.stubGlobal('ResizeObserver', FakeResizeObserver)
let lines = 2
vi.spyOn(Element.prototype, 'clientHeight', 'get').mockReturnValue(44)
vi.spyOn(Element.prototype, 'scrollHeight', 'get').mockImplementation(() => lines * 22)
try {
const loadHistory = vi.fn<DeliveryHistoryInjected['loadHistory']>().mockResolvedValue(result(0, 1))
const view = render(<DeliveryHistory
id={id} sessionId={sessionId} latestMessageId={undefined} loadHistory={loadHistory} t={makeTranslate(en)}
/>)
const region = await screen.findByRole('region', { name: en['delivery.label'] })
const prompt = region.querySelector<HTMLElement>(`.${css.savedPrompt}`)!
expect(within(region).queryByRole('button')).toBeNull()
const collapsed = FakeResizeObserver.made.at(-1)!
expect(collapsed.observe).toHaveBeenCalledWith(prompt)
// A narrower panel wraps the instruction onto a third line.
lines = 3
act(() => { collapsed.fire() })
fireEvent.click(within(region).getByRole('button', { name: en['delivery.expand'] }))
// The expanded instruction is not measured; its toggle stays until collapsed.
expect(collapsed.disconnect).toHaveBeenCalledTimes(1)
lines = 2
fireEvent.click(within(region).getByRole('button', { name: en['delivery.collapse'] }))
expect(within(region).queryByRole('button')).toBeNull()
const remeasured = FakeResizeObserver.made.at(-1)!
expect(remeasured).not.toBe(collapsed)
view.unmount()
expect(remeasured.disconnect).toHaveBeenCalledTimes(1)
} finally {
vi.restoreAllMocks()
vi.unstubAllGlobals()
}
})
it('appends real-sized pages stably without duplicate IDs and contains overlapping older-page requests', async () => {
const h = mount()
await screen.findByText(formatScheduleNextRun(records[0]!.scheduledAt, en['time.locale']))
@@ -18,11 +18,11 @@
en: bc11406a04989a27
zh: 27e5fffa2b3eda6d
/deepseek-ai-dsh-client-ui-settings-account/use-this-package/desktop-onboarding:
en: ff989846c5f3065c
zh: 46b284c494f1dd8c
en: c1c4db71fd09ca13
zh: a40c0a01e9951945
/deepseek-ai-dsh-client-ui-settings-account/understand-the-implementation:
en: 156bbaf3d44a3328
zh: 7acf7d2e8e8bf1e4
en: 6b3ad3b6860100cf
zh: aeced4fb37642f15
/deepseek-ai-dsh-client-ui-settings-account/further-exploration:
en: c785923662681191
zh: f24389c25e954035
@@ -55,16 +55,16 @@ The notice is painted as soon as the server offers it, including underneath the
<a id="desktop-onboarding"></a>
### Desktop onboarding
The Desktop preload marker enables the introduction after account credentials are stored. Progress belongs to the local Host settings document, not the Platform account: unfinished steps resume after restart, and completing or skipping the flow prevents another run on that installation. Browser clients do not mount it. API-key presence comes from the native welcome backend through the boolean-only Desktop preload bridge, using the same credential discovery as login. A signed-out installation with a configured model API key bypasses the pages and applies standard process, detailed usage, and enabled developer tools.
The Desktop preload marker enables the introduction after account credentials are stored. Progress belongs to the local Host settings document, not the Platform account: unfinished steps resume after restart, and completing or skipping the flow prevents another run on that installation. Browser clients do not mount it. API-key presence comes from the native welcome backend through the boolean-only Desktop preload bridge, using the same credential discovery as login. A signed-out installation with a configured model API key bypasses the pages and applies standard process, detailed usage, and enabled Coding Tools.
The welcome page requires Get started. The credit page always appears. A positive balance confirmed at first entry selects Continue as the primary action and Add credits as the secondary action. This choice stays fixed until the onboarding controller is recreated and is never persisted; late balance results cannot change the buttons. Only a confirmed absence of positive credit adds a confirmation before continuing without recharge; pending or failed queries do not block continuing. Purpose descriptions remain unchanged when selected. Returning from the native top-up view keeps the credit page and re-reads profile and balance in the background after any read already running, whether loading or payment succeeded. That read does not recompute the funded-mode choice, so a completed recharge leaves the buttons as first entered while continuing still evaluates the refreshed balance. Next confirms only a known zero balance; Skip uses the credit warning on the credit page and the setup warning on the question pages. Office-only use completes with compact presentation; development or both purposes also ask for process detail. Back preserves selections, and explicit skip from any supported step applies standard process, compact performance/usage, and disabled developer tools. This flow creates no workspace or demonstration task.
The welcome page requires Get started. The credit page always appears. A positive balance confirmed at first entry selects Continue as the primary action and Add credits as the secondary action. This choice stays fixed until the onboarding controller is recreated and is never persisted; late balance results cannot change the buttons. Only a confirmed absence of positive credit adds a confirmation before continuing without recharge; pending or failed queries do not block continuing. Purpose descriptions remain unchanged when selected. Returning from the native top-up view keeps the credit page and re-reads profile and balance in the background after any read already running, whether loading or payment succeeded. That read does not recompute the funded-mode choice, so a completed recharge leaves the buttons as first entered while continuing still evaluates the refreshed balance. Next confirms only a known zero balance; Skip uses the credit warning on the credit page and the setup warning on the question pages. Office-only use completes with compact presentation; development or both purposes also ask for process detail. Back preserves selections, and explicit skip from any supported step applies standard process, compact performance/usage, and disabled Coding Tools. This flow creates no workspace or demonstration task.
<a id="understand-the-implementation"></a>
## Understand the implementation
The Account section appears first in Settings only while signed in; it is hidden before account state loads and after sign-out. The plugin owns one Host snapshot stream shared through framework hooks by settings.section and settings.launcher. The launcher opens Settings and offers Sign out only while an account credential is stored. Platform failures leave the menu available for retry. It maintains no independent credential state, so no invariant companion is published.
The desktop controller writes progress to `ui-settings-account` through Host settings. Completion first applies `ui-chat.transcriptView` and `ui-chat.performanceUsage` together, then calls the shared developer-tools preference to persist `ui-settings.enabled`, and finally saves the done marker; a rejected write keeps the flow available for retry. Successful completion reveals the workspace through a 180ms fade; choices and ordinary navigation preview immediately while writes run in order. A failed final queued write restores saved progress and allows retry. Completion waits for queued choices and retains the page until preferences persist; reduced motion skips the fade. Onboarding choices compact, standard, and detailed apply directly to the corresponding Chat work-detail modes. API-key entry and explicit skip select standard; onboarding does not select verbose. Complete Figma illustration layers are bundled as transparent, palette-compressed 3× PNG assets; welcome layers are merged with local sidebar blur; recharge uses a 70% opaque foreground window with local blur of the covered illustration, and headings use the bundled Montserrat brand font; English headings and descriptions use Light (300), except the DeepSeek Harness brand within headings uses Medium (500); card titles retain Regular (400) when selected. English footer navigation uses Light (300). Confirmed onboarding sets a 960px minimum window width and enlarges narrower windows; completion releases the minimum without restoring the previous size. Loading or already-completed onboarding does not resize the window. Empty footer navigation space passes pointer events through to the primary action. Cards exclude native window dragging and own the keyboard focus outline for their checkbox. The account package owns its onboarding overlay and control geometry. Each step and the confirmation dialog has a separate component; the flow coordinates navigation, transitions, and recharge. The overlay hides the workspace while initial account and progress state loads. Question headings and cards follow the Figma positions at 1440 × 920 and 960 × 600, independently of their action buttons. On macOS, content clears the body portal’s inherited no-drag region and a first-child drag band leaves an 8px native resize margin; interactive controls and later dialogs exclude their regions, and a native Platform page disables the underlying onboarding band. The upper workspace popup illustration has an opaque background in both languages and themes; the underlying sidebar omits its selected-row highlight. Welcome keeps its illustration at its original size, repositions and clips the artwork in narrow windows, and reveals the complete illustration on wide windows. The [onboarding decision](../../../.agents/notes/implemented/feature/2026-09-16-desktop-onboarding.md) owns persistence and presentation trade-offs.
The desktop controller writes progress to `ui-settings-account` through Host settings. Completion first applies `ui-chat.transcriptView` and `ui-chat.performanceUsage` together, then calls the shared Coding Tools preference to persist `ui-settings.enabled`, and finally saves the done marker; a rejected write keeps the flow available for retry. Successful completion reveals the workspace through a 180ms fade; choices and ordinary navigation preview immediately while writes run in order. A failed final queued write restores saved progress and allows retry. Completion waits for queued choices and retains the page until preferences persist; reduced motion skips the fade. Onboarding choices compact, standard, and detailed apply directly to the corresponding Chat work-detail modes. API-key entry and explicit skip select standard; onboarding does not select verbose. Complete Figma illustration layers are bundled as transparent, palette-compressed 3× PNG assets; welcome layers are merged with local sidebar blur; recharge uses a 70% opaque foreground window with local blur of the covered illustration, and headings use the bundled Montserrat brand font; English headings and descriptions use Light (300), except the DeepSeek Harness brand within headings uses Medium (500); card titles retain Regular (400) when selected. English footer navigation uses Light (300). Confirmed onboarding sets a 960px minimum window width and enlarges narrower windows; completion releases the minimum without restoring the previous size. Loading or already-completed onboarding does not resize the window. Empty footer navigation space passes pointer events through to the primary action. Cards exclude native window dragging and own the keyboard focus outline for their checkbox. The account package owns its onboarding overlay and control geometry. Each step and the confirmation dialog has a separate component; the flow coordinates navigation, transitions, and recharge. The overlay hides the workspace while initial account and progress state loads. Question headings and cards follow the Figma positions at 1440 × 920 and 960 × 600, independently of their action buttons. On macOS, content clears the body portal’s inherited no-drag region and a first-child drag band leaves an 8px native resize margin; interactive controls and later dialogs exclude their regions, and a native Platform page disables the underlying onboarding band. The upper workspace popup illustration has an opaque background in both languages and themes; the underlying sidebar omits its selected-row highlight. Welcome keeps its illustration at its original size, repositions and clips the artwork in narrow windows, and reveals the complete illustration on wide windows. The [onboarding decision](../../../.agents/notes/implemented/feature/2026-09-16-desktop-onboarding.md) owns persistence and presentation trade-offs.
<a id="further-exploration"></a>
## Further Exploration
@@ -55,16 +55,16 @@ Chat 通过 `shell.overlay` 中的宿主把已绑定 Session 的实时欠费交
<a id="desktop-onboarding"></a>
### Desktop 引导
Desktop preload 标记在账号凭证已存储后启用引导。进度属于本地 Host 设置文档,不属于 Platform 账号:重启后恢复未完成的步骤,完成或跳过后不再于当前安装中重复运行。浏览器客户端不挂载此流程。API Key 是否存在由原生欢迎后端通过仅返回布尔值的 Desktop preload 桥接提供,与登录共用凭证发现逻辑。未登录但已配置模型 API Key 的安装会绕过这些页面,应用标准过程、详细用量和开启开发者工具的配置。
Desktop preload 标记在账号凭证已存储后启用引导。进度属于本地 Host 设置文档,不属于 Platform 账号:重启后恢复未完成的步骤,完成或跳过后不再于当前安装中重复运行。浏览器客户端不挂载此流程。API Key 是否存在由原生欢迎后端通过仅返回布尔值的 Desktop preload 桥接提供,与登录共用凭证发现逻辑。未登录但已配置模型 API Key 的安装会绕过这些页面,应用标准过程、详细用量和开启代码工作工具的配置。
欢迎页必须点击“开始设置”。额度页始终展示。首次进入时若已确认有正余额,则主操作为“继续”,次操作为“去充值”。该选择在引导控制器重建前保持固定且不持久化,后到的余额结果不会改变按钮。仅在确认不存在正余额时,继续而不充值才需确认;查询中或失败时不阻止继续。用途卡片选中后描述不变。从原生充值视图返回后保留额度页,并在等待任何已在进行的读取结束后于后台重读资料与余额,不受页面加载或支付结果影响。该读取不会重新计算该选择,因此充值成功后按钮仍保持首次进入时的状态,而继续前行仍按刷新后的余额判断。“下一步”仅在确认零余额时提示;额度页的“跳过”使用充值提示,答题页使用设置提示。仅选办公用途时以简洁展示完成;选择开发或两种用途时还会询问过程详情。上一步保留已有选择,任意可跳过步骤均采用标准工作过程、简洁性能与用量以及关闭开发者工具的默认值。此流程不创建工作区或示例任务。
欢迎页必须点击“开始设置”。额度页始终展示。首次进入时若已确认有正余额,则主操作为“继续”,次操作为“去充值”。该选择在引导控制器重建前保持固定且不持久化,后到的余额结果不会改变按钮。仅在确认不存在正余额时,继续而不充值才需确认;查询中或失败时不阻止继续。用途卡片选中后描述不变。从原生充值视图返回后保留额度页,并在等待任何已在进行的读取结束后于后台重读资料与余额,不受页面加载或支付结果影响。该读取不会重新计算该选择,因此充值成功后按钮仍保持首次进入时的状态,而继续前行仍按刷新后的余额判断。“下一步”仅在确认零余额时提示;额度页的“跳过”使用充值提示,答题页使用设置提示。仅选办公用途时以简洁展示完成;选择开发或两种用途时还会询问过程详情。上一步保留已有选择,任意可跳过步骤均采用标准工作过程、简洁性能与用量以及关闭代码工作工具的默认值。此流程不创建工作区或示例任务。
<a id="understand-the-implementation"></a>
## 理解实现
仅登录后在设置导航首位显示账号页;账号状态加载完成前以及退出登录后隐藏该页。 插件持有一条 Host 快照流,通过框架 hook 供 settings.section 和 settings.launcher 共享。入口菜单可打开设置,并仅在已存储账号凭证时提供退出登录。Platform 请求失败时保留菜单,以供重试。插件不维护独立凭证状态,因此不发布 invariant。
桌面控制器通过 Host 设置将进度写入 `ui-settings-account`。完成时先一并应用 `ui-chat.transcriptView` 与 `ui-chat.performanceUsage`,再通过共享开发者工具偏好保存 `ui-settings.enabled`,最后保存完成标记;写入被拒绝时保留引导以供重试。成功完成后通过 180ms 淡出显露工作区;选择和普通翻页立即预览,写入在后台按顺序执行。队列最后一次写入失败时恢复已保存进度,并允许重试。完成操作会等待选择队列及偏好保存成功,再关闭页面;减少动态效果偏好会跳过淡出。onboarding 选项 compact、standard、detailed 直接应用到对应的 Chat 工作过程模式。API Key 进入与显式跳过均采用 standard;onboarding 不选择 verbose。完整 Figma 示意图层以透明、调色板压缩的 3× PNG 资源打包;欢迎页图层合并并烘焙侧栏局部模糊;充值页前景窗口不透明度为 70%,仅烘焙其覆盖插图区域的模糊,标题使用已打包的 Montserrat 品牌字体,英文标题和描述使用 Light(300),标题中的 DeepSeek Harness 使用 Medium(500);卡片标题选中前后均使用 Regular(400)。英文底部导航使用 Light(300)。确认进入引导后,窗口最小宽度设为 960px,较窄窗口自动放大;完成后解除宽度限制,但不恢复原尺寸。加载中或已完成引导的启动不改变窗口尺寸。底部导航空白区域透过鼠标事件,避免遮挡主操作。卡片区域不参与原生窗口拖动,复选框的键盘焦点仅在卡片外框展示。账号包负责引导覆盖层和控件尺寸样式。每个步骤和确认弹窗各有独立组件,流程组件协调导航、切换动画和充值。初始账号与进度加载期间,覆盖层隐藏工作区。答题标题和卡片独立于操作按钮,位置对齐 Figma 的 1440 × 920 和 960 × 600 断点。macOS 的内容层清除 body 浮层继承的 no-drag,位于内容之前的拖拽层保留 8px 原生缩放边缘;交互控件和后续弹窗排除自身区域;原生平台页面打开时禁用底层引导拖拽层。上方工作区弹窗插画在两种语言和主题下均使用不透明背景,下层侧边栏不绘制选中行高亮。欢迎页保持示意图的原始尺寸,在窄窗口内调整插图位置并裁切,在宽窗口内展示完整插图。[引导决策](../../../.agents/notes/implemented/feature/2026-09-16-desktop-onboarding.zh.md)记录持久化与展示方面的取舍。
桌面控制器通过 Host 设置将进度写入 `ui-settings-account`。完成时先一并应用 `ui-chat.transcriptView` 与 `ui-chat.performanceUsage`,再通过共享代码工作工具偏好保存 `ui-settings.enabled`,最后保存完成标记;写入被拒绝时保留引导以供重试。成功完成后通过 180ms 淡出显露工作区;选择和普通翻页立即预览,写入在后台按顺序执行。队列最后一次写入失败时恢复已保存进度,并允许重试。完成操作会等待选择队列及偏好保存成功,再关闭页面;减少动态效果偏好会跳过淡出。onboarding 选项 compact、standard、detailed 直接应用到对应的 Chat 工作过程模式。API Key 进入与显式跳过均采用 standard;onboarding 不选择 verbose。完整 Figma 示意图层以透明、调色板压缩的 3× PNG 资源打包;欢迎页图层合并并烘焙侧栏局部模糊;充值页前景窗口不透明度为 70%,仅烘焙其覆盖插图区域的模糊,标题使用已打包的 Montserrat 品牌字体,英文标题和描述使用 Light(300),标题中的 DeepSeek Harness 使用 Medium(500);卡片标题选中前后均使用 Regular(400)。英文底部导航使用 Light(300)。确认进入引导后,窗口最小宽度设为 960px,较窄窗口自动放大;完成后解除宽度限制,但不恢复原尺寸。加载中或已完成引导的启动不改变窗口尺寸。底部导航空白区域透过鼠标事件,避免遮挡主操作。卡片区域不参与原生窗口拖动,复选框的键盘焦点仅在卡片外框展示。账号包负责引导覆盖层和控件尺寸样式。每个步骤和确认弹窗各有独立组件,流程组件协调导航、切换动画和充值。初始账号与进度加载期间,覆盖层隐藏工作区。答题标题和卡片独立于操作按钮,位置对齐 Figma 的 1440 × 920 和 960 × 600 断点。macOS 的内容层清除 body 浮层继承的 no-drag,位于内容之前的拖拽层保留 8px 原生缩放边缘;交互控件和后续弹窗排除自身区域;原生平台页面打开时禁用底层引导拖拽层。上方工作区弹窗插画在两种语言和主题下均使用不透明背景,下层侧边栏不绘制选中行高亮。欢迎页保持示意图的原始尺寸,在窄窗口内调整插图位置并裁切,在宽窗口内展示完整插图。[引导决策](../../../.agents/notes/implemented/feature/2026-09-16-desktop-onboarding.zh.md)记录持久化与展示方面的取舍。
<a id="further-exploration"></a>
## 深入探索
@@ -30,7 +30,7 @@ export const onboardingCopy = {
onboardingBack: '上一步',
onboardingSkip: '跳过',
onboardingSkipTitle: '跳过设置?',
onboardingSkipDescription: '你可以随时前往 个人中心 → 通用设置 中调整工作过程、性能与用量展示方式或开启开发者工具。',
onboardingSkipDescription: '你可以随时前往 个人中心 → 通用设置 中调整工作过程、性能与用量展示方式或开启代码工作工具。',
onboardingKeepSetting: '继续设置',
onboardingNoCreditTitle: '暂时跳过充值?',
onboardingNoCreditDescription: '没有可用额度时,DeepSeek Harness 将无法开始新的任务。你可以稍后前往 个人中心 → 账号与余额 进行充值。',
@@ -73,7 +73,7 @@ export const onboardingEnglishCopy: Record<keyof typeof onboardingCopy, string>
onboardingBack: 'Back',
onboardingSkip: 'Skip',
onboardingSkipTitle: 'Skip setup?',
onboardingSkipDescription: 'You can change how progress, performance, and usage are displayed, or enable developer tools, anytime in Profile → General Settings.',
onboardingSkipDescription: 'You can change how progress, performance, and usage are displayed, or enable Coding Tools, anytime in Profile → General Settings.',
onboardingKeepSetting: 'Continue setup',
onboardingNoCreditTitle: 'Skip adding credits?',
onboardingNoCreditDescription: 'DeepSeek Harness can’t start new tasks without credits. You can add them later in Profile → Accounts.',
@@ -9,8 +9,8 @@
en: 1017c980d55e1945
zh: d3febd2062fce20f
/deepseek-ai-dsh-client-ui-settings-general/summary:
en: 56ad6c35c1c4745c
zh: 1911baf2e1ebddab
en: cd94b4a8dae32670
zh: 333b8603e77f92d7
/deepseek-ai-dsh-client-ui-settings-general/table-of-contents:
en: aeb9c4bfdb19bc56
zh: 45a29bea90cff239
@@ -18,8 +18,8 @@
en: da896a9cf06fe5a5
zh: 27545af457695988
/deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section:
en: de484a8406f3fc17
zh: a3831774d457c49a
en: 8b8a7c6287bb303b
zh: 19535112531aedd5
/deepseek-ai-dsh-client-ui-settings-general/use-this-package/opening-the-configuration-file:
en: ca2b4f5ccfbe2705
zh: bd1dc1d9879d0f0e
@@ -27,8 +27,8 @@
en: 27f0f6ec55f00228
zh: 69b47ff1efd81c17
/deepseek-ai-dsh-client-ui-settings-general/understand-the-implementation:
en: 731eb44eaa3ed2ae
zh: eb71745cb05334d5
en: 41726bcd4ae37c0a
zh: d994b70ca7997da8
/deepseek-ai-dsh-client-ui-settings-general/understand-the-implementation/ledger-projections:
en: 3e3c2c72f87e8b23
zh: 12b10ba2d37c9077
@@ -51,8 +51,8 @@
en: ca75c51c89c2c9b0
zh: de54f3467ef3b9be
/deepseek-ai-dsh-client-ui-settings-general/known-limitations-and-deferred-work:
en: eb93fe357edc777a
zh: e9f1bb38af42de90
en: 97dd637a320ac334
zh: faa5c512c56a128e
/deepseek-ai-dsh-client-ui-settings-general/known-limitations-and-deferred-work/dev-note:
en: b36039e49a66b8e0
zh: 306682cd90cce1db
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
Use this package to give the dsh web client a Settings panel, connection-recovery control, feature-contributed navigation, and sequential first-run onboarding. Users can open it from the sidebar, retry a failed connection immediately, and access a local configuration file when the Host makes one available on a loopback browser. Feature packages supply their own settings rows, sections, and onboarding steps; this package supplies their shared presentation and the Developer tools switch without adding onboarding copy.
Use this package to give the dsh web client a Settings panel, connection-recovery control, feature-contributed navigation, and sequential first-run onboarding. Users can open it from the sidebar, retry a failed connection immediately, and access a local configuration file when the Host makes one available on a loopback browser. Feature packages supply their own settings rows, sections, and onboarding steps; this package supplies their shared presentation and the Coding Tools switch without adding onboarding copy.
## Table of Contents
@@ -39,9 +39,9 @@ Settings visibility and section selection live in the shell owner store. The she
The current release version appears at the bottom of General Settings in Web and Desktop, using the build’s `DSH_CLIENT_VERSION` metadata and the active language. Partial builds without version metadata omit the row.
The Developer tools switch controls the shared preference described by [ui-settings](../ui-settings/README.md#use-this-package). It is available in both Web and desktop, follows accepted changes immediately, and disables duplicate input while a write settles. A failed write displays localized retry guidance.
The Coding Tools switch controls the shared `ui-settings.enabled` preference described by [ui-settings](../ui-settings/README.md#use-this-package). It is available in both Web and desktop, follows accepted changes immediately, and disables duplicate input while a write settles. A failed write displays localized retry guidance.
The General section holds the built-in Developer tools and Current version rows alongside rows registered into `settings.general.item` by feature packages. Each registrant owns its row copy and behavior. The Appearance row, for example, lives in ui-theme.
The General section holds the built-in Coding Tools and Current version rows alongside rows registered into `settings.general.item` by feature packages. Each registrant owns its row copy and behavior. The Appearance row, for example, lives in ui-theme.
### Opening the configuration file
@@ -61,7 +61,7 @@ The shell declares settings.launcher for an account-owned sidebar menu and retai
<details>
<summary>Implementation internals — click to expand</summary>
The shell owns the chrome and the projections; it contributes the Developer tools and Current version rows, while feature registrants own their additional content and copy.
The shell owns the chrome and the projections; it contributes the Coding Tools and Current version rows, while feature registrants own their additional content and copy.
### Ledger projections
@@ -112,7 +112,7 @@ None; this package neither assembles nor sends a provider request.
These limits define what the shell itself provides versus what features must supply; they are current package constraints.
- **Additional General rows require their feature plugins** — the shell supplies Developer tools and Current version; feature plugins supply the remaining preferences.
- **Additional General rows require their feature plugins** — the shell supplies Coding Tools and Current version; feature plugins supply the remaining preferences.
- **The Windows caption badge keeps a side-opening bubble** — `DesktopUpdateBadge` occupies `sidebar.toggle.badge` in the caption and requests `side="right"`, so the Desktop-owned menu text can cover its bubble while the sidebar is collapsed on Windows; the sidebar toggle and New Session bubbles open below the caption instead (#4688).
<a id="dev-note"></a>
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
使用本包可为 dsh Web 客户端提供 Settings 面板、连接恢复控件、由功能包贡献的导航,以及依次进行的首次运行引导。用户可以从侧边栏打开面板、立即重试失败的连接,并在宿主为回环浏览器提供本地配置文件时访问该文件。各功能包提供自己的设置行、分区和引导步骤;本包提供共享的界面展示和开发者工具开关,但不添加引导文案。
使用本包可为 dsh Web 客户端提供 Settings 面板、连接恢复控件、由功能包贡献的导航,以及依次进行的首次运行引导。用户可以从侧边栏打开面板、立即重试失败的连接,并在宿主为回环浏览器提供本地配置文件时访问该文件。各功能包提供自己的设置行、分区和引导步骤;本包提供共享的界面展示和代码工作工具开关,但不添加引导文案。
## 目录
@@ -39,9 +39,9 @@ kind: "package-reference"
Web 与桌面端的通用设置底部显示当前发布版本,使用构建注入的 `DSH_CLIENT_VERSION` 元数据和当前语言。缺少版本元数据的局部构建不显示该行。
开发者工具开关控制 [ui-settings](../ui-settings/README.zh.md#use-this-package) 定义的共享偏好。Web 和桌面端均提供此开关,立即跟随已接受的变更,并在写入完成前禁用重复输入。写入失败时显示本地化的重试提示。
代码工作工具开关控制 [ui-settings](../ui-settings/README.zh.md#use-this-package) 定义的共享偏好 `ui-settings.enabled`。Web 和桌面端均提供此开关,立即跟随已接受的变更,并在写入完成前禁用重复输入。写入失败时显示本地化的重试提示。
「通用」分区承载内置的开发者工具行与当前版本行,以及功能包注册进 `settings.general.item` 的行。每个注册方拥有自己的行文案与行为。例如「外观」行位于 ui-theme。
「通用」分区承载内置的代码工作工具行与当前版本行,以及功能包注册进 `settings.general.item` 的行。每个注册方拥有自己的行文案与行为。例如「外观」行位于 ui-theme。
### 打开配置文件
@@ -61,7 +61,7 @@ Web 与桌面端的通用设置底部显示当前发布版本,使用构建注
<details>
<summary>实现细节——点击展开</summary>
外壳拥有界面框架与投影,并提供开发者工具行与当前版本行;功能注册方拥有其余内容与文案。
外壳拥有界面框架与投影,并提供代码工作工具行与当前版本行;功能注册方拥有其余内容与文案。
### 账本投影
@@ -112,7 +112,7 @@ Web 与桌面端的通用设置底部显示当前发布版本,使用构建注
这些限制说明外壳自身提供什么、功能包必须提供什么;它们是当前包约束。
- **额外的通用设置行需要相应功能插件**:外壳提供开发者工具与当前版本;功能插件提供其他偏好。
- **额外的通用设置行需要相应功能插件**:外壳提供代码工作工具与当前版本;功能插件提供其他偏好。
- **Windows 顶栏徽标的气泡仍向右展开**:`DesktopUpdateBadge` 占用顶栏的 `sidebar.toggle.badge` 且请求 `side="right"`,侧栏收起时其气泡会被 Desktop 菜单文字遮挡;侧栏开关与新建会话的气泡则改在顶栏下方展开(#4688)。
<a id="dev-note"></a>
@@ -29,9 +29,9 @@ export const zh = {
'openDocument.error': '无法打开配置文件',
'general.nav': '通用设置',
'general.currentVersion': '当前版本:{version}',
'developerTools.title': '开发者工具',
'developerTools.title': '代码工作工具',
'developerTools.error': '保存失败,请重试',
'developerTools.description': '显示用于调试和排查问题的工具与信息',
'developerTools.description': '开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换',
'connection.error': '连接异常,刷新重试',
'connection.connecting': '重新连接中',
'connection.connected': '连接成功',
@@ -71,9 +71,9 @@ export const en = {
'openDocument.error': 'Could not open configuration file',
'general.nav': 'General',
'general.currentVersion': 'Current version: {version}',
'developerTools.title': 'Developer tools',
'developerTools.title': 'Coding Tools',
'developerTools.error': 'Could not save. Please try again.',
'developerTools.description': 'Show tools and information for debugging and troubleshooting',
'developerTools.description': 'Shows trajectory, code diffs, and Agent preset switching in new chats',
'connection.error': 'Disconnected',
'connection.connecting': 'Reconnecting',
'connection.connected': 'Connected',
@@ -81,7 +81,7 @@ it('toggles developer tools using the accepted setting and disables duplicate wr
finish = () => { state.set(enabled); resolve() }
}))
render(<DeveloperToolsRow {...kit} t={t} useDeveloperTools={bindSnapshotSelector(state)} setEnabled={setEnabled} />)
const toggle = screen.getByRole('switch', { name: 'Developer tools' })
const toggle = screen.getByRole('switch', { name: 'Coding Tools' })
expect(toggle.getAttribute('aria-checked')).toBe('false')
fireEvent.click(toggle)
expect(setEnabled).toHaveBeenCalledWith(true)
@@ -214,7 +214,7 @@ it('reports a failed developer-tool write and allows retry', async () => {
const state = createSnapshotStore(false)
const setEnabled = vi.fn().mockRejectedValueOnce(new Error('offline')).mockImplementation(async (enabled: boolean) => { state.set(enabled) })
render(<DeveloperToolsRow {...kit} t={t} useDeveloperTools={bindSnapshotSelector(state)} setEnabled={setEnabled} />)
const toggle = screen.getByRole('switch', { name: 'Developer tools' })
const toggle = screen.getByRole('switch', { name: 'Coding Tools' })
fireEvent.click(toggle)
expect((await screen.findByRole('alert')).textContent).toBe('Could not save. Please try again.')
expect(toggle.hasAttribute('disabled')).toBe(false)
@@ -30,8 +30,8 @@
en: 589397a35f82244e
zh: 342fbceee2c173a2
/deepseek-ai-dsh-client-ui-settings-models/use-this-package/extension-slots:
en: 6979796f0332268f
zh: 00896a005cc4f4e2
en: 65e9d8822b67b6c2
zh: 7e83be9defda28ee
/deepseek-ai-dsh-client-ui-settings-models/understand-the-implementation:
en: a87afbec5e339b36
zh: 6348c81af7e650e1
+1 -1
View File
@@ -61,7 +61,7 @@ After the versioned notice step completes, the DeepSeek step projects first-run
The section declares two seats for plugins distributed outside this repository, typed in [`src/client/slot-contract.ts`](src/client/slot-contract.ts) and exported from `./client`. `settings.models.provider-card` (keyed) renders inside every card that shows a directory row — a saved row's card, its first-run setup posture, and the add-provider draft — dispatched with `entryKey = settingsNs` and owner props carrying the row's `ConfigurableProviderView`, its configured state, and its confirmed api-key credential state, so one registration under an adapter family's namespace receives every card of that family, hand-declared routes included; the hand-declared draft card has no directory row yet and dispatches nothing until saved. `settings.models.footer` (list) renders after the rows and the add controls. A registrant activates through `ctx.slots.inject` with a type-only import of this package's `/client` entry; without registrants both seats render nothing.
The Models page includes **DeepSeek Account** (`deepseek-account`, **DeepSeek 账号** in Chinese). Its editor exposes the shared DeepSeek model catalog without API-key or base-URL inputs. The account row is hidden when its available model catalog is empty, including before sign-in and after sign-out; it returns when account models become available.
The Models page includes **DeepSeek Account** (`deepseek-account`, **DeepSeek 账号** in Chinese). Its editor exposes the shared DeepSeek model catalog without API-key or base-URL inputs, and saves that catalog under the account route's own settings section (`llm-deepseek-account` by default), so an account edit never rewrites the `llm-deepseek` section the official route reads. The account row is hidden when its available model catalog is empty, including before sign-in and after sign-out; it returns when account models become available.
-----
@@ -61,7 +61,7 @@ API 密钥输入框初始为空,并通过 `autocomplete="new-password"` 请求
本分区为仓库外分发的插件声明两个席位,类型定义在 [`src/client/slot-contract.ts`](src/client/slot-contract.ts) 并从 `./client` 导出。`settings.models.provider-card`(keyed)渲染在每张展示目录行的卡片内部——已保存行的卡片、其首次运行 setup 形态、以及「添加提供商」草稿卡——以 `entryKey = settingsNs` 分发,owner props 携带该行的 `ConfigurableProviderView`、其 configured 状态与已确认的 api-key 凭据状态,因此以某适配器家族的 namespace 注册一次即可收到该家族的全部卡片,含手工声明的路由;手工声明的草稿卡尚无目录行,保存之前不分发。`settings.models.footer`(list)渲染在行列表与新增控件之后。注册方通过 `ctx.slots.inject` 激活,并以 type-only import 引入本包 `/client` 入口;没有注册方时两个席位均不渲染任何内容。
Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepSeek Account**)。其编辑器展示共享的 DeepSeek 模型目录,不提供 API Key 或 Base URL 输入框。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。
Models 页面包含 **DeepSeek 账号**(`deepseek-account`,英文为 **DeepSeek Account**)。其编辑器展示共享的 DeepSeek 模型目录,不提供 API Key 或 Base URL 输入框;目录保存到账号路由自己的设置段(默认为 `llm-deepseek-account`),因此账号侧的编辑不会改写 official 路由读取的 `llm-deepseek` 段。账号可用模型目录为空时隐藏账号行,包括登录前和退登后;账号模型恢复可用时重新显示。
-----
@@ -182,8 +182,9 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
const node = useMemo(() => schema.nodeAtPath(root, settingsPath), [root, schema, settingsPath])
const fallback = schema.getPath(namespace.value, settingsPath)
const disabled = props.readOnly || busy
const layout = layoutOf(namespace.ns)
const accountProvider = props.provider === 'deepseek-account'
// Account settings use a configurable Cordis entry id.
const layout = accountProvider ? 'deepseek' : layoutOf(namespace.ns)
const keyRef = refFor(schema, namespace, settingsPath, props.provider)
// The same schema read the create card makes, so the choices offered here
// and there cannot drift apart: both come from the adapter's own `Config`.
@@ -137,9 +137,29 @@ function wireNamespaces(): SettingsNamespaceView[] {
secrets: [],
revision: 4,
},
{
ns: 'llm-deepseek-account',
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
value: {
baseURL: 'https://base',
defaultContextWindow: 1_000_000,
maxTokens: 256_000,
models: DEFAULT_DEEPSEEK_MODELS,
},
base: { defaultContextWindow: 1_000_000, maxTokens: 256_000, models: DEFAULT_DEEPSEEK_MODELS },
user: {},
autoGenerate: true, applies: 'live',
secrets: [],
revision: 0,
},
]
}
/** The account provider's settings namespace, under a composition's entry id. */
function accountNamespace(ns = 'llm-deepseek-account'): SettingsNamespaceView {
return { ...wireNamespaces().find(view => view.ns === 'llm-deepseek-account')!, ns }
}
/** Credentials answers over the Remote carrier, which has no envelope. */
function remoteOk<T>(value: T) {
return { ok: true as const, value }
@@ -1884,18 +1904,103 @@ describe('apiKeyFailure', () => {
})
it.each([en, zh])('edits the account model catalog without credential or endpoint fields', async (copy) => {
const scripted = scriptedFace({})
const mutate = vi.fn(() => Promise.resolve(remoteOk(accountNamespace())))
const scripted = scriptedFace({ mutate })
const ops = operationsWith(scripted.face)
const describe = vi.spyOn(ops, 'describeCredential')
const namespace = accountNamespace()
const onClose = vi.fn()
render(<ProviderEditor provider="deepseek-account" displayName={copy.deepSeekAccount}
namespace={wireNamespaces()[0]!} settingsPath={[]} schema={settingsSchema}
operations={ops} t={key => copy[key]} readOnly={false} onClose={() => {}} />)
namespace={namespace} settingsPath={[]} schema={settingsSchema}
operations={ops} t={key => copy[key]} readOnly={false} onClose={onClose} />)
expect(screen.queryByLabelText(copy.keyInput)).toBeNull()
expect(screen.queryByLabelText(copy.baseUrl)).toBeNull()
expect(describe).not.toHaveBeenCalled()
expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy()
expect(screen.queryByText(content => content.includes(copy.advancedHint))).toBeNull()
await expect(`${document.body.textContent}\n`)
.toMatchFileSnapshot(`./expected/deepseek-account-${copy === en ? 'en' : 'zh'}.txt`)
const set = vi.spyOn(ops, 'storeCredential')
fireEvent.change(screen.getAllByLabelText(new RegExp(copy.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } })
fireEvent.click(screen.getByText(copy.apply))
await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) })
expect(mutate.mock.calls[0]).toEqual([
'llm-deepseek-account',
[{
op: 'set',
path: ['models'],
value: [
{
id: 'deepseek-v4-mini',
name: 'DeepSeek-V4-Flash',
description: 'Preserved hidden detail',
contextWindow: 1_000_000,
},
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: 1_000_000 },
],
}],
0,
])
expect(set).not.toHaveBeenCalled()
})
it('keeps the DeepSeek editor for an account route under a renamed settings entry', async () => {
const namespace = accountNamespace('team-account-entry')
const mutate = vi.fn(() => Promise.resolve(remoteOk(namespace)))
const ops = operationsWith(scriptedFace({ mutate }).face)
const set = vi.spyOn(ops, 'storeCredential')
const onClose = vi.fn()
render(<ProviderEditor provider="deepseek-account" displayName={en.deepSeekAccount}
namespace={namespace} settingsPath={[]} schema={settingsSchema}
operations={ops} t={t} readOnly={false} onClose={onClose} />)
expect(screen.queryByText(content => content.includes(en.advancedHint))).toBeNull()
expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy()
fireEvent.change(screen.getAllByLabelText(new RegExp(en.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } })
fireEvent.click(screen.getByText(en.apply))
await waitFor(() => { expect(onClose).toHaveBeenCalledWith(true) })
expect(mutate).toHaveBeenCalledWith('team-account-entry', expect.any(Array), 0)
expect(set).not.toHaveBeenCalled()
})
it('opens the account row from the section and saves to its own namespace', async () => {
const namespace = accountNamespace()
const mutate = vi.fn(() => Promise.resolve(remoteOk(namespace)))
const { controller, set } = await mountSection({ mutate })
const row = controller.store.getSnapshot().rows[0]!
await act(async () => { controller.store.update((state) => {
state.rows = [{
...row,
accountAvailable: true,
entry: {
provider: 'deepseek-account', displayName: en.deepSeekAccount, settingsNs: namespace.ns, settingsPath: [], active: true,
},
}, ...state.rows]
}) })
fireEvent.click(screen.getByRole('button', {
name: providerCopy(en.editProvider, { provider: 'deepseek-account', displayName: en.deepSeekAccount }),
}))
expect(screen.getByDisplayValue('deepseek-v4-flash')).toBeTruthy()
fireEvent.change(screen.getAllByLabelText(new RegExp(en.modelId))[0]!, { target: { value: 'deepseek-v4-mini' } })
fireEvent.click(screen.getByText(en.apply))
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
expect(mutate.mock.calls[0]).toEqual([
'llm-deepseek-account',
[{
op: 'set',
path: ['models'],
value: [
{
id: 'deepseek-v4-mini',
name: 'DeepSeek-V4-Flash',
description: 'Preserved hidden detail',
contextWindow: 1_000_000,
},
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: 1_000_000 },
],
}],
0,
])
expect(set).not.toHaveBeenCalled()
})
it('renders the localized account row and supports catalogs without capacity defaults', async () => {
@@ -1907,7 +2012,7 @@ it('renders the localized account row and supports catalogs without capacity def
}) })
expect(screen.getByText(en.deepSeekAccount)).toBeTruthy()
view.unmount()
const namespace = wireNamespaces()[0]!
const namespace = accountNamespace()
render(<ProviderEditor provider="deepseek-account" displayName={en.deepSeekAccount}
namespace={{ ...namespace, value: { models: [] }, base: { models: [] } }} settingsPath={[]} schema={settingsSchema}
operations={operationsWith(scripted.face)} t={t} readOnly={false} onClose={() => {}} />)
@@ -62,6 +62,15 @@ const NAMESPACES = [
secrets: [],
revision: 0,
},
{
ns: 'llm-deepseek-account',
schema: {},
value: { baseURL: 'https://base' },
base: { baseURL: 'https://base' },
autoGenerate: true, applies: 'live' as const,
secrets: [],
revision: 0,
},
{
ns: 'llm-pi-ai',
schema: {},
@@ -346,7 +355,7 @@ describe('edge joins', () => {
it.each([false, true])('uses account availability without asking for an API key: %s', async (accountAvailable) => {
const { ctx, mirror, seenRefs } = api({ accountAvailable, providers: async () => ok({ providers: [{
provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek', settingsPath: [], active: true,
provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek-account', settingsPath: [], active: true,
}] }) })
const store = new ModelsSettingsStore(ctx, settingsSchema, mirror)
await store.load()
@@ -356,12 +365,13 @@ it.each([false, true])('uses account availability without asking for an API key:
expect(rows[0]).toMatchObject({ accountAvailable: true, apiKeyEnv: undefined, credential: undefined })
expect(providerUsable(rows[0]!)).toBe(true)
}
expect(store.store.getSnapshot().namespaces.get('llm-deepseek-account')?.ns).toBe('llm-deepseek-account')
expect(seenRefs).toEqual([])
})
it('removes the account row after sign-out and restores it after sign-in', async () => {
const overrides = { accountAvailable: true, providers: async () => ok({ providers: [{
provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek', settingsPath: [], active: true,
provider: 'deepseek-account', displayName: 'DeepSeek Account', settingsNs: 'llm-deepseek-account', settingsPath: [], active: true,
}, ...DIRECTORY] }) }
const { ctx, mirror } = api(overrides)
const store = new ModelsSettingsStore(ctx, settingsSchema, mirror)
@@ -4,19 +4,19 @@
# pnpm run verify-translation-pairing --write packages/client/ui-settings-subagent/README.md
/:
en: 91ac9c71d17a231a
zh: 0e18ebe47ff01aed
zh: c0f89c3d36b51581
/deepseek-ai-dsh-client-ui-settings-subagent:
en: d1fc570c45a5f42e
zh: 01eabcac395bca62
/deepseek-ai-dsh-client-ui-settings-subagent/summary:
en: 4b6bf8db1752b902
zh: 5bdba2bfe06234b4
zh: b8ac25ba6bdb96fb
/deepseek-ai-dsh-client-ui-settings-subagent/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-client-ui-settings-subagent/use-this-package:
en: 09ac65efa3352ef4
zh: 3eda3d453764fdff
zh: 0a6f9e74e304ef1f
/deepseek-ai-dsh-client-ui-settings-subagent/understand-the-implementation:
en: 5124ebc1880ba9fa
zh: 89c2eb2d483962a7
@@ -1,5 +1,5 @@
---
description: "dsh Web 客户端插件页上的 Subagent 设置页:subagent 命名空间的委派深度与并行容量,以及 subagent-model-selection 里 Agent 可选的模型,同一页一次保存。"
description: "dsh Web 客户端插件页上的子智能体设置页:subagent 命名空间的委派深度与并行容量,以及 subagent-model-selection 里 Agent 可选的模型,同一页一次保存。"
kind: "package-reference"
---
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
在侧栏打开**插件**,在官方分组里选择 **Subagent**,即可设置委派可以多深、多宽,以及 Agent 可以为 Subagent 选择哪些模型。页面把 Host 的两个命名空间 `subagent` 与 `subagent-model-selection` 收在一次保存之下;Host 服务其中任一命名空间时页面就存在,并只显示被服务的部分。
在侧栏打开**插件**,在官方分组里选择**子智能体**,即可设置委派可以多深、多宽,以及 Agent 可以为子智能体选择哪些模型。页面把 Host 的两个命名空间 `subagent` 与 `subagent-model-selection` 收在一次保存之下;Host 服务其中任一命名空间时页面就存在,并只显示被服务的部分。
## 目录
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
**运行限制**把**最大递归深度**和 **Subagent 并行数量上限**并排放置,窄屏时上下堆叠;每个标签旁的信息按钮展开它的规则——深度是一张两行示例表,容量是计数口径——填了不在范围内的整数会阻止保存并在字段下说明。深度让位于工具自己的上限;容量统计同一主 Agent 下所有层级存活的 Subagent,主 Agent 不计入。
**运行限制**把**最大递归深度**和**子智能体并行数量上限**并排放置,窄屏时上下堆叠;每个标签旁的信息按钮展开它的规则——深度是一张两行示例表,容量是计数口径——填了不在范围内的整数会阻止保存并在字段下说明。深度让位于工具自己的上限;容量统计同一主 Agent 下所有层级存活的子智能体,主 Agent 不计入。
**模型选择**把权限开关和精确的适配器路由一起暂存。开启时至少要选一条路由;关闭会保留已选路由以备后用。Host 已存储但没有适配器公布的路由留在**已保存但当前不可用**分组里并且仍可移除;加载失败的提供方会被报告而不隐藏其他提供方,加载失败时提供**重试**。
@@ -69,23 +69,23 @@ export const zh: Record<SubagentSettingsLocaleKey, string> = {
save: '保存',
saving: '保存中…',
saveFailed: '本部署没有接受这些值,已保留供你修改。',
subagentTitle: 'Subagent',
subagentDescription: '设置 Subagent 的递归层级、数量和模型。',
subagentTitle: '子智能体',
subagentDescription: '设置子智能体的递归层级、数量和模型。',
subagentLimitsTitle: '运行限制',
subagentMaxDepth: '最大递归深度',
subagentDepthHelpLabel: '最大递归深度说明',
subagentDepthHelp: '限制 Agent 创建 Subagent 的递归层级。',
subagentDepthZero: '禁用 Subagent',
subagentDepthOne: '仅允许主 Agent 创建 Subagent',
subagentDepthHelp: '限制 Agent 创建子智能体的递归层级。',
subagentDepthZero: '禁用子智能体',
subagentDepthOne: '仅允许主 Agent 创建子智能体',
subagentDepthOverride: '如果某个工具单独设置了最大递归深度,以该工具的设置为准。',
subagentMaxActive: 'Subagent 并行数量上限',
subagentCapacityHelpLabel: 'Subagent 并行数量上限说明',
subagentCapacityHelp: '同一主 Agent 下,所有递归层级同时存活的 Subagent 总数,主 Agent 不计入。达到上限时,新的启动请求会被拒绝。',
subagentMaxActive: '子智能体并行数量上限',
subagentCapacityHelpLabel: '子智能体并行数量上限说明',
subagentCapacityHelp: '同一主 Agent 下,所有递归层级同时存活的子智能体总数,主 Agent 不计入。达到上限时,新的启动请求会被拒绝。',
subagentDepthInvalid: '请输入不小于 0 的整数。',
subagentCapacityInvalid: '请输入不小于 1 的整数。',
subagentModelSelectionTitle: '模型选择',
subagentModelSelectionToggle: '允许 Agent 为 Subagent 选择模型',
subagentModelSelectionChoose: '开启后,Agent 可以从下方授权模型中,为每个 Subagent 选择提供方、模型和推理强度。仅影响新会话。',
subagentModelSelectionToggle: '允许 Agent 为子智能体选择模型',
subagentModelSelectionChoose: '开启后,Agent 可以从下方授权模型中,为每个子智能体选择提供方、模型和推理强度。仅影响新会话。',
subagentModelSelectionAllowed: 'Agent 可选择的模型',
subagentModelSelectionLoading: '正在加载模型…',
subagentModelSelectionLoadFailed: '无法加载模型。',
@@ -96,7 +96,7 @@ export const zh: Record<SubagentSettingsLocaleKey, string> = {
subagentModelSelectionEmpty: '当前没有模型提供方公布模型。',
subagentModelSelectionRequired: '保存前请至少选择一个模型。',
subagentModelSelectionConflict: '设置已在其他位置更新。请放弃修改后重试。',
subagentModelSelectionOff: '关闭后,Subagent 使用配置的默认模型或继承父 Agent 的模型;已选模型会保留。',
subagentModelSelectionOff: '关闭后,子智能体使用配置的默认模型或继承父 Agent 的模型;已选模型会保留。',
}
/**
@@ -62,7 +62,7 @@ describe('ui-settings-subagent apply', () => {
await vi.waitFor(() => { expect(slots.entries('plugins.item')).toHaveLength(1) })
const entry = slots.entries('plugins.item')[0]!
expect(entry.options).toMatchObject({ id: 'subagent', order: 30 })
expect(resolveSlotLabel(entry.options.label)).toBe('Subagent')
expect(resolveSlotLabel(entry.options.label)).toBe('子智能体')
expect(entry.locale).toBe(NS)
const face = (entry.inject as () => Pick<SubagentCardFace, 'hooks'>)()
expect(Object.keys(face.hooks).sort()).toEqual(['subagentLimitsCard', 'subagentModelSelectionCard'])
+4 -4
View File
@@ -18,8 +18,8 @@
en: 2417d8409aad6ca7
zh: 449eee623a2f334b
/deepseek-ai-dsh-client-ui-settings/use-this-package/configuration-forms:
en: 4aee5cd02d22d2d3
zh: be90194b2c975f1d
en: 5bda0401a6a4f158
zh: 6778a64d3e2a7453
/deepseek-ai-dsh-client-ui-settings/use-this-package/following-served-namespaces:
en: 2edd420502bebce1
zh: 6624abe01110c012
@@ -51,8 +51,8 @@
en: ca75c51c89c2c9b0
zh: de54f3467ef3b9be
/deepseek-ai-dsh-client-ui-settings/known-limitations-and-deferred-work:
en: 5315511eb8650c68
zh: c9b4f825a82e4447
en: d8ef5c4f3a48e05f
zh: 7c335dc8543249c6
/deepseek-ai-dsh-client-ui-settings/known-limitations-and-deferred-work/dev-note:
en: 26f0b742de49074e
zh: ee7321bad1101bf1
+2 -2
View File
@@ -29,7 +29,7 @@ Feature plugins use this package to store and edit their preferences without re-
### Configuration forms
`ctx.configForms.developerTools` owns the shared Web and desktop preference `ui-settings.enabled`, defaulting to `true`. Its `enabled` observable publishes accepted choices and `setEnabled` uses the same ordered settings writes. Desktop and loopback Web persist to the Host document; remote Web keeps this choice in one browser-local observable until reload without issuing Host writes. This controls presentation and HTML preview permissions, not Host authorization or Session recording. Host-backed clients keep developer features disabled until the first accepted schema-resolved value arrives; missing or failed initial responses do not enable them. Later refreshes retain the last accepted choice.
`ctx.configForms.developerTools` owns the Coding Tools switch and the shared Web and desktop preference `ui-settings.enabled`, defaulting to `true`. Its `enabled` observable publishes accepted choices and `setEnabled` uses the same ordered settings writes. Desktop and loopback Web persist to the Host document; remote Web keeps this choice in one browser-local observable until reload without issuing Host writes. This controls presentation and HTML preview permissions, not Host authorization or Session recording. Host-backed clients keep developer features disabled until the first accepted schema-resolved value arrives; missing or failed initial responses do not enable them. Later refreshes retain the last accepted choice.
Feature adapters use `ctx.configForms.get(entryId)` to obtain accepted values and a write queue shared by every editor of that Host entry. Snapshots contain resolved `value`, inherited `base`, raw `user`, revision, writability, and persistence mode. `set` and `unset` submit one operation; `mutate` submits one atomic operation list. Staged editors pass the revision read before editing; conflicts preserve their drafts. Unsetting removes the override and restores inheritance.
@@ -103,7 +103,7 @@ None; this package neither assembles nor sends a provider request.
These limits define where the settings transport cannot reach; they are current package constraints.
- **Non-loopback pages get no durable settings** — this Client keeps Host persistence disabled there, so a form starts `unavailable` and never crosses the wire; form writes are inert even though Connection authentication covers the API. The shared Developer tools preference instead provides browser-local changes.
- **Non-loopback pages get no durable settings** — this Client keeps Host persistence disabled there, so a form starts `unavailable` and never crosses the wire; form writes are inert even though Connection authentication covers the API. The shared Coding Tools preference instead provides browser-local changes.
<a id="dev-note"></a>
### Dev Note
+2 -2
View File
@@ -29,7 +29,7 @@ kind: "package-reference"
### 配置表单
`ctx.configForms.developerTools` 管理 Web 和桌面端共享的偏好 `ui-settings.enabled`,默认为 `true`。其 `enabled` 可观察值发布已接受的选择,`setEnabled` 使用相同的有序设置写入。桌面端和回环 Web 将设置持久化到 Host 文档;远程 Web 将此选择保存在单个浏览器本地可观察值中,刷新后重置,不发送 Host 写入。此设置控制界面展示和 HTML 预览权限,不控制 Host 授权或 Session 记录。 使用 Host 偏好的客户端在首个经过 schema 解析并接受的值到达前保持开发者功能关闭;首次响应缺失或失败不会启用它们。后续刷新保留已接受的选择。
`ctx.configForms.developerTools` 管理代码工作工具开关与 Web 和桌面端共享的偏好 `ui-settings.enabled`,默认为 `true`。其 `enabled` 可观察值发布已接受的选择,`setEnabled` 使用相同的有序设置写入。桌面端和回环 Web 将设置持久化到 Host 文档;远程 Web 将此选择保存在单个浏览器本地可观察值中,刷新后重置,不发送 Host 写入。此设置控制界面展示和 HTML 预览权限,不控制 Host 授权或 Session 记录。 使用 Host 偏好的客户端在首个经过 schema 解析并接受的值到达前保持开发者功能关闭;首次响应缺失或失败不会启用它们。后续刷新保留已接受的选择。
功能适配器使用 `ctx.configForms.get(entryId)` 获取该 Host 条目所有编辑器共享的已接受值和写入队列。快照包含解析后的 `value`、继承 `base`、原始 `user`、修订号、可写性和持久化模式。`set` 与 `unset` 提交单个操作,`mutate` 提交一个原子操作列表。暂存编辑器传入编辑前读取的修订号;冲突时保留草稿。清除操作移除覆盖并恢复继承。
@@ -103,7 +103,7 @@ kind: "package-reference"
这些限制说明设置传输层够不到的地方;它们是当前包约束。
- **非 loopback 页面没有持久化设置**:本 Client 在那里禁用 Host 持久化,因此 表单以 `unavailable` 起步且从不跨线路;尽管 Connection 认证覆盖 API,表单写入仍在那里无效。共享的开发者工具偏好单独提供浏览器本地变更。
- **非 loopback 页面没有持久化设置**:本 Client 在那里禁用 Host 持久化,因此 表单以 `unavailable` 起步且从不跨线路;尽管 Connection 认证覆盖 API,表单写入仍在那里无效。共享的代码工作工具偏好单独提供浏览器本地变更。
<a id="dev-note"></a>
### 开发备注
@@ -21,8 +21,8 @@
en: 949156aa02ec8b07
zh: d3675754e2a37b5d
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/how-it-reads:
en: c6d2f387753d85f7
zh: ae1617fb90a10fa4
en: 5d8bf2e43d9b6dd0
zh: b0d2d1d5a6f520db
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/excel-preview:
en: b99b0e0b8efc2853
zh: 4327352d1334fc6b
@@ -46,7 +46,7 @@ After all text pages load, Markdown images use the authenticated `/api/file` rou
<a id="how-it-reads"></a>
## How it reads
Developer tools selects the HTML preview policy in both Web and desktop. The renderer receives `interactivePreview` from plugin assembly. Off uses a DOMPurify-sanitized complete static document in an iframe with no sandbox permissions: CSP blocks scripts, external resources, connections, forms, and nested frames; all `href` and `xlink:href` attributes, refresh directives, and declarative shadow roots are removed before reparsing. Inline styles and data images remain visible, and related files are not read. On uses the scripted Blob preview described below. A mode change unmounts the previous frame and aborts its pending related-file reads. Static preview releases CSS/JS resource subscriptions and keeps the root file watched. Other document formats retain their own policies.
Coding Tools selects the HTML preview policy in both Web and desktop. The renderer receives `interactivePreview` from plugin assembly. Off uses a DOMPurify-sanitized complete static document in an iframe with no sandbox permissions: CSP blocks scripts, external resources, connections, forms, and nested frames; all `href` and `xlink:href` attributes, refresh directives, and declarative shadow roots are removed before reparsing. Inline styles and data images remain visible, and related files are not read. On uses the scripted Blob preview described below. A mode change unmounts the previous frame and aborts its pending related-file reads. Static preview releases CSS/JS resource subscriptions and keeps the root file watched. Other document formats retain their own policies.
Both HTML modes set the initial iframe name to `dsh-sidebar-html-<tab-id>` for Desktop shortcut routing. This correlation does not grant the preview access to the parent document.
@@ -57,7 +57,7 @@ The body reads its record, navigation and lifetime through `useTabInfo().tab`. `
- **Complete bytes** — PDF, HTML, common images, and spreadsheets call `remote.workspaceFiles.readBytes(sessionId, path, {}, signal)`. The binary Remote returns `data: Uint8Array<ArrayBuffer>` directly for `{ kind: 'bytes', data }`. The Host's `maxFileBytes` cap rejects oversized files rather than truncating them. PDF and spreadsheet renderers copy retained bytes before Worker transfer, keeping the Preview buffer usable. Bytes stay in transient view state, never persisted layouts or Session JSONL. Loading-mode changes retire previous results.
- **Reload** — manual reload rereads only the current Preview tab through its Remote callbacks, preserving its scroll preference and retiring older requests. Automatic refresh uses the same callbacks after a `ResourceGroup` member changes. Each member's first metadata establishes a baseline without triggering reload or first-read version reconciliation; later changes received during a read remain pending for another refresh. Reads neither refresh shared metadata nor clear another tab's notice.
When Developer tools is enabled, HTML fills the body edge to edge in a Blob iframe with exactly `sandbox="allow-scripts"`, without `allow-same-origin`; scripts cannot access the parent application's origin or file reader. The renderer loads directly declared relative `.js` classic scripts and `.css` stylesheets through its injected Remote callback, with fixed safety limits of 4 MiB per asset, 32 MiB total, and 64 distinct assets. Host code resolves the related path, and `readBytes` with `baseFile` returns native bytes. The dependency Resource is added after the read returns, using its `absolutePath` or a string `error.details.path` on failure; without a Host path, the Client does not guess one. Inside the renderer, base64 is used only to embed the iframe bootstrap payload in script text. A `<base href>` leaves dependency resolution to the browser, as do HTTPS resources. Local module imports, CSS `url()`/`@import`, and dynamic `fetch` do not use Host file access. Read failures, invalid UTF-8, or exceeded limits fail the preview rather than publishing a partial asset package. Replacing or unmounting the document releases its Blob URL.
When Coding Tools is enabled, HTML fills the body edge to edge in a Blob iframe with exactly `sandbox="allow-scripts"`, without `allow-same-origin`; scripts cannot access the parent application's origin or file reader. The renderer loads directly declared relative `.js` classic scripts and `.css` stylesheets through its injected Remote callback, with fixed safety limits of 4 MiB per asset, 32 MiB total, and 64 distinct assets. Host code resolves the related path, and `readBytes` with `baseFile` returns native bytes. The dependency Resource is added after the read returns, using its `absolutePath` or a string `error.details.path` on failure; without a Host path, the Client does not guess one. Inside the renderer, base64 is used only to embed the iframe bootstrap payload in script text. A `<base href>` leaves dependency resolution to the browser, as do HTTPS resources. Local module imports, CSS `url()`/`@import`, and dynamic `fetch` do not use Host file access. Read failures, invalid UTF-8, or exceeded limits fail the preview rather than publishing a partial asset package. Replacing or unmounting the document releases its Blob URL.
PNG, JPEG, GIF, WebP, BMP, ICO, and SVG render through Blob URLs in an `<img>` static-image context, rounded inside a 12px inset. Images default to fit width without enlarging content smaller than the pane; 100% uses the image's intrinsic CSS-pixel width. The shared zoom controls can produce horizontal and vertical scrolling, but do not provide drag-to-pan. Zoom changes retain the same `<img>` and Blob URL, so animated images continue playing. Raster formats can soften above their intrinsic size, while SVG remains on the browser's vector rendering path. SVG markup never enters the application DOM or an iframe, so its scripts cannot execute or reach the parent page. Replacing or unmounting the image revokes its Blob URL.
@@ -46,7 +46,7 @@ tab 使用 `fileAddressFor` 构造的 Session 地址,携带相对或绝对路
<a id="how-it-reads"></a>
## 怎么读
Web 和桌面端均通过开发者工具选择 HTML 预览策略。渲染器从插件组装层接收 `interactivePreview`。关闭时,将经 DOMPurify 清理的完整静态文档放入不授予沙箱权限的 iframe:CSP 禁止脚本、外部资源、连接、表单和嵌套框架;所有 `href` 和 `xlink:href` 属性、刷新指令及声明式 Shadow DOM 均在重新解析前被移除。行内样式和 data 图片仍可显示,不读取关联文件。开启时使用下述支持脚本的 Blob 预览。切换模式会卸载之前的框架并中止其待处理关联文件读取。静态预览释放 CSS/JS 的 Resource 订阅,根文件继续监听。其他文档格式保持各自策略。
Web 和桌面端均通过代码工作工具选择 HTML 预览策略。渲染器从插件组装层接收 `interactivePreview`。关闭时,将经 DOMPurify 清理的完整静态文档放入不授予沙箱权限的 iframe:CSP 禁止脚本、外部资源、连接、表单和嵌套框架;所有 `href` 和 `xlink:href` 属性、刷新指令及声明式 Shadow DOM 均在重新解析前被移除。行内样式和 data 图片仍可显示,不读取关联文件。开启时使用下述支持脚本的 Blob 预览。切换模式会卸载之前的框架并中止其待处理关联文件读取。静态预览释放 CSS/JS 的 Resource 订阅,根文件继续监听。其他文档格式保持各自策略。
两种 HTML 模式均将 iframe 初始名称设为 `dsh-sidebar-html-<tab-id>`,供 Desktop 快捷键路由关联目标。这种关联不授予预览访问父文档的权限。
@@ -57,7 +57,7 @@ Web 和桌面端均通过开发者工具选择 HTML 预览策略。渲染器从
- **完整字节** —— PDF、HTML、常见图片和表格调用 `remote.workspaceFiles.readBytes(sessionId, path, {}, signal)`。二进制 Remote 直接返回 `data: Uint8Array<ArrayBuffer>`,供 `{ kind: 'bytes', data }` 使用。Host 的 `maxFileBytes` 上限拒绝超大文件,不截断。PDF 和表格渲染器在传给 Worker 前复制保留的字节,使 Preview 缓冲区仍可使用。字节仅保存在临时视图状态中,绝不进入持久布局或 Session JSONL。加载模式变化会淘汰先前结果。
- **重新载入** —— 手动重新载入仅让当前 Preview tab 通过自己的 Remote 回调重读,保留滚动偏好并淘汰旧请求。`ResourceGroup` 成员变化后,自动刷新使用相同回调。各成员首次元数据仅建立基线,不触发重新载入或首读版本对账;后续在读取期间收到的变化仍会留待下一次刷新。读取既不刷新共享元数据,也不清除其它 tab 的提示。
开启开发者工具时,HTML 以贴合正文四边的 Blob iframe 运行,沙箱属性严格为 `sandbox="allow-scripts"`,不含 `allow-same-origin`;脚本无法访问父应用的源或文件读取接口。渲染器通过注入的 Remote 回调,加载直接声明的相对 `.js` 经典脚本和 `.css` 样式表;固定安全上限为单个资源 4 MiB、总计 32 MiB、64 个不同资源。Host 代码解析关联路径,带 `baseFile` 的 `readBytes` 返回原生字节。依赖 Resource 在读取返回后加入,使用返回的 `absolutePath`,失败时则使用字符串类型的 `error.details.path`;没有 Host 路径时,Client 不自行猜测。在渲染器内部,base64 仅用于把 iframe 引导载荷嵌入脚本文本。`<base href>` 将依赖解析交给浏览器,HTTPS 资源也由浏览器处理。本地模块 import、CSS `url()`/`@import` 和动态 `fetch` 不使用 Host 文件访问。读取失败、无效 UTF-8 或超出上限都使预览失败,不发布部分资源包。替换或卸载文档会释放其 Blob URL。
开启代码工作工具时,HTML 以贴合正文四边的 Blob iframe 运行,沙箱属性严格为 `sandbox="allow-scripts"`,不含 `allow-same-origin`;脚本无法访问父应用的源或文件读取接口。渲染器通过注入的 Remote 回调,加载直接声明的相对 `.js` 经典脚本和 `.css` 样式表;固定安全上限为单个资源 4 MiB、总计 32 MiB、64 个不同资源。Host 代码解析关联路径,带 `baseFile` 的 `readBytes` 返回原生字节。依赖 Resource 在读取返回后加入,使用返回的 `absolutePath`,失败时则使用字符串类型的 `error.details.path`;没有 Host 路径时,Client 不自行猜测。在渲染器内部,base64 仅用于把 iframe 引导载荷嵌入脚本文本。`<base href>` 将依赖解析交给浏览器,HTTPS 资源也由浏览器处理。本地模块 import、CSS `url()`/`@import` 和动态 `fetch` 不使用 Host 文件访问。读取失败、无效 UTF-8 或超出上限都使预览失败,不发布部分资源包。替换或卸载文档会释放其 Blob URL。
PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `<img>` 静态图片上下文中渲染,带 12px 内边距和圆角。图片默认适应宽度,但不会放大小于面板的内容;100% 使用图片的固有 CSS 像素宽度。共享缩放控件可产生横向和纵向滚动,但不提供拖拽平移。缩放不会替换 `<img>` 或 Blob URL,因此动画图片会继续播放。位图超过固有尺寸后可能变虚,SVG 则继续使用浏览器的矢量渲染路径。SVG 标记绝不进入应用 DOM 或 iframe,因此其中的脚本无法执行,也无法访问父页面。替换或卸载图片会撤销其 Blob URL。
@@ -9,8 +9,8 @@
en: ec051915b3d81cda
zh: 3a0ef832e3a5e419
/deepseek-ai-dsh-time-context/summary:
en: 675b446b73df2990
zh: 7b051c07244f55cb
en: e4e308b9fd3fcd56
zh: 9fb4d40ebe621d6a
/deepseek-ai-dsh-time-context/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -21,8 +21,8 @@
en: 104b5d5609b00f33
zh: f6115b4eaa24bda7
/deepseek-ai-dsh-time-context/use-this-package/configuration:
en: 7b4accf50fd83abb
zh: 8804f10f66bbf159
en: e6ac1f330c18c69c
zh: 9e969a5b377ba62a
/deepseek-ai-dsh-time-context/use-this-package/choosing-the-zone:
en: 7eed0d784ad7381b
zh: 47a849df0ce80105
@@ -57,14 +57,14 @@
en: d583e2c01b797bef
zh: 74b908a4fd7972e7
/deepseek-ai-dsh-time-context/model-experience/preparation-time-temporal-context/token-effect:
en: cbcec14d9437c2a5
zh: 6628d75c0411959d
en: fc72bbd239df14db
zh: 750a04f8d4f1b6db
/deepseek-ai-dsh-time-context/model-experience/preparation-time-temporal-context/kv-cache-effect:
en: 2fbf2175168e8244
zh: f681f89596f500f5
/deepseek-ai-dsh-time-context/known-limitations-and-deferred-work:
en: e2637b93efef4f46
zh: c1d3321bbfe871ef
en: 8fcf131f1f00626b
zh: a99b928aca0b1fab
/deepseek-ai-dsh-time-context/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 3bb0c20c93f16f98
+5 -5
View File
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
`dsh-time-context` gives the model a clock: on eligible steps it appends a durable, source-attributed reading with the current time, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. It helps the model interpret otherwise-unqualified dates and times in the user's browser zone, and tells it to ask when current-turn browser zones are mixed or missing. The shipped Web bundle mounts it with Schedule. A positive `refreshIntervalMs` reduces how often readings accumulate; omission or `0` injects at every eligible step.
`dsh-time-context` gives the model a clock: on eligible steps it appends a durable, source-attributed reading with the current time, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. It helps the model interpret otherwise-unqualified dates and times in the user's browser zone, and tells it to ask when current-turn browser zones are mixed or missing. The shipped Web bundle mounts it with Schedule. Readings default to a 10-minute minimum interval; `refreshIntervalMs: 0` injects at every eligible step.
## Table of Contents
@@ -33,7 +33,7 @@ Each injected reading has three lines: an ISO-shaped timestamp with numeric offs
### Configuration
The minimal mount needs no configuration. A positive `refreshIntervalMs` suppresses injections that fall within that many milliseconds of the latest one; omission or `0` injects at every eligible entering pre-step whose signal is not already aborted.
The minimal mount needs no configuration. A positive `refreshIntervalMs` suppresses injections that fall within that many milliseconds of the latest one; omission uses 600000 ms (10 minutes), while `0` injects at every eligible entering pre-step whose signal is not already aborted.
```yaml
- name: '@deepseek-ai/dsh-time-context'
@@ -44,7 +44,7 @@ The minimal mount needs no configuration. A positive `refreshIntervalMs` suppres
| Field | Default | Meaning |
|---|---|---|
| `timeZone` | process zone | Fallback display zone when the open turn has no unique browser zone |
| `refreshIntervalMs` | `0` (every eligible step) | Minimum milliseconds between durable injections in one session |
| `refreshIntervalMs` | `600000` (10 minutes) | Minimum milliseconds between durable injections in one session |
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-time-context) is the exhaustive source for every accepted field and its JSDoc.
@@ -121,7 +121,7 @@ Elapsed since the preceding step context: <duration-or-unavailable>.
#### Token effect
Each reading accumulates until compaction shadows it. A positive interval reduces additions; omission or `0` adds one at every eligible preparation attempt.
Each reading accumulates until compaction shadows it. A positive interval reduces additions; `0` adds one at every eligible preparation attempt.
#### KV Cache effect
@@ -138,7 +138,7 @@ These limits define when clock context is a poor fit. They are current package c
- **Mixed turns ask** — if one open turn contains prompts from different browser zones, the model is told to clarify rather than guess which one owns an unqualified time.
- **Fallback is not user authority** — the configured or process zone formats the clock when current-turn browser zones are missing or mixed, but the model-facing policy still says to clarify.
- **Whole-second display** — timestamps and durations omit sub-second precision even though durable event times retain milliseconds.
- **History cost between compactions** — omission or `0` retains one reading for every eligible attempt; a positive interval reduces but does not eliminate this cost and may leave a later request without fresh browser-zone guidance.
- **History cost between compactions** — `0` retains one reading for every eligible attempt; a positive interval reduces but does not eliminate this cost and may leave a later request without fresh browser-zone guidance.
<a id="dev-note"></a>
### Dev Note
+5 -5
View File
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
`dsh-time-context` 给模型一只时钟:在符合条件的步骤上,它追加一条持久、带来源的读数,包含当前时间、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。它帮助模型按用户的浏览器时区解释未明确限定时区的日期与时间;时区来源混杂或缺失时,它告诉模型去询问。发布的 Web bundle 会随 Schedule 一起挂载它。正的 `refreshIntervalMs` 会减少读数累积的频率;省略或设为 `0` 时,每个符合条件的步骤都会注入。
`dsh-time-context` 给模型一只时钟:在符合条件的步骤上,它追加一条持久、带来源的读数,包含当前时间、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。它帮助模型按用户的浏览器时区解释未明确限定时区的日期与时间;时区来源混杂或缺失时,它告诉模型去询问。发布的 Web bundle 会随 Schedule 一起挂载它。读数默认采用 10 分钟的最小间隔;`refreshIntervalMs: 0` 会在每个符合条件的步骤注入。
## 目录
@@ -33,7 +33,7 @@ kind: "package-reference"
### 配置
最小挂载无需任何配置。正的 `refreshIntervalMs` 会抑制距最近一次注入不足该毫秒数的注入;省略或设为 `0` 时,每个信号尚未中止且将进入步骤的合格 pre-step 都会注入。
最小挂载无需任何配置。正的 `refreshIntervalMs` 会抑制距最近一次注入不足该毫秒数的注入;省略时采用 600000 毫秒(10 分钟),设为 `0` 时,每个信号尚未中止且将进入步骤的合格 pre-step 都会注入。
```yaml
- name: '@deepseek-ai/dsh-time-context'
@@ -44,7 +44,7 @@ kind: "package-reference"
| 字段 | 默认值 | 含义 |
|---|---|---|
| `timeZone` | 进程时区 | 当前开放轮次没有唯一浏览器时区时的显示回退时区 |
| `refreshIntervalMs` | `0`(每个合格步骤) | 同一会话中两次持久注入之间的最小毫秒数 |
| `refreshIntervalMs` | `600000`(10 分钟) | 同一会话中两次持久注入之间的最小毫秒数 |
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-time-context)是每个受支持字段及其 JSDoc 的穷尽式真源。
@@ -121,7 +121,7 @@ Elapsed since the preceding step context: <duration-or-unavailable>.
#### Token 影响
每个读数都会累积,直到压缩将其遮蔽。正数间隔会减少新增读数;省略或设为 `0` 时,每次合格的准备尝试都会添加一条。
每个读数都会累积,直到压缩将其遮蔽。正数间隔会减少新增读数;设为 `0` 时,每次合格的准备尝试都会添加一条。
#### KV Cache 影响
@@ -138,7 +138,7 @@ Elapsed since the preceding step context: <duration-or-unavailable>.
- **混合轮次会询问**:如果同一个开放轮次包含来自不同浏览器时区的提示词,模型会收到要求澄清的指令,而不会猜测哪个时区拥有未限定的时间。
- **回退值不代表用户权威**:浏览器来源信息缺失或混杂时,配置或进程时区用于格式化时钟,但面向模型的策略仍要求澄清。
- **整秒显示**:时间戳与持续时间省略亚秒精度,尽管持久事件时间保留毫秒。
- **压缩之间的历史成本**:省略或设为 `0` 时,每次合格尝试都会保留一条读数;正数间隔可以降低但无法消除该成本,也可能使后续请求缺少新鲜的浏览器时区指导。
- **压缩之间的历史成本**:设为 `0` 时,每次合格尝试都会保留一条读数;正数间隔可以降低但无法消除该成本,也可能使后续请求缺少新鲜的浏览器时区指导。
<a id="dev-note"></a>
### 开发备注
+5 -8
View File
@@ -56,7 +56,7 @@ export const inject = ['agents', 'sessionProjections']
export interface Config {
/** Fallback display zone when the open turn has no unique browser zone. Omit to use the process zone. */
timeZone?: string
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject at every eligible step. */
/** Minimum milliseconds between durable injections in one session. Defaults to 600000 (10 minutes); 0 injects at every eligible step. */
refreshIntervalMs?: number
}
@@ -115,11 +115,8 @@ function renderText(
}
/** Reject refresh intervals that cannot represent an exact elapsed-millisecond threshold. */
function validateRefreshInterval(refreshIntervalMs: number | undefined): void {
if (refreshIntervalMs !== undefined && (
!Number.isSafeInteger(refreshIntervalMs)
|| refreshIntervalMs < 0
)) {
function validateRefreshInterval(refreshIntervalMs: number): void {
if (!Number.isSafeInteger(refreshIntervalMs) || refreshIntervalMs < 0) {
throw new TypeError(
`time-context: refreshIntervalMs must be a non-negative safe integer, got ${String(refreshIntervalMs)}`,
)
@@ -134,7 +131,7 @@ function validateRefreshInterval(refreshIntervalMs: number | undefined): void {
*/
export function apply(ctx: Context, config: Config): void {
const timeZone = config.timeZone
const refreshIntervalMs = config.refreshIntervalMs
const refreshIntervalMs = config.refreshIntervalMs ?? 600_000
validateRefreshInterval(refreshIntervalMs)
let fallbackFormatter: Intl.DateTimeFormat
try {
@@ -193,7 +190,7 @@ export function apply(ctx: Context, config: Config): void {
if (decision.kind === 'reject' || signal.aborted) return decision
const now = Date.now()
const state = ctx.sessionProjections.stateOf(agent.session, 'timeContext') as TimeContextProjection
if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) {
if (refreshIntervalMs > 0) {
const lastInjection = state.lastInjectionTime
if (lastInjection != null
&& now >= lastInjection
@@ -28,7 +28,7 @@ async function jsonlFiles(dir: string): Promise<string[]> {
}
describe('time-context through the production headless profile', () => {
it('uses the process zone and persists one ordered context event per request', async () => {
it('uses the process zone and throttles context across turns by default', async () => {
let events: SessionEvent[] = []
const { stderr } = await runLoaderSmoke({
label: 'time-context headless smoke',
@@ -52,7 +52,7 @@ describe('time-context through the production headless profile', () => {
(event): event is SessionEvent<'user/message'> => event.type === 'user/message'
&& event.data.source.kind === 'time-context')
const starts = events.filter(event => event.type === 'step/start')
expect(contexts).toHaveLength(2)
expect(contexts).toHaveLength(1)
expect(starts).toHaveLength(2)
for (let index = 0; index < contexts.length; index += 1) {
expect(contexts[index]!.seq).toBeGreaterThan(starts[index]!.seq)
@@ -73,10 +73,6 @@ describe('time-context through the production headless profile', () => {
/Time sampled while preparing turn 1, step 1: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00\[Asia\/Shanghai\]/,
)
expect(contextText[0]).toContain('Elapsed since the preceding model-visible message: unavailable.')
expect(contextText[1]).toMatch(/Time sampled while preparing turn 2, step 1:/)
expect(contextText[1]).toMatch(
/Elapsed since the preceding model-visible message: (?:\d+d )?(?:\d+h )?(?:\d+m )?\d+s\./,
)
const headers = events.filter(event => event.type === 'request/header')
expect(JSON.stringify(headers)).not.toContain('Time sampled while preparing')
@@ -206,11 +206,8 @@ describe('durable step context', () => {
)
})
it.each([
['omitted interval', {}],
['zero interval', { refreshIntervalMs: 0 }],
] as const)('uses the preceding durable step-context timestamp after step one with %s', async (_label, config) => {
const { ctx } = await mount(config)
it('uses the preceding durable step-context timestamp after step one with zero interval', async () => {
const { ctx } = await mount({ refreshIntervalMs: 0 })
const session = Session.create(SessionId('later-step'))
const agent = sessionAgent(session)
openMessageTurn(session, 3)
@@ -292,8 +289,11 @@ describe('durable step context', () => {
expect(contextTexts(session)[1]).toContain('Elapsed since the preceding step context: 0s.')
})
it('uses a shadowed durable injection after resume and injects at the exact threshold', async () => {
const { ctx } = await mount({ refreshIntervalMs: 1_000 })
it.each([
['default interval', {}, 600_000],
['explicit interval', { refreshIntervalMs: 1_000 }, 1_000],
] as const)('uses a shadowed durable injection after resume at the exact %s threshold', async (_label, config, interval) => {
const { ctx } = await mount(config)
const original = Session.create(SessionId('seed-source'))
openMessageTurn(original, 1)
await fire(ctx, sessionAgent(original), 1, 1)
@@ -312,7 +312,7 @@ describe('durable step context', () => {
const resumed = Session.create(SessionId('resumed'), original.snapshotEvents())
const resumedAgent = sessionAgent(resumed)
vi.setSystemTime(BASE + 999)
vi.setSystemTime(BASE + interval - 1)
openMessageTurn(resumed, 2)
const beforeSkip = resumed.snapshotEvents().length
@@ -321,7 +321,7 @@ describe('durable step context', () => {
expect(resumed.snapshotEvents()).toHaveLength(beforeSkip)
expect(contextTexts(resumed)).toHaveLength(1)
vi.setSystemTime(BASE + 1_000)
vi.setSystemTime(BASE + interval)
await fire(ctx, resumedAgent, 2, 2)
expect(contextTexts(resumed)).toHaveLength(2)
@@ -472,7 +472,7 @@ describe('real agent-loop request history', () => {
it('persists one ordered context per request, accumulates readings, and leaves system headers unchanged', async () => {
const adapter = new ScriptedAdapter([toolCallResponse(), textResponse('done')])
const ctx = await loopHarness(adapter)
const ctx = await loopHarness(adapter, { refreshIntervalMs: 0 })
ctx.tools.register(defineContentToolFixture({
name: 'tick',
description: 'advance fake time',
+2 -2
View File
@@ -45,8 +45,8 @@
en: dc44a88997777988
zh: 56454aeace1c8b77
/deepseek-ai-dsh-tools/understand-the-implementation/ptc-mode:
en: b75b027e45340107
zh: beffa345cf76df51
en: 9e67cfffae432d15
zh: 3a061b8e805c07cd
/deepseek-ai-dsh-tools/understand-the-implementation/extension-points:
en: c2a14d6f22bc9428
zh: 89c1414fca465931
+1 -1
View File
@@ -130,7 +130,7 @@ New sub-calls use `<parent>:ptc:<n>` ids. Consumers treat these ids as opaque an
Successful image-bearing subcall results become deferred user-message context with `source.kind` set to `ptc-mode`. Other additional contexts retain their producing tool's attribution.
`run_code` accepts `timeoutMs` when the mounted runtime supports an override; its schema reports the configured default and maximum, the runtime's usage instructions and the Session working directory. The Node default is 120,000 ms with a 600,000 ms cap, including nested tool and approval waits. A wider `sandbox_permissions` mode requires a non-empty `justification` and approval before the program starts. The grant applies to that complete execution; standing Session policy and nested tools retain their own authority. Programs are never replayed automatically: inspect earlier effects before explicitly retrying a denied program.
`run_code` accepts `timeoutMs` when the mounted runtime supports an override; its schema reports the configured default and maximum, the runtime's usage instructions and the Session working directory. The Node default is 120,000 ms with a 600,000 ms cap, including nested tool and approval waits. A wider `sandbox_permissions` mode requires a non-empty `justification` and approval before the program starts. The schema asks the model to write that reason in the current user's language. The grant applies to that complete execution; standing Session policy and nested tools retain their own authority. Programs are never replayed automatically: inspect earlier effects before explicitly retrying a denied program.
<a id="extension-points"></a>
### Extension points
+1 -1
View File
@@ -130,7 +130,7 @@ ctx.tools.register(defineTool({
成功且包含图片的子调用结果会成为延后的 user-message 上下文,其 `source.kind` 为 `ptc-mode`。其他 additional context 保留其生产工具的归属。
当已挂载运行时支持覆盖时,`run_code` 接受 `timeoutMs`;其 schema 报告配置的默认值和上限、运行时使用说明及 Session 工作目录。Node 默认值为 120,000 ms,上限为 600,000 ms,包含嵌套工具和审批等待。更宽的 `sandbox_permissions` 模式要求非空 `justification`,并在程序启动前获得审批。授权仅用于该次完整执行;常驻 Session 策略与嵌套工具保留各自权限。程序不会自动重放:显式重试被拒程序前,应检查先前已发生的效果。
当已挂载运行时支持覆盖时,`run_code` 接受 `timeoutMs`;其 schema 报告配置的默认值和上限、运行时使用说明及 Session 工作目录。Node 默认值为 120,000 ms,上限为 600,000 ms,包含嵌套工具和审批等待。更宽的 `sandbox_permissions` 模式要求非空 `justification`,并在程序启动前获得审批。schema 提示模型使用当前用户的语言撰写理由。授权仅用于该次完整执行;常驻 Session 策略与嵌套工具保留各自权限。程序不会自动重放:显式重试被拒程序前,应检查先前已发生的效果。
<a id="extension-points"></a>
### 扩展点
+1 -1
View File
@@ -108,7 +108,7 @@ const RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
const RUN_CODE_CONTROLS = {
timeoutMs: { type: 'number', description: 'Positive elapsed-time budget in milliseconds, capped by the deployment maximum.' },
sandbox_permissions: { type: 'string', enum: [...ESCALATION_TARGETS], description: 'Wider sandbox mode for this complete program execution; requires justification and approval.' },
justification: { type: 'string', description: 'Reason this complete program needs wider access, shown to the user for approval.' },
justification: { type: 'string', description: 'Reason this complete program needs wider access, shown to the user for approval. Use the language of the user’s current request.' },
} as const
function controlParameters(runtime: PtcRuntime | undefined) {
@@ -4467,7 +4467,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'ApprovalRequestEvent',
declaration: 'export interface ApprovalRequestEvent {\n readonly agent: Agent;\n readonly toolName: string;\n readonly callId?: ToolCallId;\n readonly reason?: string;\n readonly signal?: AbortSignal;\n}',
declaration: 'export interface ApprovalRequestEvent {\n readonly agent: Agent;\n readonly toolName: string;\n readonly callId?: ToolCallId;\n readonly reason?: string;\n readonly displayReason?: {\n readonly en: string;\n readonly [locale: string]: string;\n };\n readonly signal?: AbortSignal;\n}',
},
{
name: 'ArchiveSessionOptions',
+2 -2
View File
@@ -27,8 +27,8 @@
en: 7eb705e3b132fcba
zh: 883207b13d381f9e
/deepseek-ai-dsh-tool-fs/use-this-package/policy-and-sandbox-behavior:
en: 245b4be79ea4cc6b
zh: ff15b41e778fadab
en: 53d4ac2db089ef39
zh: 214ea051ed56f500
/deepseek-ai-dsh-tool-fs/use-this-package/failures-and-recovery:
en: 962b09e21305a04b
zh: 382d2b8cce217569
Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff. Show more