diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml index 6b8dc5e1c0..07e39db02d 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md index 03a4269dd7..bb44957b6d 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md @@ -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 diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md index f11e0956c9..b7e07a376c 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md @@ -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 路由 diff --git a/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml new file mode 100644 index 0000000000..737e671e9a --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.i18n.yaml @@ -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 diff --git a/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.md b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.md new file mode 100644 index 0000000000..a4f4bf6e30 --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.md @@ -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. diff --git a/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.zh.md b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.zh.md new file mode 100644 index 0000000000..7f2d42f73b --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-09-19-timed-user-question-two-settlements.zh.md @@ -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 后继续存在。关闭面板不写记录,可见性遵循普通折叠,而不是保证回复始终可见。 diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 3766a01de6..7d3a25f9af 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -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() } diff --git a/apps/web/tests/agent-preset-selection.e2e.ts b/apps/web/tests/agent-preset-selection.e2e.ts index cfa4e81252..48f1722d7c 100644 --- a/apps/web/tests/agent-preset-selection.e2e.ts +++ b/apps/web/tests/agent-preset-selection.e2e.ts @@ -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() diff --git a/apps/web/tests/changed-files-turn.e2e.ts b/apps/web/tests/changed-files-turn.e2e.ts index fb47567d36..d356d48a64 100644 --- a/apps/web/tests/changed-files-turn.e2e.ts +++ b/apps/web/tests/changed-files-turn.e2e.ts @@ -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) diff --git a/apps/web/tests/developer-tools-settings.e2e.ts b/apps/web/tests/developer-tools-settings.e2e.ts index aec863b432..40481128d2 100644 --- a/apps/web/tests/developer-tools-settings.e2e.ts +++ b/apps/web/tests/developer-tools-settings.e2e.ts @@ -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 }) diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index c4bf43d2eb..cc5c1fa763 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -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') diff --git a/apps/web/tests/expected/desktop-locale/automatic.expected.md b/apps/web/tests/expected/desktop-locale/automatic.expected.md index 4e10073523..e72b731f31 100644 --- a/apps/web/tests/expected/desktop-locale/automatic.expected.md +++ b/apps/web/tests/expected/desktop-locale/automatic.expected.md @@ -22,8 +22,8 @@ - button "标准" - text: 性能与用量 选择性能与用量信息展示的详细程度 - button "详细" - - text: 开发者工具 显示用于调试和排查问题的工具与信息 - - switch "开发者工具" [checked] + - text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换 + - switch "代码工作工具" [checked] - text: 快捷键 - paragraph: 查看当前可用的快捷键和输入操作 - button "查看快捷键" diff --git a/apps/web/tests/expected/desktop-locale/selected.expected.md b/apps/web/tests/expected/desktop-locale/selected.expected.md index eadbe2289d..e94627b044 100644 --- a/apps/web/tests/expected/desktop-locale/selected.expected.md +++ b/apps/web/tests/expected/desktop-locale/selected.expected.md @@ -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" diff --git a/apps/web/tests/expected/plugin-config/official.expected.md b/apps/web/tests/expected/plugin-config/official.expected.md index 4e53ddd016..d408f284f3 100644 --- a/apps/web/tests/expected/plugin-config/official.expected.md +++ b/apps/web/tests/expected/plugin-config/official.expected.md @@ -24,8 +24,8 @@ - button "查看 Agent 循环": Agent 循环 - text: 控制 Agent 派发工具调用的方式。 - listitem: - - button "查看 Subagent": Subagent - - text: 设置 Subagent 的递归层级、数量和模型。 + - button "查看 子智能体": 子智能体 + - text: 设置子智能体的递归层级、数量和模型。 - listitem: - button "查看 网页搜索": 网页搜索 - text: 设置 DeepSeek 的搜索提供方。 diff --git a/apps/web/tests/expected/plugin-config/subagent.expected.md b/apps/web/tests/expected/plugin-config/subagent.expected.md index e1214b7fd7..1f324d7161 100644 --- a/apps/web/tests/expected/plugin-config/subagent.expected.md +++ b/apps/web/tests/expected/plugin-config/subagent.expected.md @@ -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] diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md index e9b345be5f..2f7b9bc832 100644 --- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md +++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md @@ -24,8 +24,8 @@ - button "查看 Agent 循环": Agent 循环 - text: 控制 Agent 派发工具调用的方式。 - listitem: - - button "查看 Subagent": Subagent - - text: 设置 Subagent 的递归层级、数量和模型。 + - button "查看 子智能体": 子智能体 + - text: 设置子智能体的递归层级、数量和模型。 - listitem: - button "查看 网页搜索": 网页搜索 - text: 设置 DeepSeek 的搜索提供方。 diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md index 978daa2f30..61c517953b 100644 --- a/apps/web/tests/expected/plugin-manager/manager.expected.md +++ b/apps/web/tests/expected/plugin-manager/manager.expected.md @@ -24,8 +24,8 @@ - button "查看 Agent 循环": Agent 循环 - text: 控制 Agent 派发工具调用的方式。 - listitem: - - button "查看 Subagent": Subagent - - text: 设置 Subagent 的递归层级、数量和模型。 + - button "查看 子智能体": 子智能体 + - text: 设置子智能体的递归层级、数量和模型。 - listitem: - button "查看 网页搜索": 网页搜索 - text: 设置 DeepSeek 的搜索提供方。 diff --git a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md index 2ad9903a2e..7bdc9cf4e3 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md @@ -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" diff --git a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md index 2ad9903a2e..7bdc9cf4e3 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md @@ -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" diff --git a/apps/web/tests/expected/settings-chrome/dialog.expected.md b/apps/web/tests/expected/settings-chrome/dialog.expected.md index a4a9349ffb..8056341afa 100644 --- a/apps/web/tests/expected/settings-chrome/dialog.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog.expected.md @@ -22,8 +22,8 @@ - button "标准" - text: 性能与用量 选择性能与用量信息展示的详细程度 - button "详细" - - text: 开发者工具 显示用于调试和排查问题的工具与信息 - - switch "开发者工具" + - text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换 + - switch "代码工作工具" - text: 快捷键 - paragraph: 查看当前可用的快捷键和输入操作 - button "查看快捷键" diff --git a/apps/web/tests/fixtures/time-context-every-step.patch.yml b/apps/web/tests/fixtures/time-context-every-step.patch.yml new file mode 100644 index 0000000000..22c6bd919f --- /dev/null +++ b/apps/web/tests/fixtures/time-context-every-step.patch.yml @@ -0,0 +1,4 @@ +# Browser-zone assertions need a fresh reading after the non-browser preparation turn. +- id: time-context + config: + refreshIntervalMs: 0 diff --git a/apps/web/tests/plugin-config.e2e.ts b/apps/web/tests/plugin-config.e2e.ts index 3a65d644da..9a140bcf46 100644 --- a/apps/web/tests/plugin-config.e2e.ts +++ b/apps/web/tests/plugin-config.e2e.ts @@ -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() diff --git a/apps/web/tests/schedule-after.e2e.ts b/apps/web/tests/schedule-after.e2e.ts index be0b066543..0a59e8a252 100644 --- a/apps/web/tests/schedule-after.e2e.ts +++ b/apps/web/tests/schedule-after.e2e.ts @@ -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', diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 82c4f7e90e..93e951bd77 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -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 } ``` diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index a04b23ea82..90a705a820 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -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 } ``` diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index bafab73675..5312354d60 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -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) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index faa7c6b3b6..6baa07d2d2 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -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) | diff --git a/docs/subsystems/approval.i18n.yaml b/docs/subsystems/approval.i18n.yaml index d11fac4c66..ebdb2ead96 100644 --- a/docs/subsystems/approval.i18n.yaml +++ b/docs/subsystems/approval.i18n.yaml @@ -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 diff --git a/docs/subsystems/approval.md b/docs/subsystems/approval.md index 656325aa44..5a2513775a 100644 --- a/docs/subsystems/approval.md +++ b/docs/subsystems/approval.md @@ -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 /** diff --git a/docs/subsystems/approval.zh.md b/docs/subsystems/approval.zh.md index a22aec2ef2..74aa7fbfc1 100644 --- a/docs/subsystems/approval.zh.md +++ b/docs/subsystems/approval.zh.md @@ -50,7 +50,7 @@ type ApprovalPolicy = 'ask' | 'never' ## 审批请求 -`ApprovalRequest` 以足够精确的方式标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而非渲染另一份可能漂移的副本。 +`ApprovalRequest` 以足够精确的方式标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而非渲染另一份可能漂移的副本。可选的 `displayReason` 向界面传递请求方提供的本地化字符串;`reason` 仍是审计文本。 ```ts type-equiv /** diff --git a/docs/subsystems/web-client.i18n.yaml b/docs/subsystems/web-client.i18n.yaml index f4673fc8b6..da20febdb7 100644 --- a/docs/subsystems/web-client.i18n.yaml +++ b/docs/subsystems/web-client.i18n.yaml @@ -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 diff --git a/docs/subsystems/web-client.md b/docs/subsystems/web-client.md index f07094d649..1f57008daf 100644 --- a/docs/subsystems/web-client.md +++ b/docs/subsystems/web-client.md @@ -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. diff --git a/docs/subsystems/web-client.zh.md b/docs/subsystems/web-client.zh.md index 5a0069abb4..9bb2420704 100644 --- a/docs/subsystems/web-client.zh.md +++ b/docs/subsystems/web-client.zh.md @@ -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。 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index ba5cf0f276..d92c8d583f 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -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": [ diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index b0b7f2944f..9c05b6f477 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -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": [ diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index 30722e5cf9..8e3015ad7c 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/README.i18n.yaml @@ -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 diff --git a/packages/client/README.zh.md b/packages/client/README.zh.md index d5c33dc99d..da9ee4d12b 100644 --- a/packages/client/README.zh.md +++ b/packages/client/README.zh.md @@ -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 清单标签页 | — | diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index 68a78586f7..7f08d21282 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-agent-preset/README.md b/packages/client/ui-agent-preset/README.md index 4cdf3e1ece..ca7bba393e 100644 --- a/packages/client/ui-agent-preset/README.md +++ b/packages/client/ui-agent-preset/README.md @@ -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. diff --git a/packages/client/ui-agent-preset/README.zh.md b/packages/client/ui-agent-preset/README.zh.md index fce38232f8..d2a3471f45 100644 --- a/packages/client/ui-agent-preset/README.zh.md +++ b/packages/client/ui-agent-preset/README.zh.md @@ -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 显示在标题旁。指南文案与示例归本包所有。 diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts index 586665fc85..30b3e79eac 100644 --- a/packages/client/ui-agent-preset/src/client/locales.ts +++ b/packages/client/ui-agent-preset/src/client/locales.ts @@ -67,8 +67,8 @@ export const en: Record = { 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 = { creatorDraft: '让 Agent 帮我创建预设模式', - enableDevToolsToSetDefault: '请先在通用设置中开启开发者工具,再设置默认值', - enableDevToolsToCreate: '请先在通用设置中开启开发者工具,再启动创造模式', + enableDevToolsToSetDefault: '请先在通用设置中开启代码工作工具,再设置默认值', + enableDevToolsToCreate: '请先在通用设置中开启代码工作工具,再启动创造模式', } // The resolution itself is the shared fold in `dsh-agent-preset-registry/display`, diff --git a/packages/client/ui-approval/README.i18n.yaml b/packages/client/ui-approval/README.i18n.yaml index efbfdf2cb6..7fef3ae2d8 100644 --- a/packages/client/ui-approval/README.i18n.yaml +++ b/packages/client/ui-approval/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-approval/README.md b/packages/client/ui-approval/README.md index ce2ccdbbec..4986683615 100644 --- a/packages/client/ui-approval/README.md +++ b/packages/client/ui-approval/README.md @@ -33,7 +33,7 @@ None; approval request and response rendering does not alter a model request. -- **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. diff --git a/packages/client/ui-approval/README.zh.md b/packages/client/ui-approval/README.zh.md index d7a94ae915..7015ba5f44 100644 --- a/packages/client/ui-approval/README.zh.md +++ b/packages/client/ui-approval/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" -- **面板只提供临时决定**——它支持仅本次允许和拒绝;持久权限策略仍由 Host 侧审批包拥有。 +- **面板只提供临时决定**——它支持仅本次允许和拒绝;持久权限策略仍由 Host 侧审批包拥有。请求方提供的本地化展示文案跟随界面语言,不改写审计原因,也不翻译模型生成的文本。 diff --git a/packages/client/ui-approval/src/client/ApprovalPanel.tsx b/packages/client/ui-approval/src/client/ApprovalPanel.tsx index 9dce13573c..bdfcba3f02 100644 --- a/packages/client/ui-approval/src/client/ApprovalPanel.tsx +++ b/packages/client/ui-approval/src/client/ApprovalPanel.tsx @@ -14,11 +14,13 @@ export function ApprovalPanel(props: ApprovalComposerProps) { const detail = approval.callId === undefined ? null : props.renderSlot('conversation.approval.detail', { callId: approval.callId }) - return + const reason = approval.displayReason === undefined ? approval.reason : props.resolveReason(approval.displayReason) + return } -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')} > -
{pending.reason ?? t('escalation', { toolName: pending.toolName })}
+
{reason ?? t('escalation', { toolName: pending.toolName })}
{detail !== null &&
{detail}
}
diff --git a/packages/client/ui-approval/src/client/contract/slots.ts b/packages/client/ui-approval/src/client/contract/slots.ts index b129b97d94..2558b40ea4 100644 --- a/packages/client/ui-approval/src/client/contract/slots.ts +++ b/packages/client/ui-approval/src/client/contract/slots.ts @@ -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 @@ -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() 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): string +} + /** Full props of the approval composer takeover. */ export type ApprovalComposerProps = PropsRuntime<'conversation.composer'> & PropsRenderSlots<'conversation.approval.detail'> & { matched: PendingApproval } & PropsLocale<'approval'> + & ApprovalInjected diff --git a/packages/client/ui-approval/src/client/index.ts b/packages/client/ui-approval/src/client/index.ts index acad0bc207..ca6d674e36 100644 --- a/packages/client/ui-approval/src/client/index.ts +++ b/packages/client/ui-approval/src/client/index.ts @@ -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() @@ -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) => ctx.locale.resolveText(reason), + }), children: { 'conversation.approval.detail': { kind: 'single', scope: 'session' }, }, diff --git a/packages/client/ui-approval/tests/ui-approval.client.spec.tsx b/packages/client/ui-approval/tests/ui-approval.client.spec.tsx index 6e18ace13c..e9edc316a5 100644 --- a/packages/client/ui-approval/tests/ui-approval.client.spec.tsx +++ b/packages/client/ui-approval/tests/ui-approval.client.spec.tsx @@ -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 { let registration: { options: { select(props: { pendingInteraction: PendingApproval | undefined }): PendingApproval | null + inject(): ApprovalInjected } component: unknown } | undefined @@ -82,6 +85,7 @@ async function setupPlugin(): Promise { ctx.provide('slots', { inject: injectSlot, register } as never) ctx.provide('locale', { register: vi.fn(() => disposeLocale), + resolveText: (reason: NonNullable) => 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) => 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() + expect(screen.getByText('English explanation')).toBeTruthy() + expect(screen.queryByText('audit reason')).toBeNull() + view.rerender( 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' }) diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 8b219abf38..26e8bd6b1b 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 7c7739fee9..f660ca562a 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -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. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 88ac5cb99f..84489d8b1c 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -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(资源释放)时移除注册。回调绑定留在输入模块内部。 diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml index d133a9b4f5..21b34b5eef 100644 --- a/packages/client/ui-deliverables/README.i18n.yaml +++ b/packages/client/ui-deliverables/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md index 7ddbd58bb6..b194a9d3b4 100644 --- a/packages/client/ui-deliverables/README.md +++ b/packages/client/ui-deliverables/README.md @@ -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 diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md index 609ba65f19..06ce26aebd 100644 --- a/packages/client/ui-deliverables/README.zh.md +++ b/packages/client/ui-deliverables/README.zh.md @@ -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 影响 diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index f550c9ce59..6ccf703b39 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index 7c07237925..37fb96bb7c 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -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({ diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml index 4abe9ccb45..9797b69beb 100644 --- a/packages/client/ui-schedule/README.i18n.yaml +++ b/packages/client/ui-schedule/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md index 9a431dba22..577e9e4ccc 100644 --- a/packages/client/ui-schedule/README.md +++ b/packages/client/ui-schedule/README.md @@ -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. diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md index 27b125e092..743d8a5525 100644 --- a/packages/client/ui-schedule/README.zh.md +++ b/packages/client/ui-schedule/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" 行在保存进行中、目录加载中、删除期间以及任务未运行时禁用。仅当本地草稿与已存储任务不同时才出现保存栏:显示“有未保存的修改”及“取消”和“保存修改”,保存期间“保存修改”变为“保存中…”。“取消”恢复已存储的值。仅当草稿没有未保存修改时,目录刷新才会成为显示值,因此未保存的草稿会保留到权威刷新之后。保存失败保留草稿并显示本地化提示;冲突后针对刷新后的记录重试。任务从目录消失时,“删除”会禁用,直到任务重新出现。更新成功后刷新权威目录。更新被拒绝或无法确认与目录读取失败相互区分:“重试”重新加载目录,不将该更新显示为失败。若更新本身无法确认,应在重试前检查任务。切换任务或关闭详情后才到达的响应不会替换当前视图,任何失败都不会回滚宿主已开始的写入。 -打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。旧回执没有提示文本时,绝不以当前指令代替。 +打开“任务运行记录”时才请求最新的 20 条已保存记录。“加载更早的记录”按追加顺序从新到旧增加一页,不受实际时间回拨影响。该视图中页签条就是整个页头:名称输入框和“下次运行”行只属于“规则”。每条“已保存的任务运行记录”以时钟图标和任务时区下的本地月份名与时钟字段开头(一次性记录使用浏览器时区),并在存在时显示实际保存的提示文本。超过两行的提示文本只显示前两行并提供“展开”,展开后可“收起”;面板宽度变化时,收起状态的提示文本会重新判断是否超过两行。旧回执没有提示文本时,绝不以当前指令代替。 最后一页加载完成后,非空且确认裁剪过的历史在详情面板左下角显示无分隔线的清理提示,信息图标可展开或收起当前宿主的保留规则。空历史、未确认裁剪的旧格式历史、尚未加载完的分页和读取失败均不显示清理提示。 diff --git a/packages/client/ui-schedule/src/client/DeliveryHistory.tsx b/packages/client/ui-schedule/src/client/DeliveryHistory.tsx index 89f71adc6c..2e6c1f3a36 100644 --- a/packages/client/ui-schedule/src/client/DeliveryHistory.tsx +++ b/packages/client/ui-schedule/src/client/DeliveryHistory.tsx @@ -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(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 <> +

{prompt}

+ {clamped && } + +} + /** * 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
- {record.prompt !== undefined &&

{record.prompt}

} + {record.prompt !== undefined && }
)} {page?.nextBefore !== undefined && failure === undefined &&