Merge branch 'master' into feat/schedule-reminder-framing
This commit is contained in:
commit
1f05deac2e
677 files changed
+27508
-5868
No files matched your search
+6
-6
@@ -21,8 +21,8 @@
|
||||
en: 774c3bf80a48ffbe
|
||||
zh: c513245789aa8efd
|
||||
/agent-note-right-sidebar-tab-types-and-navigation/decision/navigation-ctx-sidebarright:
|
||||
en: b32bad4c3b8c2cc8
|
||||
zh: 57ba7b8d3451e478
|
||||
en: c52363f57001b7ac
|
||||
zh: bbcbdbc038cbf316
|
||||
/agent-note-right-sidebar-tab-types-and-navigation/decision/addresses:
|
||||
en: eeb1e7e7478a2f2c
|
||||
zh: bddbc5bc78245f63
|
||||
@@ -30,8 +30,8 @@
|
||||
en: 6386bde2654f9a0a
|
||||
zh: 700439be6a01e162
|
||||
/agent-note-right-sidebar-tab-types-and-navigation/alternatives-considered:
|
||||
en: 91fbca006e8de5bc
|
||||
zh: f48d3ce1dd7b63bf
|
||||
en: c47eb4e2f3c307f9
|
||||
zh: fd4515b8be88506e
|
||||
/agent-note-right-sidebar-tab-types-and-navigation/consequences:
|
||||
en: 3cdb1159ee55d851
|
||||
zh: dcd068005e678a89
|
||||
@@ -39,5 +39,5 @@
|
||||
en: 49449a6c8465c098
|
||||
zh: 1f476cc1226aa9cd
|
||||
/agent-note-right-sidebar-tab-types-and-navigation/deferred:
|
||||
en: fa27b7684e38329b
|
||||
zh: d270e0cdcab73732
|
||||
en: 5b2416692028bf89
|
||||
zh: 65930102aa253bf2
|
||||
+4
-2
@@ -72,7 +72,7 @@ interface SidebarRightTabParamsMap {} // key: kind — a page type declar
|
||||
|
||||
`openResource` accepts the union of every declared resource shape and `openTab<K>` the shape declared for `K`; a body narrows `navigation.params` by the protocol or kind it knows it serves. Parameters belong to the resource type rather than to the viewer because a line number is a fact about a file location, not about the text preview, and any type that claims `file` addresses receives the same shape. Values must be JSON-serializable, and a record must be rebuildable from address and parameters alone, because undo, redo, reload, and HMR rebuild tabs after the opener is gone.
|
||||
|
||||
Beside the two opens, the face carries `close(tabId)`, `active()`, `isExpanded()`, `toggleExpanded()`, and four operational methods — `focus(tabId)`, `split(paneId?)` (returning the new pane, or `undefined` when the pane budget or the width rule forbids the split, recording nothing), `float(tabId, rect?)`, and `dock(paneId)` — each recording one history entry and a no-op on a missing target or one already in the requested state. There is no layout snapshot, no subscription, and no lookup by address: the face grants control over the layout, not a view of it. The seat publishes its binding — its session, its store's actions, and its surface — while mounted; a command on the public face acts on the mounted session and throws with no mounted session surface. A tab's own actions reach their session's own store instead: the slot runtime mints one store per session, the plugin adopts each as it is minted, and the controller routes by session id, so an action fired after the user switched sessions still lands, and does nothing for a session whose store was never minted.
|
||||
Beside the two opens, the face carries `close(tabId)`, `active()`, `isExpanded()`, `toggleExpanded()`, and four operational methods — `focus(tabId)`, `split(paneId?)` (returning the new pane, or `undefined` when the pane budget or the width rule forbids the split, recording nothing), `float(tabId, rect?)`, and `dock(paneId)` — each recording one history entry and a no-op on a missing target or one already in the requested state. There is no layout snapshot, no subscription, and no lookup by address: the face grants control over the layout, not a view of it. The plugin names the Session on screen — the selected Session while the Conversation fills the main column — from the Session selection and the main panel, before React renders either change, and publishes it as `mounted`; a command on the public face acts on that Session through its adopted store and throws when no Session is on screen or its store has not been minted. The seat reports only what it renders with: the room its docking kit measured and whether its frame width presents the panel fullscreen automatically. A tab's own actions reach their session's own store instead: the slot runtime mints one store per session, the plugin adopts each as it is minted, and the controller routes by session id, so an action fired after the user switched sessions still lands, and does nothing for a session whose store was never minted.
|
||||
|
||||
### Addresses
|
||||
|
||||
@@ -100,6 +100,8 @@ The conversation's `openFile(path, { line? })` — tool-row path links, produced
|
||||
|
||||
**Slot priorities to express an extension taking over a builtin, then registry-minted slot keys.** The first attempt had the overriding type register its body at a lower slot priority through an exported constant — a value import across dynamic plugins, and a second rule system (slot priority) standing in for the registry's. The second attempt had the registry mint a key per registration and return it from `register()`, which made registration a two-step dance whose ordering mattered. Letting the implementation declare its own `id` — required, unique, the same string it registers its seats under — needs no constant, no minting, and no ordering, and gives the registry the identity it needs to reject duplicates.
|
||||
|
||||
**A binding the seat publishes from its own effects.** The first form had the mounted seat publish its session, actions, and surface after each commit and release them when it left. React runs a commit's passive effects in tree order, and the frame renders the Conversation column before the right column, so in a commit that switched Sessions or returned from a global panel a Conversation component opening content from its effect ran while no seat was bound — after the departing seat's release and before the arriving seat's bind — and the open threw. The Session selection and the main panel change before React renders, so deriving the on-screen Session from them removes that ordering dependency. The seat still measures pane room, because only its rendered kit can.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A type is one static object plus one or two keyed seat registrations; its occurrence information is read through injected `useTabInfo()`. The framework grows no per-type API surface, and a type shipped from outside this repository imports only types from the Sidebar package.
|
||||
@@ -116,5 +118,5 @@ The conversation's `openFile(path, { line? })` — tool-row path links, produced
|
||||
## Deferred
|
||||
|
||||
- A navigation protocol beyond `sidebar://<kind>`: sub-routes within a page, naming an implementation, and the ecosystem-facing rules for other navigation schemes.
|
||||
- Opening into a session other than the one on screen from the public face, which acts on the mounted session only; a tab's own actions already act on their tab's session.
|
||||
- Opening into a session other than the one on screen from the public face, which acts on the on-screen Session only; a tab's own actions already act on their tab's session.
|
||||
- A localized message when an open fails from the conversation; the failure is currently the thrown error's text.
|
||||
+4
-2
@@ -72,7 +72,7 @@ interface SidebarRightTabParamsMap {} // key: kind — a page type declar
|
||||
|
||||
`openResource` 接受所有已声明资源形状的联合,`openTab<K>` 接受为 `K` 声明的形状;正文按自己所服务的协议或 kind 收窄 `navigation.params`。参数属于资源类型而非查看器,因为行号是关于文件位置的事实,不是关于文本预览的,任何认领 `file` 地址的类型收到同一形状。值必须可 JSON 序列化,一条记录必须只凭地址与参数就能重建,因为撤销、重做、刷新与 HMR 都在开启方已不在时重建 tab。
|
||||
|
||||
除两种打开外,该面还有 `close(tabId)`、`active()`、`isExpanded()`、`toggleExpanded()`,以及四个操作型方法——`focus(tabId)`、`split(paneId?)`(返回新格,预算或宽度规则不允许分栏时返回 `undefined` 且不记账)、`float(tabId, rect?)` 与 `dock(paneId)`——每个记一条历史,目标不存在或已在目标态时为 no-op。没有布局快照、没有订阅、没有按地址查找:该面给的是对布局的控制权,不是布局的视图。座位挂载期间发布其绑定——自己的会话、其 store 的 action 与其面;公开面上的命令作用于已挂载会话,没有已挂载会话面时抛错。tab 自己的动作则到达其会话自己的 store:slot 运行时每个会话铸一个 store,插件在铸出时逐个收养,控制器按会话 id 路由,因此用户切换会话之后触发的动作照样落地,而 store 从未铸出的会话什么也不做。
|
||||
除两种打开外,该面还有 `close(tabId)`、`active()`、`isExpanded()`、`toggleExpanded()`,以及四个操作型方法——`focus(tabId)`、`split(paneId?)`(返回新格,预算或宽度规则不允许分栏时返回 `undefined` 且不记账)、`float(tabId, rect?)` 与 `dock(paneId)`——每个记一条历史,目标不存在或已在目标态时为 no-op。没有布局快照、没有订阅、没有按地址查找:该面给的是对布局的控制权,不是布局的视图。插件依据会话选择与主面板确定屏上会话——Conversation 占据主栏时的选中会话——并在 React 渲染这两者的变化之前将其发布为 `mounted`;公开面上的命令经该会话被收养的 store 作用于它,没有会话在屏上或其 store 尚未铸出时抛错。座位只上报它渲染时依据的事实:停靠套件测得的空间,以及框架宽度是否让面板自动全屏。tab 自己的动作则到达其会话自己的 store:slot 运行时每个会话铸一个 store,插件在铸出时逐个收养,控制器按会话 id 路由,因此用户切换会话之后触发的动作照样落地,而 store 从未铸出的会话什么也不做。
|
||||
|
||||
### 地址
|
||||
|
||||
@@ -100,6 +100,8 @@ interface SidebarRightTabParamsMap {} // key: kind — a page type declar
|
||||
|
||||
**用 Slot 优先级表达 extension 接管 builtin,随后是注册表铸造的坑位键。** 第一次尝试让覆盖方经一个导出常量以更低的 Slot 优先级注册正文——这是动态插件间的值引用,也是拿第二套规则(Slot 优先级)替注册表的规则站台。第二次尝试让注册表为每次注册铸一个键并从 `register()` 返回,这把注册变成了两步且顺序敏感的舞步。让实现自己声明 `id`——必填、唯一、与它注册坑位所用的同一个串——既不需要常量,也不需要铸键与顺序,还给了注册表拒绝重复所需的身份。
|
||||
|
||||
**由座位在自己的 effect 里发布绑定。** 最初的形式由已挂载的座位在每次提交后发布自己的会话、action 与面,离开时释放。React 按树序运行一次提交的被动 effect,而框架先渲染 Conversation 列、后渲染右栏,于是在切换会话或从全局面板返回的那次提交里,从自己 effect 打开内容的 Conversation 组件运行时没有任何座位已绑定——在离开的座位释放之后、到来的座位绑定之前——打开随之抛错。会话选择与主面板都在 React 渲染之前变化,从它们推导屏上会话便消除了这一顺序依赖。分栏空间仍由座位测量,因为只有它渲染出的套件能测。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 一个类型 = 一个静态对象 + 一到两个 keyed 坑位注册;其实例信息通过注入的 `useTabInfo()` 读取。框架不长任何按类型的 API 面,仓外类型从 Sidebar 包只引类型。
|
||||
@@ -116,5 +118,5 @@ interface SidebarRightTabParamsMap {} // key: kind — a page type declar
|
||||
## Deferred
|
||||
|
||||
- `sidebar://<kind>` 之外的导航协议:页内子路由、点名实现、以及面向生态的其它导航 scheme 规则。
|
||||
- 从公开面往屏上会话之外的会话里打开;公开面只作用于已挂载的会话,而 tab 自己的动作已作用于其所在会话。
|
||||
- 从公开面往屏上会话之外的会话里打开;公开面只作用于屏上会话,而 tab 自己的动作已作用于其所在会话。
|
||||
- 从会话区打开失败时的本地化提示;目前是抛错文本本身。
|
||||
+43
@@ -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/implemented/architecture/2026-09-19-timed-user-question-two-settlements.md
|
||||
/agent-note-timed-user-questions-as-two-settlements:
|
||||
en: 5b6fa73158a11213
|
||||
zh: b55c32342d6e6692
|
||||
/agent-note-timed-user-questions-as-two-settlements/problem:
|
||||
en: f665127a9b5b8225
|
||||
zh: b19845f62310f3a2
|
||||
/agent-note-timed-user-questions-as-two-settlements/decision:
|
||||
en: ce869bf2eed04e81
|
||||
zh: 5b1fcf44c44c6137
|
||||
/agent-note-timed-user-questions-as-two-settlements/decision/modes-and-scope:
|
||||
en: adccc5dc758ad8f6
|
||||
zh: 50304d68db42e8b1
|
||||
/agent-note-timed-user-questions-as-two-settlements/decision/ownership:
|
||||
en: 0543678ad9d26fc5
|
||||
zh: aa6ba207e661c567
|
||||
/agent-note-timed-user-questions-as-two-settlements/decision/foreground-wait-and-claim-lifetime:
|
||||
en: 447713bbdf1ab4d8
|
||||
zh: 2757678ae47822aa
|
||||
/agent-note-timed-user-questions-as-two-settlements/decision/durable-state-and-late-answers:
|
||||
en: 93f3def953d10a2e
|
||||
zh: e1106340e0043254
|
||||
/agent-note-timed-user-questions-as-two-settlements/decision/card-state-and-delivery-races:
|
||||
en: 3dd7321bd37c51bf
|
||||
zh: 2d1f058732bd6d0f
|
||||
/agent-note-timed-user-questions-as-two-settlements/decision/local-countdown-and-panel-actions:
|
||||
en: 3a33f2583e30e32a
|
||||
zh: 0ad7e8d1e47dc510
|
||||
/agent-note-timed-user-questions-as-two-settlements/decision/reply-node-and-grouping:
|
||||
en: 10323b0e0340a40b
|
||||
zh: a17fc3a4d4d820a7
|
||||
/agent-note-timed-user-questions-as-two-settlements/alternatives-considered:
|
||||
en: bf61d0ec80c63bb4
|
||||
zh: d4ce4d4438ffb013
|
||||
/agent-note-timed-user-questions-as-two-settlements/verification:
|
||||
en: d912a73a5ff409eb
|
||||
zh: 7b626794773e2301
|
||||
/agent-note-timed-user-questions-as-two-settlements/consequences:
|
||||
en: 2bc99c99be8ca9da
|
||||
zh: 8f262ec8b9b0945e
|
||||
+25
-21
@@ -1,6 +1,6 @@
|
||||
# Agent Note: Timed user questions as two settlements
|
||||
|
||||
Status: proposed
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-19-timed-user-question-two-settlements.zh.md)
|
||||
|
||||
@@ -10,14 +10,14 @@ A question can block an agent even when useful work does not depend on the answe
|
||||
|
||||
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
|
||||
## Decision
|
||||
|
||||
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.
|
||||
Foreground answers use 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.
|
||||
- A positive integer timeout selects `askTimed()`; `-1` selects blocking `ask()`. A native 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.
|
||||
|
||||
@@ -46,27 +46,27 @@ Client countdown expiry rejects the waterfall with `ASK_TIMED_OUT`. Unattended H
|
||||
|
||||
### 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.
|
||||
The `userQuestions` projection reconstructs question state from existing Session events. For native calls, 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 PTC sub-call enters the projection when its `tool/ptc-dispatch` result is pending, since only `run_code` appears in the model request header.
|
||||
|
||||
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.
|
||||
A continued question accepts `answer(agent, callId, answer)`. The method returns `false` for an unknown or non-continued call, throws `REPLY_QUEUED` while another reply awaits admission, 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.
|
||||
The inbox message is the durable queued reply. A per-Session reservation covers the interval from queuing through claim and admission; claiming a restored inbox reply creates the reservation again. Admission as `user/message` settles the question, while discard or a Turn that ends without admission releases the reservation for another answer. 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.
|
||||
No new Session event type or wait-state log is needed. The message-source extension requires the repository's persistence-type acknowledgement. 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.
|
||||
`QuestionCards` shares one `PendingQuestion` between the live request and the continued projection, keyed by Session and `callId`. A forwarded request keeps its card through the claim's first frame even before the projection lists it. A blocking request without a call id has its own per-request card with a reload-unique key, so an old persisted draft cannot attach to another request. 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 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. An accepted RPC submission steers a reply toward the nearest step without settling the question, and the panel closes. A current tool wait must finish before that step can read the reply; if the steer is discarded, the question can be reopened and answered again. 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`.
|
||||
A tool-call-keyed card is removed when its call is absent from the active projection and no forwarded request 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.
|
||||
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. Once a late reply enters the Inbox, the tool row displays its submitted answers and “View answers” expands the read-only transcript while admission is pending. A settled call opens a read-only panel with recorded answers. Discarding the queued reply restores the answer action.
|
||||
|
||||
### Reply node and grouping
|
||||
|
||||
@@ -86,7 +86,7 @@ This decision adds no process-role field, independent-layout flag, user-message
|
||||
|
||||
**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.
|
||||
**Persist a whole-value wait/focus/edit stream or use Host focus leases.** Durable question status is derivable from tool and admitted user 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.
|
||||
|
||||
@@ -94,20 +94,24 @@ This decision adds no process-role field, independent-layout flag, user-message
|
||||
|
||||
**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.
|
||||
**A dedicated late-reply Session event beside the inbox message.** The inbox splice records the queued reply and the admitted user message records its delivery. A third event duplicates those facts and adds another persistence type.
|
||||
|
||||
## Acceptance criteria
|
||||
## Verification
|
||||
|
||||
- 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.
|
||||
- [Service tests](../../../../packages/interaction/user-questions/tests/user-questions.spec.ts) and [wait tests](../../../../packages/interaction/user-questions/tests/timed-wait.spec.ts) cover claimed and unclaimed waits, last-claim disconnect, cancellation, timeout error mapping, continued-only answers, and exact answer-id validation.
|
||||
- [Projection tests](../../../../packages/interaction/user-questions/tests/projection.spec.ts) distinguish legacy and timed schemas and cover native and PTC pending calls, other failures, resume repair, queued reply cancellation, and admitted settlement.
|
||||
- [Client listener tests](../../../../packages/client/ui-user-questions/tests/browser-plugin.client.spec.ts) and [composer tests](../../../../packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx) cover claim retention through Host acceptance, focus before handshake, indefinite waits, draft retention, RPC resubmission, and panel actions.
|
||||
- [Grouping tests](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts) and [browser replay](../../../../apps/web/tests/question-composer.e2e.ts) 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 round-trip, cancellation, plan-review intent, and Session-remount regressions remain covered by their owning tests. Live multi-Client delivery races and real Host restart remain integration coverage gaps beyond isolated lifecycle and projection tests.
|
||||
|
||||
## Risks
|
||||
## Consequences
|
||||
|
||||
The model can continue independent work while the question remains answerable. Business-owned timing and reply rendering reuse the existing transport and conversation APIs, at the cost of local rather than globally coordinated waiting and ordinary folded visibility.
|
||||
|
||||
- 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.
|
||||
- A queued late reply can be canceled before the agent admits it. The question stays answerable until an admitted `user/message` records the answer.
|
||||
- A PTC sub-call interrupted before its `tool/ptc-dispatch` result is recorded cannot be reconstructed from the parent `run_code` failure, so it cannot take a late answer after recovery.
|
||||
+24
-20
@@ -1,6 +1,6 @@
|
||||
# Agent Note: 计时用户提问的两次结算
|
||||
|
||||
Status: proposed
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-19-timed-user-question-two-settlements.md) | 中文
|
||||
|
||||
@@ -10,14 +10,14 @@ Status: proposed
|
||||
|
||||
前台工具结果与最终回答的生命周期不同。`user-questions/request` waterfall(瀑布式事件)只结算一次;返回 pending 后仍保留它,会产生一条活过调用方的第二回答路径。倒计时、聚焦和编辑的归属也不同于持久问题状态。
|
||||
|
||||
## Proposal
|
||||
## Decision
|
||||
|
||||
前台回答继续使用现有 Remote Event waterfall。超时结束该请求并返回 pending;迟到回答通过业务 RPC steer 一条持久用户消息。接手 stream 只控制前台等待,既不传递回答,也不替代 Remote Event。
|
||||
|
||||
### 模式与范围
|
||||
|
||||
- 默认 `mode: legacy` 保留阻塞式 schema 和 `ask()` 路径。显式 `mode: timed` 增加 `timeout`,省略时使用配置默认值 120 秒。
|
||||
- 正整数超时选择 `askTimed()`;`-1` 选择阻塞式 `ask()`。timed schema 发出的阻塞调用仍参与计时问题投影和答案查看,并不等同于 legacy 调用。
|
||||
- 正整数超时选择 `askTimed()`;`-1` 选择阻塞式 `ask()`。timed schema 发出的原生阻塞调用仍参与计时问题投影和答案查看,并不等同于 legacy 调用。
|
||||
- 计划评审保留 `ask()`、`plan-review` 意图及 `BAD_INTENT` 校验。本决策不把计划评审迁移到其他包。
|
||||
- 提供 Agent 时,人机交互要求它是运行时中确切的存活根实例。陈旧实例以 `CALLER_NOT_LIVE` 失败;被其他 agent 拥有的子实例以 `DELEGATED_CALLER` 失败。持久 Session 的血缘关系不妨碍恢复后的运行时根实例提问。
|
||||
|
||||
@@ -46,27 +46,27 @@ Client 倒计时到期以 `ASK_TIMED_OUT` 拒绝 waterfall。无人接手时,H
|
||||
|
||||
### 持久状态与迟到回答
|
||||
|
||||
`userQuestions` 投影从现有 Session 事件重建问题状态。它通过 `request/header` 记录的 `timeout` 参数识别 timed schema,不跟踪 legacy 调用。有效的 `tool/call` 打开问题。其 `tool/result` 在 pending 或 `TOOL_OUTCOME_UNKNOWN` 时将问题标为已继续,在有效回答批次时结算并保存答案,其他结果或失败则移除问题。
|
||||
`userQuestions` 投影从现有 Session 事件重建问题状态。对原生调用,它通过 `request/header` 记录的 `timeout` 参数识别 timed schema,不跟踪 legacy 调用。有效的 `tool/call` 打开问题。其 `tool/result` 在 pending 或 `TOOL_OUTCOME_UNKNOWN` 时将问题标为已继续,在有效回答批次时结算并保存答案,其他结果或失败则移除问题。PTC 子调用在 `tool/ptc-dispatch` 结果为 pending 时进入投影,因为模型请求头只列出 `run_code`。
|
||||
|
||||
已继续的问题接受 `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()` 方法体。
|
||||
已继续的问题接受 `answer(agent, callId, answer)`。未知或非已继续调用返回 `false`;另一条回复等待准入时抛出 `REPLY_QUEUED`;回答批次没有恰好包含每个问题一次时抛出 `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,也能显示迟到回答。无法使用的已记录回复文本以空答案批次结算。
|
||||
inbox 消息是持久的排队回复。逐 Session 预约覆盖从排队、领取到准入的期间;领取恢复后的 inbox 回复时会重建预约。回复作为 `user/message` 准入时问题结算;丢弃回复或 Turn 结束而未准入时释放预约,允许再次回答。已结算答案批次保留在投影中,让原工具行即使自身结果仍是 pending,也能显示迟到回答。无法使用的已记录回复文本以空答案批次结算。
|
||||
|
||||
无需新增 Session 事件类型或等待状态日志。消息来源扩展需要仓库的持久化类型变更确认。`dismissed` 为历史记录保留可读性,但没有生产者:隐藏面板不会伪造回复。
|
||||
无需新增 Session 事件类型或等待状态日志。消息来源扩展需要仓库的持久化类型变更确认。隐藏面板不会伪造回复。
|
||||
|
||||
### 卡片状态与投递竞争
|
||||
|
||||
`QuestionCards` 按 Session 和 `callId`,让实时请求与已继续投影共享一个 `PendingQuestion`。没有调用 id 的阻塞请求拥有独立的逐请求卡片。开放卡片通过 waterfall 提交;缺少该通道时禁用提交。已继续卡片使用迟到回答 RPC。
|
||||
`QuestionCards` 按 Session 和 `callId`,让实时请求与已继续投影共享一个 `PendingQuestion`。转发请求在投影列出问题之前,仍会让卡片存活至接手 stream 的首帧。没有调用 id 的阻塞请求拥有独立的逐请求卡片,并使用跨重载唯一的键,避免旧草稿附着到另一请求。开放卡片通过 waterfall 提交;缺少该通道时禁用提交。已继续卡片使用迟到回答 RPC。
|
||||
|
||||
waterfall 提交没有送达确认。Gateway 可能丢弃输掉结算竞争的结果,因此卡片保留草稿并保持忙碌,等待投影同步。若提交期间问题变为已继续,控件带重提提示重新可用,用户可以通过 RPC 提交保留的草稿。这不是自动重试,也不保证每次本地提交都能到达模型。
|
||||
waterfall 提交没有送达确认。Gateway 可能丢弃输掉结算竞争的结果,因此卡片保留草稿并保持忙碌,等待投影同步。若提交期间问题变为已继续,控件带重提提示重新可用,用户可以通过 RPC 提交保留的草稿。RPC 接受提交后会将回复作为 steer 送往最近一步,但不会立即结算问题,同时关闭面板。当前工具等待须先结束,该步才能读取回复;若 steer 被丢弃,可以重新打开问题并再次回答。这不是自动重试,也不保证每次本地提交都能到达模型。
|
||||
|
||||
按工具调用标识的卡片在调用不属于活动投影且没有 waterfall 时移除。移除会关闭卡片并清理草稿。本地提交和传输取消帧都不能单独决定问题已经回答。无调用标识的阻塞卡片随 waterfall 结束;关闭这种阻塞面板以 `ASK_CANCELLED` 拒绝请求。
|
||||
按工具调用标识的卡片在调用不属于活动投影且没有转发中的请求时移除。移除会关闭卡片并清理草稿。本地提交和传输取消帧都不能单独决定问题已经回答。无调用标识的阻塞卡片随 waterfall 结束;关闭这种阻塞面板以 `ASK_CANCELLED` 拒绝请求。
|
||||
|
||||
### 本地倒计时与面板操作
|
||||
|
||||
Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会暂停倒计时,失焦后从保留的剩余时间继续。接手握手前的聚焦在 deadline 到达时仍生效;握手前失焦不会制造计时器。首次编辑或「慢慢回答」冻结倒计时,该状态与浏览器本地草稿一起保存。
|
||||
|
||||
只有回答通道就绪、没有活动倒计时且卡片未锁定时才自动聚焦。隐藏计时调用的面板会保留接手和倒计时,工具行可以重新打开面板。已结算调用依据记录的问题与答案打开只读面板,不产生另一条回答通道。
|
||||
只有回答通道就绪、没有活动倒计时且卡片未锁定时才自动聚焦。隐藏计时调用的面板会保留接手和倒计时,工具行可以重新打开面板。迟到回复进入 Inbox 后,工具行显示已提交的答案;等待准入期间,「查看回答」展开只读的行内记录。已结算调用打开只读面板,显示记录的答案。排队回复被丢弃后,回答操作恢复可用。
|
||||
|
||||
### 回复节点与分组
|
||||
|
||||
@@ -86,7 +86,7 @@ Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会
|
||||
|
||||
**立即委托所有计时请求。** 向故意留空的回答方集合派发请求,浪费现有阻塞回答路径,还要求特殊处理 `NO_PROVIDER`。委托表示回答方不可用,不代表计时模式本身。
|
||||
|
||||
**持久化整值等待/聚焦/编辑流,或使用 Host 聚焦租约。** 持久问题状态可从工具与 inbox 事件推导。聚焦和编辑属于本地 UI 状态;记录它们或同步聚焦租约会增加 Host 状态,却不能让多个 Client 共享同一编辑会话。
|
||||
**持久化整值等待/聚焦/编辑流,或使用 Host 聚焦租约。** 持久问题状态可从工具事件与已准入的用户事件推导。聚焦和编辑属于本地 UI 状态;记录它们或同步聚焦租约会增加 Host 状态,却不能让多个 Client 共享同一编辑会话。
|
||||
|
||||
**在通用 `MessageItem` 内重新分类 JSON、借用 relay form,或使用默认 context 行。** 解析文本来识别业务消息混淆了来源与内容;relay form 表示其他 agent 的消息,通用 context 行则会暴露 JSON。专用来源、Definition 和渲染器拥有问题呈现。
|
||||
|
||||
@@ -94,20 +94,24 @@ Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会
|
||||
|
||||
**面板关闭时写仅入日志的关闭事件,或伪造放弃消息。** 仅入日志会让模型看不到用户决定,还增加另一套持久机制。伪造回复则把隐藏面板当成回答。因此关闭面板仍让计时问题保持可回答。
|
||||
|
||||
**在 inbox 消息之外增加专用迟到回复 Session 事件。** inbox 插入与接纳的用户消息已经记录回复。第三个事件重复同一事实,并增加另一种持久化类型。
|
||||
**在 inbox 消息之外增加专用迟到回复 Session 事件。** inbox 插入记录排队回复,准入的用户消息记录其交付。第三个事件重复这些事实,并增加另一种持久化类型。
|
||||
|
||||
## Acceptance criteria
|
||||
## 验证
|
||||
|
||||
- 服务测试覆盖有人与无人接手、最后接手方断开、取消、超时错误映射、仅接受已继续问题的回答,以及答案 id 的精确校验。
|
||||
- 投影测试区分 legacy 与 timed schema,并覆盖 pending、已回答、其他失败、恢复修复及 inbox/消息重复结算。
|
||||
- Client 测试覆盖接手保留到 Host 接受结果、握手前聚焦、无限期等待、草稿保留、RPC 重提及面板隐藏/重开。
|
||||
- 分组与浏览器回放覆盖保留两个投影但只渲染一次回复、普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。
|
||||
- legacy 问答往返、取消、计划评审意图及 Session 重新挂载保留既有行为。真实多 Client 投递竞争与真实 Host 重启仍是独立生命周期和投影测试之外的集成覆盖缺口。
|
||||
- [服务测试](../../../../packages/interaction/user-questions/tests/user-questions.spec.ts)与[等待测试](../../../../packages/interaction/user-questions/tests/timed-wait.spec.ts)覆盖有人与无人接手、最后接手方断开、取消、超时错误映射、仅接受已继续问题的回答,以及答案 id 的精确校验。
|
||||
- [投影测试](../../../../packages/interaction/user-questions/tests/projection.spec.ts)区分 legacy 与 timed schema,并覆盖原生及 PTC pending 调用、其他失败、恢复修复、排队回复取消和准入后结算。
|
||||
- [Client 监听器测试](../../../../packages/client/ui-user-questions/tests/browser-plugin.client.spec.ts)与[composer 测试](../../../../packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx)覆盖接手保留到 Host 接受结果、握手前聚焦、无限期等待、草稿保留、RPC 重提及面板操作。
|
||||
- [分组测试](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts)与[浏览器回放](../../../../apps/web/tests/question-composer.e2e.ts)覆盖保留两个投影但只渲染一次回复、普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。
|
||||
- legacy 问答往返、取消、计划评审意图及 Session 重新挂载的回归仍由各自测试覆盖。真实多 Client 投递竞争与真实 Host 重启仍是独立生命周期和投影测试之外的集成覆盖缺口。
|
||||
|
||||
## Risks
|
||||
## Consequences
|
||||
|
||||
模型可以继续独立工作,问题同时保持可回答。业务层拥有的计时与回复渲染复用现有传输和对话 API,代价是等待由本地而非全局协调,可见性遵循普通折叠。
|
||||
|
||||
- 「慢慢回答」只影响一个 Client。其他 Client 可以回答或让共享 waterfall 到期,本地聚焦不构成全局暂停。
|
||||
- 接手 stream 的终止性失败可能让前台问题失败,而不是产生 pending。不为该失败提供无人接手等待的 fallback。
|
||||
- Host 与 Client 时钟不必对齐,但 Client 系统时钟变化可能移动到期时间,传输延迟可能推迟本地倒计时的开始。
|
||||
- 重复投影仍保留在存储中。聚合在结构性分组重建时增加线性扫描;普通纯文本 stream 更新保留增量路径。这是算法复杂度界限,不是实测延迟结论。
|
||||
- 无人回答的已继续问题可以在重新打开 Session 后继续存在。关闭面板不写记录,可见性遵循普通折叠,而不是保证回复始终可见。
|
||||
- 排队的迟到回复可能在 agent 准入前被取消。只有已准入的 `user/message` 记录答案,问题在此之前保持可回答。
|
||||
- PTC 子调用若在记录 `tool/ptc-dispatch` 结果前中断,无法从父 `run_code` 的失败中重建,因此恢复后不能接受迟到回答。
|
||||
@@ -6,11 +6,11 @@
|
||||
en: 5e8c5673a0669d9c
|
||||
zh: c8442a6186288a2a
|
||||
/agent-note-a-seen-prompt-marker-keeps-its-own-tail-grace/problem:
|
||||
en: ac4fb08e3203cee9
|
||||
zh: 69fcfcb4f8fc0319
|
||||
en: 906f8707987ff36c
|
||||
zh: 40430ec1e03ee340
|
||||
/agent-note-a-seen-prompt-marker-keeps-its-own-tail-grace/decision:
|
||||
en: 653c74442decb65a
|
||||
zh: 46f01c45d48b846b
|
||||
en: a998f5c6abdd68eb
|
||||
zh: a7cdd13e1e4de6c6
|
||||
/agent-note-a-seen-prompt-marker-keeps-its-own-tail-grace/alternatives-considered:
|
||||
en: 21a9a19b76f42758
|
||||
zh: 30a8f5425b59900d
|
||||
@@ -18,5 +18,5 @@
|
||||
en: b58904921286a1db
|
||||
zh: a696c2954d86c04f
|
||||
/agent-note-a-seen-prompt-marker-keeps-its-own-tail-grace/testing:
|
||||
en: 78253191eea198fa
|
||||
zh: 32bdd6807ddc13a0
|
||||
en: c741ea002d239ae6
|
||||
zh: d68846526e63ce51
|
||||
@@ -6,11 +6,11 @@ English | [中文](2026-09-27-pwsh-prompt-tail-grace.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Windows sessions on the self-hosted lane settled one or two of seven sends on the controlled prompt and charged the silence tier for the rest (2026-09-25/26, issue #2487). The exact readiness evidence is the controlled prompt's printable tail after the OSC `133;D` marker, and the bound on waiting for it was `idleSilenceMs + handoffGraceMs`. A native Windows probe shows the render arrives as two pty chunks: the marker alone, then the five-byte `dsh> ` 2-28 ms later (56 renders), with nothing printable after any prompt and no terminal queries in the output. The marker is written by the shell's own prompt function and the tail by the same render, so a tail that arrives past the bound means the host stalled that delivery, not that the prompt is absent. The session settled `inferred_idle` anyway, and each such send cost the tool call roughly three seconds.
|
||||
Windows sessions on the self-hosted lane settled one or two of seven sends on the controlled prompt and charged the silence tier for the rest (2026-09-25/26, issue #2487). The exact readiness evidence is the controlled prompt's printable tail after the OSC `133;D` marker, and the bound on waiting for it was `idleSilenceMs + handoffGraceMs`. A native Windows probe shows the render arrives as two pty chunks: the marker alone, then the five-byte `dsh> ` 2-28 ms later (56 renders under Windows PowerShell 5.1, the same two-chunk render under pwsh 7.6.6), with nothing printable after any prompt and no terminal queries in the output. The marker is written by the shell's own prompt function and the tail by the same render, so a tail that arrives past the bound means the host stalled that delivery, not that the prompt is absent. The session settled `inferred_idle` anyway, and each such send cost the tool call roughly three seconds. The lane's own failures are not that state: the first master run carrying this field (run 36326153388, 2026-09-27 14:30Z) took the same 20.8 s as the run before it (36309006133), so its degraded sends settled at the plain bound with the tolerance inert; the [Windows-lane observation note](../testing/2026-09-27-observation-waits-on-observed-state.md) records the open investigation.
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh-terminal-bash` gains a validated `promptTailGraceMs` configuration field, default `0`. While a send has seen the prompt marker and its printable tail has not arrived, the silence bound becomes `idleSilenceMs + handoffGraceMs + promptTailGraceMs`; every other bound, wait reason, and code path is unchanged. The [persistent PTY readiness design](../feature/2026-07-16-persistent-pty-sessions.md) owns the tier ladder this bound extends. Zero reproduces the previous bound exactly, so an upgrade changes no deployment's behaviour. The persistent pwsh Loader-composition case sets 5000 ms, because the hosts whose stalls the field is for are exactly the ones that run it, and its assertion (at least six `stdin_read` settlements, no `inferred_idle`) then measures the controlled-prompt path instead of the host's delivery timing. `validateConfig` accepts zero for this field — the one numeric bound whose zero is a documented value — and rejects negative or fractional values.
|
||||
`dsh-terminal-bash` gains a validated `promptTailGraceMs` configuration field, default `0`. While a send has seen the prompt marker and its printable tail has not arrived, the silence bound becomes `idleSilenceMs + handoffGraceMs + promptTailGraceMs`; every other bound, wait reason, and code path is unchanged. The [persistent PTY readiness design](../feature/2026-07-16-persistent-pty-sessions.md) owns the tier ladder this bound extends. Zero reproduces the previous bound exactly, so an upgrade changes no deployment's behaviour. The persistent pwsh Loader-composition case keeps the default: the self-hosted lane's degraded sends settle at the plain bound with the tolerance present or absent, so a nonzero value there would only lengthen a never-arriving-tail fallback from 3.3 s to 8.3 s per send while reporting a genuinely late tail as a slow pass instead of a named failure. `validateConfig` accepts zero for this field — the one numeric bound whose zero is a documented value — and rejects negative or fractional values.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -28,4 +28,4 @@ A deployment whose console renderer stalls keeps those sends on the exact path b
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/terminal/terminal-bash/tests/session.spec.ts` pins every state with fake timers: with `promptTailGraceMs` set, a marker whose tail arrives after the plain bound stays pending and then settles `stdin_read`; a marker whose tail never arrives falls back to `inferred_idle` at the extended bound; a tail that arrived and was invalidated by later output keeps the plain bound; and with the field at zero the same delayed delivery settles `inferred_idle`. `packages/terminal/terminal-bash/tests/config.spec.ts` accepts zero and rejects negative or fractional values, and rejects a nonzero tolerance shorter than one `pollIntervalMs`. `packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts` carries the end-to-end evidence on native Windows: with every session's prompt tail withheld for four seconds the case passes with the configured tolerance — nine settles, eight `stdin_read` and the `exit` command's `session_exit`, in 33.4 s and 37.5 s — and fails with the tolerance patched to zero, where two sends settle `inferred_idle` with `promptSeen` true, `promptTextSeen` false, an empty tail and `idleFor` 3312-3316 ms.
|
||||
`packages/terminal/terminal-bash/tests/session.spec.ts` pins every state with fake timers: with `promptTailGraceMs` set, a marker whose tail arrives after the plain bound stays pending and then settles `stdin_read`; a marker whose tail never arrives falls back to `inferred_idle` at the extended bound; a tail that arrived and was invalidated by later output keeps the plain bound; and with the field at zero the same delayed delivery settles `inferred_idle`. `packages/terminal/terminal-bash/tests/config.spec.ts` accepts zero and rejects negative or fractional values, and rejects a nonzero tolerance shorter than one `pollIntervalMs`. The end-to-end evidence on native Windows came from a guest-patched copy of `packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts` (Windows PowerShell 5.1, 2026-09-27): with every session's prompt tail withheld for four seconds and the tolerance at 5000 ms the case passed — nine settles, eight `stdin_read` and the `exit` command's `session_exit`, in 33.4 s and 37.5 s — and with the tolerance at zero two sends settled `inferred_idle` with `promptSeen` true, `promptTextSeen` false, an empty tail and `idleFor` 3312-3316 ms. The committed case runs with the default and prints a per-send readiness timeline on failure, so that state is named when it occurs rather than inferred from wall time.
|
||||
@@ -6,11 +6,11 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
自托管 lane 上的 Windows 会话在七次 send 中只有一到两次按受控提示符结算,其余都要支付静默层(2026-09-25/26,issue #2487)。精确的就绪证据是 OSC `133;D` 标记之后受控提示符的可打印尾部,而等待它的上界是 `idleSilenceMs + handoffGraceMs`。原生 Windows 探测表明该渲染以两个 pty 分块到达:先是标记本身,2–28 毫秒后是 5 字节的 `dsh> `(56 次渲染),任何提示符之后都没有可打印文本,输出中也没有终端查询。标记由 shell 自己的 prompt 函数写出、尾部由同一次渲染写出,因此尾部晚于上界到达意味着宿主机拖慢了这次投递,而不是提示符缺失。会话此前仍会按 `inferred_idle` 结算,每次这样的 send 让工具调用多付约三秒。
|
||||
自托管 lane 上的 Windows 会话在七次 send 中只有一到两次按受控提示符结算,其余都要支付静默层(2026-09-25/26,issue #2487)。精确的就绪证据是 OSC `133;D` 标记之后受控提示符的可打印尾部,而等待它的上界是 `idleSilenceMs + handoffGraceMs`。原生 Windows 探测表明该渲染以两个 pty 分块到达:先是标记本身,2–28 毫秒后是 5 字节的 `dsh> `(Windows PowerShell 5.1 下 56 次渲染,pwsh 7.6.6 下同样是两分块渲染),任何提示符之后都没有可打印文本,输出中也没有终端查询。标记由 shell 自己的 prompt 函数写出、尾部由同一次渲染写出,因此尾部晚于上界到达意味着宿主机拖慢了这次投递,而不是提示符缺失。会话此前仍会按 `inferred_idle` 结算,每次这样的 send 让工具调用多付约三秒。该 lane 自身的失败不是这种状态:首个带着该字段的 master run(run 36326153388,2026-09-27 14:30Z)与其前一个 run(36309006133)同样耗时 20.8 秒,因此其退化的 send 是在宽限不起作用的情况下按普通上界结算的;[Windows lane 观测笔记](../testing/2026-09-27-observation-waits-on-observed-state.zh.md)记录了这项仍未关闭的调查。
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh-terminal-bash` 新增一个经过校验的 `promptTailGraceMs` 配置字段,默认 `0`。当某次 send 已看到提示符标记而其可打印尾部尚未到达时,静默上界变为 `idleSilenceMs + handoffGraceMs + promptTailGraceMs`;其他上界、等待原因与代码路径均不变。该上界所扩展的层级阶梯由[持久 PTY 就绪设计](../feature/2026-07-16-persistent-pty-sessions.zh.md)拥有。零精确复现此前的上界,因此升级不改变任何部署的行为。持久 pwsh 的 Loader 组合用例设置 5000 毫秒,因为需要该字段的宿主机正是运行该用例的那些,而其断言(至少六次 `stdin_read` 结算、不含 `inferred_idle`)由此度量受控提示符路径,而不是宿主机的投递时序。`validateConfig` 允许该字段取零——这是唯一一个零为文档化取值的数值上界——并拒绝负数与小数。
|
||||
`dsh-terminal-bash` 新增一个经过校验的 `promptTailGraceMs` 配置字段,默认 `0`。当某次 send 已看到提示符标记而其可打印尾部尚未到达时,静默上界变为 `idleSilenceMs + handoffGraceMs + promptTailGraceMs`;其他上界、等待原因与代码路径均不变。该上界所扩展的层级阶梯由[持久 PTY 就绪设计](../feature/2026-07-16-persistent-pty-sessions.zh.md)拥有。零精确复现此前的上界,因此升级不改变任何部署的行为。持久 pwsh 的 Loader 组合用例保持默认值:自托管 lane 上退化的 send 无论有没有该宽限都按普通上界结算,因此在那里设置非零值只会把尾部永不到达时的回退从每次 send 3.3 秒拉长到 8.3 秒,并把真正迟到的尾部报告成一次缓慢的通过,而不是一次点名状态的失败。`validateConfig` 允许该字段取零——这是唯一一个零为文档化取值的数值上界——并拒绝负数与小数。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -28,4 +28,4 @@ Status: implemented
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/terminal/terminal-bash/tests/session.spec.ts` 用假定时器固定每一种状态:设置 `promptTailGraceMs` 时,尾部晚于普通上界到达的标记保持挂起,随后按 `stdin_read` 结算;尾部始终不到时在扩展上界处回落到 `inferred_idle`;尾部已到达但被后续输出失效时保持普通上界;字段为零时,同样的延迟投递按 `inferred_idle` 结算。`packages/terminal/terminal-bash/tests/config.spec.ts` 接受零、拒绝负数或小数,并拒绝非零但短于一个 `pollIntervalMs` 的宽限。`packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts` 承载原生 Windows 上的端到端证据:把每个 session 的提示符尾部扣留四秒时,配置了该宽限后用例通过——九次结算、八次 `stdin_read` 加 `exit` 命令的 `session_exit`,耗时 33.4 秒与 37.5 秒;把该宽限改为零后同一停摆失败,其中两次 send 以 `inferred_idle` 结算,`promptSeen` 为 true、`promptTextSeen` 为 false、尾部为空且 `idleFor` 为 3312–3316 毫秒。
|
||||
`packages/terminal/terminal-bash/tests/session.spec.ts` 用假定时器固定每一种状态:设置 `promptTailGraceMs` 时,尾部晚于普通上界到达的标记保持挂起,随后按 `stdin_read` 结算;尾部始终不到时在扩展上界处回落到 `inferred_idle`;尾部已到达但被后续输出失效时保持普通上界;字段为零时,同样的延迟投递按 `inferred_idle` 结算。`packages/terminal/terminal-bash/tests/config.spec.ts` 接受零、拒绝负数或小数,并拒绝非零但短于一个 `pollIntervalMs` 的宽限。原生 Windows 上的端到端证据来自 guest 内打过补丁的 `packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts` 副本(Windows PowerShell 5.1,2026-09-27):把每个 session 的提示符尾部扣留四秒且宽限设为 5000 毫秒时用例通过——九次结算、八次 `stdin_read` 加 `exit` 命令的 `session_exit`,耗时 33.4 秒与 37.5 秒;宽限为零时两次 send 以 `inferred_idle` 结算,`promptSeen` 为 true、`promptTextSeen` 为 false、尾部为空且 `idleFor` 为 3312–3316 毫秒。提交的用例以默认值运行,并在失败时打印逐 send 的就绪时间线,因此该状态一旦出现就会被点名,而不是从墙钟时间推断。
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 4fb1c27a631b32d3
|
||||
zh: 9dbe9a4b9aadfca9
|
||||
/agent-note-serial-cross-platform-ci-reference/decision:
|
||||
en: 7c58d88d4c002f19
|
||||
zh: a3b961be49f2a96b
|
||||
en: 6c682d0b7b2a03ca
|
||||
zh: b75950853f45cccf
|
||||
/agent-note-serial-cross-platform-ci-reference/alternatives-considered:
|
||||
en: 3610152c5c4e93af
|
||||
zh: f4c09d327716f750
|
||||
|
||||
@@ -24,7 +24,7 @@ Platform ownership remains explicit inside that complete aggregate. `terminal-ba
|
||||
|
||||
The macOS reference runs the ordinary Vitest project in forked processes. Node 24 on macOS arm64 has aborted in its CJS lexer from a worker thread; the process boundary contains that external runtime failure without removing any test from the aggregate, while Linux and Windows retain the lower-overhead thread pool. Repository-owned races are fixed at their observation boundaries: dev bundle polling stages each candidate table, graph, and watch-baseline map before publishing a rescan, and a missing bundle remains dirty until a successful content hash. PTY readiness retains a prompt candidate while polling checks foreground ownership; the ordinary silence bound covers inherited markers from interactive children. Real PTY fixtures assemble synchronization tokens at runtime so the interactive shell's input echo cannot satisfy a child-readiness wait. The live-link package-manager e2e preserves the workflow-prepared Corepack home and pnpm metadata/store caches while isolating the other managers' mutable caches, so it does not discard reusable package-manager state before the install.
|
||||
|
||||
The standalone [Sandbox](../../../../.github/workflows/sandbox.yml) workflow belongs to the reference side of the same split. Its bwrap, Landlock x64/arm64, and Seatbelt real-kernel matrix runs only after a push to `master`. Those four jobs are diagnostic: they are not branch-protection requirements and do not feed `all checks passed` across workflow files. A fifth job, `unit-darwin`, runs the complete unit suite on `macos-latest` under its own budget, so the darwin-only unit expectations keep a platform oracle without holding the Seatbelt leg to the unit suite's duration. Pull-request CI still checks sandbox source through its ordinary unit and coverage inventory; the host-kernel and packed-install proofs report after merge.
|
||||
The standalone [Sandbox](../../../../.github/workflows/sandbox.yml) workflow belongs to the reference side of the same split. Its bwrap, Landlock x64/arm64, and Seatbelt real-kernel matrix runs only after a push to `master`. Those four jobs are diagnostic: they are not branch-protection requirements and do not feed `all checks passed` across workflow files. A fifth job, `unit-darwin`, runs the complete unit suite on `macos-latest` under its own budget, so the darwin-only unit expectations keep a platform oracle without holding the Seatbelt leg to the unit suite's duration. That job grants `DSH_COVERAGE_TEST_TIMEOUT_MS=90000`, the coverage and serial lanes' key and value, and runs the unit inventory through the `ci-unit` aggregate that consumes it, so cases that inherit Vitest's defaults keep those lanes' budget on the shared macOS runner. Pull-request CI still checks sandbox source through its ordinary unit and coverage inventory; the host-kernel and packed-install proofs report after merge.
|
||||
|
||||
Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. The ci-master and Sandbox workflows keep their cross-platform references on master pushes. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value.
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ Status: implemented
|
||||
|
||||
macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上的 Node 24 曾在工作线程中执行 CJS 词法分析器时异常终止;进程边界能够隔离这一外部运行时故障,且无需从聚合流程中删除任何测试,而 Linux 与 Windows 仍使用开销更低的线程池。仓库自身引入的竞态均在相应的观测边界修复:开发构建产物的轮询逻辑每次发布重新扫描结果前,都会先暂存候选表、候选图和候选监视基线映射;构建产物缺失后会一直保持脏状态,直到成功计算内容哈希。PTY 就绪检测会在轮询检查前台进程组归属期间保留提示符候选项;常规静默时限也涵盖从交互式子进程继承而来的标记。真实 PTY fixture 会在运行时拼接同步标记,使就绪等待逻辑不会把交互式 shell 的输入回显误判为子进程已就绪。实时链接场景下的包管理器 e2e 会保留由工作流预先准备的 Corepack 主目录、pnpm 元数据缓存和 store 缓存,同时隔离其他包管理器的可变缓存,因此不会在安装前丢弃可复用的包管理器状态。
|
||||
|
||||
独立的 [Sandbox](../../../../.github/workflows/sandbox.yml) 工作流属于同一职责划分中的参考侧。其 bwrap、Landlock x64/arm64 与 Seatbelt 真实内核矩阵只在向 `master` 推送后运行。这四个作业仅用于诊断:它们既不是分支保护的必需项,也不会跨工作流计入 `all checks passed`。第五个作业 `unit-darwin` 在 `macos-latest` 上以自己的预算运行完整单元测试套件,让只在 darwin 上生效的单元测试预期保有平台判据,同时不让 Seatbelt 作业受制于单元测试套件的时长。拉取请求 CI 仍通过常规的单元测试与覆盖率清单检查沙箱源码;宿主内核与 packed-install 验证在合并后报告结果。
|
||||
独立的 [Sandbox](../../../../.github/workflows/sandbox.yml) 工作流属于同一职责划分中的参考侧。其 bwrap、Landlock x64/arm64 与 Seatbelt 真实内核矩阵只在向 `master` 推送后运行。这四个作业仅用于诊断:它们既不是分支保护的必需项,也不会跨工作流计入 `all checks passed`。第五个作业 `unit-darwin` 在 `macos-latest` 上以自己的预算运行完整单元测试套件,让只在 darwin 上生效的单元测试预期保有平台判据,同时不让 Seatbelt 作业受制于单元测试套件的时长。该作业授予与覆盖率、串行通道相同键值的 `DSH_COVERAGE_TEST_TIMEOUT_MS=90000`,并通过消费该变量的 `ci-unit` 聚合运行单元测试清单,因此继承 Vitest 默认值的用例在共享的 macOS 运行器上保有这些通道的预算。拉取请求 CI 仍通过常规的单元测试与覆盖率清单检查沙箱源码;宿主内核与 packed-install 验证在合并后报告结果。
|
||||
|
||||
master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。ci-master 与 Sandbox 工作流把跨平台参考流程保留在 master 推送上。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。
|
||||
|
||||
|
||||
+4
-4
@@ -9,8 +9,8 @@
|
||||
en: 270b015afc8de8ac
|
||||
zh: 65aeb1e270d19a27
|
||||
/agent-note-windows-lane-observations-wait-on-the-state-they-read/decision:
|
||||
en: 479cb25421102304
|
||||
zh: 9e23396728001e2c
|
||||
en: 55a0bcab15f38620
|
||||
zh: 677545442a19c7dc
|
||||
/agent-note-windows-lane-observations-wait-on-the-state-they-read/alternatives-considered:
|
||||
en: 00e59c561085e081
|
||||
zh: 4f6ac4dfc3259f58
|
||||
@@ -18,5 +18,5 @@
|
||||
en: 46517270dd3b22b1
|
||||
zh: 94e300066237cece
|
||||
/agent-note-windows-lane-observations-wait-on-the-state-they-read/deferred:
|
||||
en: 18dc81f98375fb04
|
||||
zh: e30413cd3d201b80
|
||||
en: 8e182739fdf90baf
|
||||
zh: 5f93f7b7d9518764
|
||||
@@ -16,7 +16,7 @@ Two self-hosted Windows lane cases observed state through a wall-clock window in
|
||||
|
||||
`observingChild` stays in flight until the test has read the run record, so the read cannot race the run's end, and its bounded wait starts at the mocked launcher's hand-over instead of at test start: the recorded run is one atomic write that follows the launch, so the bound only has to cover that write. `RECORD_WAIT_MS` (2 s) is deliberately below the case budget, so a record that never appears reports as the assertion that names it rather than as a runner timeout. Both the repair-install case and the single-run case hand the child over through the same `spawned` hook.
|
||||
|
||||
The loader-composition settle-reason assertion carries `JSON.stringify(settleReasons)` as its failure message. The threshold and the assertions are unchanged.
|
||||
The loader-composition settle-reason assertions carry the recorded reasons and a per-send readiness timeline in their failure message: every decoded pty chunk with the sanitizer's marker verdict and prompt tail, every foreground poll result, every input write, each send's settle reason and elapsed time, the session's prompt evidence replayed from those chunks, and the host, shell, PSReadLine, and console-host versions. `onTestFailed` prints the same timeline when the runner's budget ends the case before the assertions. The threshold and the assertions are unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -34,4 +34,6 @@ Both plugin-manager record cases now observe the record while the run is in flig
|
||||
|
||||
## Deferred
|
||||
|
||||
The loader-composition failure of 2026-09-25/26 is an environment-sensitive readiness window, not a product logic error, and the product now owns a documented tolerance for it: the [prompt tail grace decision](../bug-fix/2026-09-27-pwsh-prompt-tail-grace.md). On Windows `isStdinWaiting` returns false and `foregroundPgid` returns the shell pid, so `stdin_read` can only come from the controlled-prompt tail. A native Windows probe (Windows 11 ARM64 guest, 8 vCPU, with the readiness probe enabled) shows the fast path is deterministic when delivery is normal: every prompt render is the OSC marker alone in one pty chunk followed by a five-byte `dsh> ` chunk 2-28 ms later (56 renders), nothing printable follows any prompt, the output carries no terminal queries, and 21 runs (baseline, six CPU burners, emulated x64, four concurrent pty-heavy instances, and Windows PowerShell 5.1 as the shell) settled all seven sends on the prompt, six `stdin_read` plus the `exit` command's `session_exit`. Withholding each session's prompt tail for four seconds reproduces the reported failure exactly: `expected 4 to be greater than or equal to 6` with `["inferred_idle","stdin_read","stdin_read","stdin_read","stdin_read","session_exit","inferred_idle"]`, the degraded settles carrying `promptSeen` true, `promptTextSeen` false, an empty tail and `idleFor` 3301/3313 ms (exactly the configured bound), 22.1/21.9 s against 17.5/20.9 s in CI, and every output assertion still passing. The CI trigger itself was not reproduced locally, so this case keeps its assertion and supplies `promptTailGraceMs` in its composition: it measures the controlled-prompt path rather than the host's delivery timing, and it still fails when no prompt marker appears at all.
|
||||
The loader-composition failure on the self-hosted Windows lane (2026-09-25..27, issue #2487) remains open, and its mechanism is not a stalled prompt tail. The master run before the [prompt tail grace](../bug-fix/2026-09-27-pwsh-prompt-tail-grace.md) landed (run 36309006133, 2026-09-27 09:19Z) and the first run carrying it with `promptTailGraceMs: 5000` in this composition (run 36326153388, 14:30Z, reasons `["stdin_read","inferred_idle","inferred_idle","inferred_idle","inferred_idle","session_exit","inferred_idle"]`) both took 20.8 s, so the degraded sends settled at the plain `idleSilenceMs + handoffGraceMs` bound: either no marker was seen, or a marker was seen and its tail was invalidated by later printable text — the two states the tolerance does not cover. The foreground comparison is not a candidate on Windows: `WindowsProcessInspector.foregroundPgid` returns the shell pid unconditionally and `isStdinWaiting` returns false, so `shellPgid` always matches and `stdin_read` can only come from the controlled-prompt tail. The composition therefore keeps the field at its product default of `0`, and the failure message's timeline names the state directly the next time the lane fails; the 2026-09-27 12:51Z run (36320394583) instead hit the 120 s case budget before any assertion, which the same print covers.
|
||||
|
||||
Native evidence with pwsh 7.6.6 and PSReadLine 2.4.5 on Windows 11 25H2 (conhost 10.0.26100.1; the probes the tail-grace note cites ran under Windows PowerShell 5.1, the guest's only preinstalled shell): the case settles every send on the prompt at baseline, under eight CPU burners plus disk churn, under emulated x64 Node, under `vitest run --coverage --maxWorkers=1`, and with a 12-row viewport that scrolls from the second command, each prompt arriving as one in-order marker chunk followed by the five-byte tail. With an 8-row, 60-column viewport ConPTY re-emits the stored `133;D` mark inside repaint frames — five markers in one send, one followed by repainted row text (`\x07\e[Hdsh> Write-Output …`) — which leaves a tail that is not a prefix of the controlled prompt; on that console host the real prompt's marker still arrived last and the send settled `stdin_read`. That repaint path is the leading hypothesis for the lane, whose Windows build and console-host version are unknown (the job logs carry neither and the pool has no dispatchable job); the failure message records `os.release()`, `os.version()`, the pwsh and PSReadLine versions, and `conhost.exe`'s file version, and `dsh-subprocess-local` spawns node-pty without `useConptyDll`, so the system console host is the one in play. The pid `0` those probes also exposed on Windows is a separate defect, tracked by issue #5297.
|
||||
+4
-2
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
`observingChild` 会保持该 run 在进行中,直到测试读到 run 记录,因此读取不可能与 run 结束竞争;其有界等待从被 mock 的启动器交接处开始,而不是从用例开始处开始:被记录的 run 只是紧随启动之后的一次原子写入,因此等待上界只需覆盖这次写入。`RECORD_WAIT_MS`(2 秒)刻意低于用例预算,于是一条始终没有出现的记录会以指名它的断言报告,而不是以 runner 超时报告。修复安装用例与单 run 用例都通过同一个 `spawned` 钩子交接子进程。
|
||||
|
||||
loader-composition 的结算层级断言把 `JSON.stringify(settleReasons)` 作为失败信息。阈值与断言本身不变。
|
||||
loader-composition 的结算层级断言把记录到的结算原因与一份逐 send 的就绪时间线放进失败信息:每个解码后的 pty 分块及 sanitizer 对它的标记判定与提示符尾部、每次前台轮询的结果、每次输入写入、每次 send 的结算原因与耗时、由这些分块回放出的会话提示符证据,以及宿主机、shell、PSReadLine 与控制台宿主的版本。当 runner 预算在断言之前结束用例时,`onTestFailed` 打印同一份时间线。阈值与断言本身不变。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -34,4 +34,6 @@ loader-composition 的结算层级断言把 `JSON.stringify(settleReasons)` 作
|
||||
|
||||
## Deferred
|
||||
|
||||
loader-composition 于 2026-09-25/26 的失败是一个对投递时序敏感的就绪窗口,而不是 product 逻辑错误;product 现在为它拥有一个文档化的容忍度:[提示符尾部宽限决策](../bug-fix/2026-09-27-pwsh-prompt-tail-grace.zh.md)。在 Windows 上 `isStdinWaiting` 返回 false、`foregroundPgid` 返回 shell 自身的 pid,因此 `stdin_read` 只能来自受控提示符尾部。原生 Windows 探测(Windows 11 ARM64 guest,8 vCPU,启用就绪探测)表明只要投递正常,快路径就是确定的:每次提示符渲染都是 OSC 标记独占一个 pty 分块,紧随其后的下一个分块是 5 字节的 `dsh> `,间隔 2–28 毫秒(56 次渲染),任何提示符之后都没有可打印文本,输出中也没有终端查询;21 次运行(基线、6 个 CPU burner、模拟 x64、4 个并发 pty 实例、以及以 Windows PowerShell 5.1 作为 shell)全部七次 send 都按提示符结算——六次 `stdin_read` 加 `exit` 命令的 `session_exit`。把每个 session 的提示符尾部扣留 4 秒可以精确复现上报的失败:`expected 4 to be greater than or equal to 6`,`settleReasons` 为 `["inferred_idle","stdin_read","stdin_read","stdin_read","stdin_read","session_exit","inferred_idle"]`,退化的结算带着 `promptSeen` 为 true、`promptTextSeen` 为 false、空尾部与 `idleFor` 3301/3313 毫秒(正是配置的上界),耗时 22.1/21.9 秒对照 CI 的 17.5/20.9 秒,且所有输出断言仍然通过。CI 的触发条件本身未能在本地复现,因此该用例保留其断言并在组合中提供 `promptTailGraceMs`:它度量受控提示符路径而不是宿主机的投递时序,且在完全没有提示符标记时仍然失败。
|
||||
自托管 Windows lane 上 loader-composition 的失败(2026-09-25..27,issue #2487)仍未关闭,其机制不是提示符尾部停摆。[提示符尾部宽限](../bug-fix/2026-09-27-pwsh-prompt-tail-grace.zh.md)合入前的 master run(run 36309006133,2026-09-27 09:19Z)与首个带着该字段、且本组合设置了 `promptTailGraceMs: 5000` 的 run(run 36326153388,14:30Z,结算原因为 `["stdin_read","inferred_idle","inferred_idle","inferred_idle","inferred_idle","session_exit","inferred_idle"]`)都耗时 20.8 秒,因此退化的 send 是按普通的 `idleSilenceMs + handoffGraceMs` 上界结算的:要么根本没看到标记,要么看到了标记但其尾部被随后的可打印文本失效——这正是该宽限不覆盖的两种状态。前台比较在 Windows 上不是候选:`WindowsProcessInspector.foregroundPgid` 无条件返回 shell pid、`isStdinWaiting` 返回 false,因此 `shellPgid` 总是匹配,`stdin_read` 只能来自受控提示符尾部。于是该组合把字段保持在 product 默认值 `0`,下一次该 lane 失败时,失败信息里的时间线会直接点名所处的状态;2026-09-27 12:51Z 的 run(36320394583)则是在任何断言之前就撞上 120 秒的用例预算,同一份打印也覆盖这条路径。
|
||||
|
||||
在 Windows 11 25H2(conhost 10.0.26100.1)上以 pwsh 7.6.6 与 PSReadLine 2.4.5 取得的原生证据(尾部宽限笔记所引用的探测运行在 Windows PowerShell 5.1 之下,那是 guest 唯一预装的 shell):基线、8 个 CPU burner 加磁盘反复读写、模拟 x64 Node、`vitest run --coverage --maxWorkers=1`、以及从第二条命令起就滚动的 12 行视口下,用例的每次 send 都按提示符结算,每个提示符都是一个按序到达的标记分块紧跟 5 字节尾部。在 8 行 60 列的视口下,ConPTY 会在重绘帧内重新发出已存储的 `133;D` 标记——一次 send 内出现五个标记,其中一个后面紧跟重绘的行文本(`\x07\e[Hdsh> Write-Output …`)——这使尾部不再是受控提示符的前缀;在该控制台宿主上,真正提示符的标记仍然最后到达,因此该 send 仍按 `stdin_read` 结算。这条重绘路径是该 lane 的首要假设,而该 lane 的 Windows 版本与控制台宿主版本未知(job 日志两者都没有,该 pool 也没有可手动触发的 job);失败信息会记录 `os.release()`、`os.version()`、pwsh 与 PSReadLine 版本以及 `conhost.exe` 的文件版本,而 `dsh-subprocess-local` 启动 node-pty 时未设置 `useConptyDll`,因此起作用的是系统控制台宿主。这些探测同时暴露的 Windows 上 pid 为 `0` 的问题是另一个缺陷,由 issue #5297 跟踪。
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-09-27-plugin-install-interceptor-lifetime.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-27-plugin-install-interceptor-lifetime.md
|
||||
/agent-note-keep-plugin-install-interception-for-the-page-lifetime:
|
||||
en: 2f732d10bfaf9f5b
|
||||
zh: fff9f6cf8bcd86a7
|
||||
/agent-note-keep-plugin-install-interception-for-the-page-lifetime/problem:
|
||||
en: 71b72bb5b13530bb
|
||||
zh: b78170e3ac09cccf
|
||||
/agent-note-keep-plugin-install-interception-for-the-page-lifetime/decision:
|
||||
en: 45d689dced9dad9f
|
||||
zh: cb89fbbb7937ca8e
|
||||
/agent-note-keep-plugin-install-interception-for-the-page-lifetime/alternatives-considered:
|
||||
en: 1027b59d76cd3ff7
|
||||
zh: c4c60fb406acb1db
|
||||
/agent-note-keep-plugin-install-interception-for-the-page-lifetime/verification:
|
||||
en: 0e7fbf7fbb47b07c
|
||||
zh: 9b52991ad8b355ca
|
||||
/agent-note-keep-plugin-install-interception-for-the-page-lifetime/consequences:
|
||||
en: 3583e26c8ec33e03
|
||||
zh: 6080722751cf15e6
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Keep plugin-install interception for the page lifetime
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-27-plugin-install-interceptor-lifetime.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [plugin-install scenario](../../../../apps/web/tests/plugin-install-cancel.e2e.ts) can leave a directory read pending after cancelling an installation. The Host emits invalidations that trigger background reads while the scenario removes its Playwright routes between installation phases. A stalled read keeps the installed package out of the list, so enabling it closes the dialog but never exposes the highlighted card.
|
||||
|
||||
## Decision
|
||||
|
||||
The scenario registers its `installBundle` and `cancelInstall` interceptors before navigation and retains them until the browser closes. Each interceptor calls a typed handler that the scenario replaces when it changes phases. The delivery and cancellation barriers retain their ordering, and `activeReplySettled` joins the lost-response handler before the next installation starts. Ordinary requests still reach the real Host.
|
||||
|
||||
The [input-state decision](2026-09-27-web-lane-assertions-name-their-input-state.md) owns the separate wait for the highlighted card's status to become `running`. Keeping interception active lets the directory read complete; polling its result still establishes the state the golden records.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Remove routes with `unrouteAll({ behavior: 'wait' })`.** Waiting for matched handlers does not settle unrelated browser requests. A captured Chromium protocol trace shows `Fetch.disable` immediately followed by a `pluginManager/listPlugins` request with no response or failure before teardown. The Host's inventory and plugin methods still answer direct calls.
|
||||
|
||||
**Advance the browser clock or extend the locator timeout.** Advancing the paused clock does not release the stalled request. The 2.4-second highlight expiry is not responsible for a package that never reaches the directory.
|
||||
|
||||
**Add a general route-switching helper.** Two endpoint handlers in one scenario do not need another fixture API. Their local delegates preserve the existing cancellation and lost-response cases without changing the browser's interception configuration.
|
||||
|
||||
## Verification
|
||||
|
||||
The unchanged scenario passes alone and fails at the same highlighted-card locator in one of four concurrent independent processes. Instrumented failures place the unanswered directory request inside the second `unrouteAll` interval; advancing the page clock leaves it unanswered, while direct Host reads complete. The protocol trace identifies the interception transition independently of the DOM assertion.
|
||||
|
||||
`DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/plugin-install-cancel.e2e.ts` passes in four concurrent independent processes (1/1 each). One protocol capture observes one `Fetch.enable`, no `Fetch.disable`, and responses for all 33 directory requests. The existing goldens cover cancellation before delivery, cancellation of the running child, manifest and lockfile restoration, retry, enabled-card status, both themes and motion preferences, highlight expiry, and lost-result recovery.
|
||||
|
||||
## Consequences
|
||||
|
||||
The browser retains two narrowly matched interceptors for its lifetime. The scenario controls their behavior without interrupting concurrent directory reads, and browser closure owns their cleanup. Product code, goldens, test deadlines, and CI worker counts are unchanged.
|
||||
|
||||
Live-page `unrouteAll` calls also remain in [sidebar-terminal](../../../../apps/web/tests/sidebar-terminal.e2e.ts), [queue-image](../../../../apps/web/tests/queue-image.e2e.ts), [subagent-conversation](../../../../apps/web/tests/subagent-conversation.e2e.ts), and [subagent-interrupt-ui](../../../../apps/web/tests/subagent-interrupt-ui.e2e.ts). These are candidates for separate overlap investigations; this scenario's reproduction does not establish the cause of failures in those tests.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 让插件安装拦截器持续到页面关闭
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-27-plugin-install-interceptor-lifetime.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
[插件安装场景](../../../../apps/web/tests/plugin-install-cancel.e2e.ts) 在取消安装后,可能留下一次未结束的目录读取。场景在安装阶段之间移除 Playwright 路由时,Host 发出的失效通知仍会触发后台读取。停滞的读取使已安装的包无法进入列表,因此启用操作虽然关闭了对话框,却始终不显示高亮卡片。
|
||||
|
||||
## Decision
|
||||
|
||||
场景在导航前注册 `installBundle` 和 `cancelInstall` 拦截器,并保留到浏览器关闭。每个拦截器调用一个带类型的处理函数,场景切换阶段时替换该函数。请求交付和取消屏障保持原有顺序,`activeReplySettled` 在下一次安装开始前等待丢失响应的处理函数结束。普通请求仍然到达真实 Host。
|
||||
|
||||
[输入状态决策](2026-09-27-web-lane-assertions-name-their-input-state.zh.md) 负责另一项等待:高亮卡片的状态变为 `running`。持续启用拦截使目录读取能够完成;轮询其结果仍负责确立预期输出记录的状态。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**用 `unrouteAll({ behavior: 'wait' })` 移除路由。** 等待匹配的处理函数结束并不意味着其他浏览器请求已结束。捕获的 Chromium 协议记录显示,`Fetch.disable` 后紧接着发出 `pluginManager/listPlugins` 请求,直到清理前都没有响应或失败事件。直接调用 Host 的目录和插件方法仍能得到答复。
|
||||
|
||||
**推进浏览器时钟或延长 locator 超时。** 推进已暂停的时钟不能释放停滞的请求。包始终未进入目录,不是 2.4 秒的高亮到期计时器造成的。
|
||||
|
||||
**增加通用路由切换辅助函数。** 一个场景中的两个端点处理函数不需要另一层 fixture(测试前置数据)API。局部委托函数保留了现有取消和丢失响应场景,也不改变浏览器的拦截配置。
|
||||
|
||||
## Verification
|
||||
|
||||
未修改的场景单独运行通过,四个并发独立进程中有一个在同一高亮卡片 locator 处失败。插桩失败将未获响应的目录请求定位到第二次 `unrouteAll` 的执行区间内;推进页面时钟后请求仍未获响应,而直接读取 Host 可以完成。协议记录独立于 DOM 断言确认了拦截状态的切换。
|
||||
|
||||
`DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/plugin-install-cancel.e2e.ts` 在四个并发独立进程中全部通过,每个进程均为 1/1。一次协议记录观察到一次 `Fetch.enable`、零次 `Fetch.disable`,且全部 33 次目录请求均收到响应。现有预期输出覆盖取消先于请求交付、取消运行中的子进程、恢复 manifest(元数据清单)与 lockfile、重试、已启用卡片状态、两种主题与动画偏好、高亮到期,以及丢失结果恢复。
|
||||
|
||||
## Consequences
|
||||
|
||||
浏览器在整个生命周期内保留两个精确匹配的拦截器。场景控制其行为时不会中断并发目录读取,浏览器关闭负责清理它们。产品代码、预期输出、测试超时和 CI worker 数保持不变。
|
||||
|
||||
[sidebar-terminal](../../../../apps/web/tests/sidebar-terminal.e2e.ts)、[queue-image](../../../../apps/web/tests/queue-image.e2e.ts)、[subagent-conversation](../../../../apps/web/tests/subagent-conversation.e2e.ts) 和 [subagent-interrupt-ui](../../../../apps/web/tests/subagent-interrupt-ui.e2e.ts) 中也仍有页面存续期间的 `unrouteAll` 调用。这些位置可分别调查请求重叠;本场景的复现并不能确定那些测试失败的原因。
|
||||
+3
-3
@@ -13,10 +13,10 @@
|
||||
zh: 4fe1f6abd6fd4c1e
|
||||
/agent-note-web-lane-assertions-name-their-input-state/alternatives-considered:
|
||||
en: 6cd5940f55500c3d
|
||||
zh: ded68b931ed81e10
|
||||
zh: 4a2e13717e87b783
|
||||
/agent-note-web-lane-assertions-name-their-input-state/verification:
|
||||
en: 6d11abb7a60f81ae
|
||||
zh: 42bc43777a3b02b7
|
||||
en: 9a35f9dbc4285674
|
||||
zh: 0995a8b137912b6b
|
||||
/agent-note-web-lane-assertions-name-their-input-state/consequences:
|
||||
en: ad3d8ec4b171b04a
|
||||
zh: e254cc9515b33e0e
|
||||
+1
-1
@@ -40,7 +40,7 @@ Two negative controls reproduce the recorded signatures with the shipped asserti
|
||||
|
||||
Measured locally, one file at a time: `DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/<file>.e2e.ts` → workspace-new-session-folding 15/15 before the change and 20/20 after, turn-tail-actions 8/8 before and 20/20 after, plugin-install-cancel 12/12 before and 20/20 after. The lane configuration `DSH_SNAPSHOT=replay DSH_WEB_SNAPSHOT_WORKERS=16 pnpm run test:web:ci` passes all three files in three consecutive local runs.
|
||||
|
||||
The install-cancel scenario carries a residual exposure this change does not remove and whose cause is not diagnosed: twice on `node 24 / snapshots and artifacts`, and once locally with the pre-change file on the same lane command, the highlighted card locator at `plugin-install-cancel.e2e.ts:217` stayed unmatched for 30 s after the enable click closed the dialog. The paused page clock rules out the highlight expiry timer, so the wait is not racing that; no local focused run or the three lane runs above reproduced it, and nothing here is known to hang the store's `load()`. The poll added by this change covers the status read after the card appears, not this locator.
|
||||
The install-cancel scenario also recorded a separate 30 s highlighted-card wait after the enable click closed the dialog: twice on `node 24 / snapshots and artifacts`, and once locally with the pre-change file on the same lane command. The page clock was paused, excluding highlight expiry. The [interceptor-lifetime decision](2026-09-27-plugin-install-interceptor-lifetime.md) owns the diagnosed directory-request stall and its fixture fix. The status poll still covers the read after the card appears.
|
||||
|
||||
The WebKit case the serial master lane also failed is not a scenario race and stays out of this change: `declared-reasoning.e2e.ts` launches Playwright's WebKit, the lane's step fetched that browser without its host libraries, and `browserType.launch` failed with "Host system is missing dependencies to run browsers". [ci.yml](../../../../.github/workflows/ci.yml) states the persistent VM image owns Playwright's Linux system packages and installs the dependency set only off the self-hosted pool, so repairing the image or the pool's ownership is a runner change, not a scenario one. Installing WebKit's libraries on a developer machine makes the file's 8 cases pass.
|
||||
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ install-cancel 场景改为轮询卡片状态,而不再单次采样,因此
|
||||
|
||||
**在场景内禁用悬停样式或 tooltip 的模态规则。** 两者都是 golden 所证明的已发布渲染与交互行为;场景级覆盖将测试一个产品并不提供的页面。
|
||||
|
||||
**提高捕获超时或重试比较。** 两者都不能确立实际捕获到的是哪个状态;`captureStableAria` 本已等待连续两次快照相等,而错误的状态同样能满足它——这一限度已由 [CI 就绪与完成决策](2026-09-08-ci-readiness-and-completion.zh.md) 与 [连接与压缩 fixture 前置条件](2026-09-12-connection-and-compaction-fixture-preconditions.zh.md) 记录。
|
||||
**提高捕获超时或重试比较。** 两者都不能确立实际捕获到的是哪个状态;`captureStableAria` 本已等待连续两次快照相等,而错误的状态同样能满足它——这一限度已由 [CI 就绪与完成决策](2026-09-08-ci-readiness-and-completion.zh.md) 与 [连接与压缩 fixture(测试前置数据)前置条件](2026-09-12-connection-and-compaction-fixture-preconditions.zh.md) 记录。
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -40,7 +40,7 @@ install-cancel 场景改为轮询卡片状态,而不再单次采样,因此
|
||||
|
||||
本地按文件实测:`DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/<file>.e2e.ts` → workspace-new-session-folding 改动前 15/15、改动后 20/20,turn-tail-actions 改动前 8/8、改动后 20/20,plugin-install-cancel 改动前 12/12、改动后 20/20。通道配置 `DSH_SNAPSHOT=replay DSH_WEB_SNAPSHOT_WORKERS=16 pnpm run test:web:ci` 连续三次运行中三个文件全部通过。
|
||||
|
||||
install-cancel 场景仍带有本次改动未消除、且原因尚未定位的残余暴露:`node 24 / snapshots and artifacts` 上有两次、把该文件恢复到改动前版本后用同一通道命令运行的本地一次,都在启用点击关闭对话框后让 `plugin-install-cancel.e2e.ts:217` 的高亮卡片 locator 持续 30 s 未匹配。页面时钟已暂停,因此可排除高亮到期计时器,该等待并非与它竞争;上述三次通道运行与所有单文件运行都未复现,且尚无证据表明 store 的 `load()` 会挂起。本次改动新增的轮询覆盖的是卡片出现之后的状态读取,而不是这个 locator。
|
||||
install-cancel 场景还记录过独立的 30 s 高亮卡片等待:启用点击关闭对话框后,`node 24 / snapshots and artifacts` 上有两次、把该文件恢复到改动前版本后用同一通道命令运行的本地一次,都未能找到卡片。当时页面时钟已暂停,因此可排除高亮到期。[拦截器生命周期决策](2026-09-27-plugin-install-interceptor-lifetime.zh.md) 负责已定位的目录请求停滞及其 fixture 修复。状态轮询仍覆盖卡片出现之后的读取。
|
||||
|
||||
串行 master 通道同时失败的 WebKit 用例不是场景竞态,也不属于本次改动:`declared-reasoning.e2e.ts` 启动 Playwright 的 WebKit,通道的步骤只取浏览器本体而未安装宿主库,于是 `browserType.launch` 以 “Host system is missing dependencies to run browsers” 失败。[ci.yml](../../../../.github/workflows/ci.yml) 明确说明持久 VM 镜像拥有 Playwright 的 Linux 系统包,并且只在非自托管池随浏览器安装依赖集,因此修复镜像或该池的归属是 runner 侧的改动,而非场景改动。在开发者机器上安装 WebKit 依赖后该文件的 8 个用例全部通过。
|
||||
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
# 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
|
||||
@@ -157,5 +157,12 @@ jobs:
|
||||
- name: Install (immutable)
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# The same key and value the coverage and serial lanes grant: on the
|
||||
# shared 3-vCPU runner, cases that inherit Vitest's 5000 ms default timed
|
||||
# out in spawn.spec.ts and changes.spec.ts (run 36370945526) while
|
||||
# passing in under a second on a quiet host. The ci-unit aggregate is
|
||||
# `pnpm run test` through the path that consumes the variable.
|
||||
- name: Unit tests (darwin parity)
|
||||
run: pnpm run test
|
||||
env:
|
||||
DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'
|
||||
run: pnpm run check:ci:unit
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile launch, plugin management, and configuration inspection",
|
||||
"version": "0.1.7-rc.2",
|
||||
"version": "0.2.0-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop-host",
|
||||
"description": "Private Node-mode host process for the Electron desktop application",
|
||||
"version": "0.1.7-rc.2",
|
||||
"version": "0.2.0-rc.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop",
|
||||
"description": "Electron desktop shell for a bundled dsh runtime and external plugins",
|
||||
"version": "0.1.7-rc.2",
|
||||
"version": "0.2.0-rc.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-web-frontend",
|
||||
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
|
||||
"version": "0.1.7-rc.2",
|
||||
"version": "0.2.0-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -115,7 +115,8 @@ describe('web e2e: experimental Auto and Full access confirmation', () => {
|
||||
expect(slashPickerBox).not.toBeNull()
|
||||
expect(composerBox).not.toBeNull()
|
||||
expect(slashPickerBox!.width).toBeGreaterThanOrEqual(220)
|
||||
expect(composerBox!.width - slashPickerBox!.width).toBeGreaterThan(1)
|
||||
expect(slashPickerBox!.width).toBeCloseTo(composerBox!.width)
|
||||
expect(slashPickerBox!.x).toBeCloseTo(composerBox!.x)
|
||||
expect(Math.abs(composerBox!.y - slashPickerBox!.y - slashPickerBox!.height - 4)).toBeLessThan(1)
|
||||
|
||||
await slashPicker.getByRole('option', { name: 'Auto review EXP' }).click()
|
||||
|
||||
@@ -129,7 +129,8 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
|
||||
const writeRow = mutationRows.find(row => row.getAttribute('data-variant') === 'write'
|
||||
&& row.textContent?.includes('new-demo.txt'))
|
||||
if (writeRow === undefined) throw new Error('fixture write row missing')
|
||||
expect(within(writeRow).getAllByText('+1 -0', { exact: true })).toHaveLength(1)
|
||||
expect(within(writeRow).getAllByText('+1', { exact: true })).toHaveLength(1)
|
||||
expect(within(writeRow).getAllByText('-0', { exact: true })).toHaveLength(1)
|
||||
expect(writeRow.querySelector('[data-diff]')).toBeNull()
|
||||
for (const row of mutationRows) {
|
||||
const toggle = row.querySelector('[data-expandable]')
|
||||
@@ -140,7 +141,8 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
|
||||
const writeDiff = writeRow.querySelector<HTMLElement>('[data-diff]')
|
||||
if (writeDiff === null) throw new Error('expanded fixture write diff missing')
|
||||
within(writeDiff).getByText('hello fixture', { exact: true })
|
||||
expect(within(writeRow).getAllByText('+1 -0', { exact: true })).toHaveLength(1)
|
||||
expect(within(writeRow).getAllByText('+1', { exact: true })).toHaveLength(1)
|
||||
expect(within(writeRow).getAllByText('-0', { exact: true })).toHaveLength(1)
|
||||
expect(writeDiff.textContent).not.toContain('+1 -0')
|
||||
|
||||
// The web render intent reaches the assembled boot graph: the fixture's
|
||||
|
||||
@@ -46,6 +46,7 @@ describe.skipIf(MODE === 'record').each([
|
||||
models: [
|
||||
{ id: 'acme-think', name: 'Acme Think' },
|
||||
{ id: 'acme-swift', name: 'Acme Swift' },
|
||||
{ id: 'acme-lite', name: 'Acme Lite' },
|
||||
].map(model => ({
|
||||
...model,
|
||||
reasoningEfforts: { off: null, high: 'high', max: 'ultra' },
|
||||
@@ -148,8 +149,10 @@ describe.skipIf(MODE === 'record').each([
|
||||
.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
} else {
|
||||
await page.getByRole('menuitem', { name: pane === 'model' ? /^模型/ : /推理等级/ }).click()
|
||||
await expect.poll(() => page.locator('[role="menuitemradio"][aria-checked="true"]')
|
||||
.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
const focused = pane === 'model'
|
||||
? page.getByRole('searchbox', { name: '搜索模型…' })
|
||||
: page.locator('[role="menuitemradio"][aria-checked="true"]')
|
||||
await expect.poll(() => focused.evaluate(element => element === element.ownerDocument.activeElement)).toBe(true)
|
||||
}
|
||||
await trigger.click()
|
||||
await menu.waitFor({ state: 'detached' })
|
||||
@@ -172,22 +175,27 @@ describe.skipIf(MODE === 'record').each([
|
||||
await page.getByRole('menuitem', { name: /^模型/ }).click()
|
||||
const current = page.getByRole('menuitemradio', { name: 'Acme Think', exact: true })
|
||||
const target = page.getByRole('menuitemradio', { name: 'Acme Swift', exact: true })
|
||||
await expect.poll(() => current.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
const search = page.getByRole('searchbox', { name: '搜索模型…' })
|
||||
await expect.poll(() => search.evaluate(element => element === element.ownerDocument.activeElement)).toBe(true)
|
||||
expect(await search.getAttribute('aria-activedescendant')).toBe(await current.getAttribute('id'))
|
||||
|
||||
// Native mousedown must not blur the focused row and unmount the menu before click in WebKit.
|
||||
// Native mousedown must not blur search and unmount the menu before click in WebKit.
|
||||
await target.getByText('Acme Swift', { exact: true }).hover()
|
||||
await page.mouse.down()
|
||||
try {
|
||||
await expect.poll(() => menu.count()).toBe(1)
|
||||
await expect.poll(() => current.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
await expect.poll(() => search.evaluate(element => element === element.ownerDocument.activeElement)).toBe(true)
|
||||
await page.getByText('Acme Gateway', { exact: true }).hover()
|
||||
} finally {
|
||||
await page.mouse.up()
|
||||
}
|
||||
// Later selection counts also include any request from this cancelled press.
|
||||
expect(selections).toBe(0)
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await expect.poll(() => target.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
await search.press('ArrowUp')
|
||||
expect(await search.getAttribute('aria-activedescendant')).toBe(await current.getAttribute('id'))
|
||||
await search.press('ArrowDown')
|
||||
expect(await search.getAttribute('aria-activedescendant')).toBe(await target.getAttribute('id'))
|
||||
await expect.poll(() => search.evaluate(element => element === element.ownerDocument.activeElement)).toBe(true)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Shift+Tab')
|
||||
await menu.waitFor({ state: 'detached' })
|
||||
@@ -222,16 +230,20 @@ describe.skipIf(MODE === 'record').each([
|
||||
}, { times: 1 })
|
||||
await trigger.click()
|
||||
await page.getByRole('menuitem', { name: /^模型/ }).click()
|
||||
await expect.poll(() => target.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
await expect.poll(() => search.evaluate(element => element === element.ownerDocument.activeElement)).toBe(true)
|
||||
expect(await search.getAttribute('aria-activedescendant')).toBe(await target.getAttribute('id'))
|
||||
await current.click()
|
||||
await page.getByRole('alert').waitFor()
|
||||
expect(selections).toBe(3)
|
||||
await compareOrRefreshGolden(POINTER_EXPECTED, await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd), MODE)
|
||||
await compareOrRefreshGolden(POINTER_EXPECTED,
|
||||
await captureStableAria(page, '[role="group"][aria-label="模型与推理等级"]', scaffold.workspaceCwd), MODE)
|
||||
await expect.poll(() => trigger.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
await page.keyboard.press('Tab')
|
||||
await expect.poll(() => target.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await expect.poll(() => current.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
await expect.poll(() => search.evaluate(element => element === element.ownerDocument.activeElement)).toBe(true)
|
||||
expect(await search.getAttribute('aria-activedescendant')).toBe(await target.getAttribute('id'))
|
||||
await search.press('ArrowUp')
|
||||
expect(await search.getAttribute('aria-activedescendant')).toBe(await current.getAttribute('id'))
|
||||
expect(await search.evaluate(element => element === element.ownerDocument.activeElement)).toBe(true)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
await menu.waitFor({ state: 'detached' })
|
||||
|
||||
@@ -80,7 +80,13 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
await page.getByRole('menuitemradio', { name: 'Messages Flash', exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'picker.expected.md'),
|
||||
await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd), webSnapshotMode())
|
||||
await captureStableAria(page, '[role="group"][aria-label="模型与推理等级"]', scaffold.workspaceCwd), webSnapshotMode())
|
||||
await page.keyboard.press('Escape')
|
||||
await page.getByRole('menuitem', { name: /推理等级/ }).click()
|
||||
const effortWeights = await page.getByRole('menuitemradio').evaluateAll(rows => rows.map(row =>
|
||||
getComputedStyle(row.querySelector('span span')!).fontWeight,
|
||||
))
|
||||
expect(new Set(effortWeights)).toEqual(new Set(['400']))
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -73,7 +73,7 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
displayName: 'Acme Gateway',
|
||||
api: 'openai-completions',
|
||||
baseURL: 'https://gateway.acme.example/v1',
|
||||
models: [{ id: MODEL, name: 'Acme Large' }],
|
||||
models: [{ id: MODEL, name: 'Acme Large' }, { id: 'acme-small', name: 'Acme Small' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -92,6 +92,223 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('keeps command popup search borders transparent in both palettes', async () => {
|
||||
const composer = page.locator('[data-composer-input]').first()
|
||||
await page.getByRole('button', { name: '添加文件或调用指令', exact: true }).click()
|
||||
const commandMenuBounds = await page.locator('[data-trigger-menu]').boundingBox()
|
||||
await page.getByRole('option', { name: /^模型/ }).click()
|
||||
const search = page.getByRole('textbox', { name: '筛选选项', exact: true })
|
||||
await search.waitFor()
|
||||
try {
|
||||
const popupBounds = await page.locator('[aria-label="/model 选项"]').boundingBox()
|
||||
expect(popupBounds).not.toBeNull()
|
||||
expect(commandMenuBounds).not.toBeNull()
|
||||
expect(popupBounds!.width).toBeCloseTo(commandMenuBounds!.width)
|
||||
expect(popupBounds!.x).toBeCloseTo(commandMenuBounds!.x)
|
||||
expect(await search.getAttribute('placeholder')).toBe('搜索模型…')
|
||||
await page.getByRole('option').first().waitFor()
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL('./expected/default-model/command-picker.expected.md', import.meta.url)),
|
||||
await captureStableAria(page, '[aria-label="/model 选项"]', scaffold.workspaceCwd),
|
||||
webSnapshotMode(),
|
||||
)
|
||||
await search.fill('no-model-matches')
|
||||
await page.getByText('没有匹配的模型。', { exact: true }).waitFor()
|
||||
await search.fill('')
|
||||
const borders = await search.evaluate((input) => {
|
||||
const body = input.ownerDocument.body
|
||||
const previousTheme = body.getAttribute('data-ds-dark-theme')
|
||||
try {
|
||||
return [false, true].map((dark) => {
|
||||
body.toggleAttribute('data-ds-dark-theme', dark)
|
||||
return getComputedStyle(input).borderColor
|
||||
})
|
||||
} finally {
|
||||
if (previousTheme === null) body.removeAttribute('data-ds-dark-theme')
|
||||
else body.setAttribute('data-ds-dark-theme', previousTheme)
|
||||
}
|
||||
})
|
||||
expect(borders).toEqual(['rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'])
|
||||
} finally {
|
||||
await search.press('Escape')
|
||||
await composer.fill('')
|
||||
}
|
||||
})
|
||||
|
||||
it('shares provider order, fuzzy result order and sticky header material between both model pickers', async () => {
|
||||
const readGroups = (surface: Locator, role: 'option' | 'menuitemradio') => surface.locator('[data-menu-group]')
|
||||
.evaluateAll((groups, rowRole) => groups.map(group => ({
|
||||
label: group.querySelector('[data-menu-group-heading]')!.textContent,
|
||||
rows: [...group.querySelectorAll(`[role="${rowRole}"]`)].map(row => row.textContent),
|
||||
})), role)
|
||||
const checkSticky = async (surface: Locator, viewport: Locator) => {
|
||||
// Bound only this test's list viewport so native scrolling can cross a heading.
|
||||
await viewport.evaluate((node) => { node.style.maxHeight = '80px'; node.scrollTop = 0 })
|
||||
const heading = surface.locator('[data-menu-group-heading]').first()
|
||||
await expect.poll(() => heading.getAttribute('data-stuck')).toBeNull()
|
||||
const clear = await heading.evaluate(node => getComputedStyle(node).backgroundColor)
|
||||
expect(clear).toBe('rgba(0, 0, 0, 0)')
|
||||
await viewport.evaluate((node) => { node.scrollTop = 10 })
|
||||
await expect.poll(() => heading.getAttribute('data-stuck')).toBe('')
|
||||
const stuck = await heading.evaluate((node) => {
|
||||
const style = getComputedStyle(node)
|
||||
return { fill: style.backgroundColor, radius: style.borderRadius, font: style.fontSize, padding: style.padding }
|
||||
})
|
||||
await viewport.evaluate((node) => { node.scrollTop = 0 })
|
||||
await expect.poll(() => heading.getAttribute('data-stuck')).toBeNull()
|
||||
return stuck
|
||||
}
|
||||
try {
|
||||
await page.getByRole('button', { name: /^选择模型/ }).click()
|
||||
await page.getByRole('menuitem', { name: /^模型/ }).click()
|
||||
const menu = page.getByRole('group', { name: '模型与推理等级', exact: true })
|
||||
const menuSearch = page.getByRole('searchbox', { name: '搜索模型…' })
|
||||
const order = await readGroups(menu, 'menuitemradio')
|
||||
const sticky = await checkSticky(menu, menu.getByRole('menu', { name: '模型', exact: true }))
|
||||
await menuSearch.fill(' ACMLG ')
|
||||
const filtered = await readGroups(menu, 'menuitemradio')
|
||||
await menuSearch.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
await page.getByRole('button', { name: '添加文件或调用指令', exact: true }).click()
|
||||
await page.getByRole('option', { name: /^模型/ }).click()
|
||||
const popup = page.locator('[aria-label="/model 选项"]')
|
||||
await popup.getByRole('option').first().waitFor()
|
||||
expect(await readGroups(popup, 'option')).toEqual(order)
|
||||
expect(await checkSticky(popup, popup.getByRole('listbox'))).toEqual(sticky)
|
||||
const popupSearch = page.getByRole('textbox', { name: '筛选选项', exact: true })
|
||||
await popupSearch.fill(' ACMLG ')
|
||||
expect(await readGroups(popup, 'option')).toEqual(filtered)
|
||||
expect(await popupSearch.evaluate(node => node === node.ownerDocument.activeElement)).toBe(true)
|
||||
} finally {
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
await page.locator('[data-composer-input]').first().fill('')
|
||||
}
|
||||
})
|
||||
|
||||
it('hides search only in the button menu when four models remain', async () => {
|
||||
const trigger = page.getByRole('button', { name: /^选择模型/ })
|
||||
const search = page.getByRole('searchbox', { name: '搜索模型…' })
|
||||
const setModels = (expanded: boolean) => scaffold.ctx.settings.update('llm-pi-ai', {
|
||||
providers: { [ROUTE]: {
|
||||
displayName: 'Acme Gateway', api: 'openai-completions', baseURL: 'https://gateway.acme.example/v1',
|
||||
models: expanded ? [{ id: MODEL, name: 'Acme Large' }, { id: 'acme-small', name: 'Acme Small' }]
|
||||
: [{ id: MODEL, name: 'Acme Large' }],
|
||||
} },
|
||||
})
|
||||
try {
|
||||
await setModels(false)
|
||||
await trigger.click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
await expect.poll(() => page.getByRole('menuitemradio').count()).toBe(4)
|
||||
expect(await search.count()).toBe(0)
|
||||
const current = page.getByRole('menuitemradio', { name: 'Origin Large', exact: true })
|
||||
await expect.poll(() => current.evaluate(node => node === node.ownerDocument.activeElement)).toBe(true)
|
||||
const rows = page.getByRole('menuitemradio')
|
||||
const currentId = await current.getAttribute('id')
|
||||
for (let index = 0; index < 4; index++) await page.keyboard.press('ArrowDown')
|
||||
expect(await page.evaluate(() => document.activeElement?.id)).toBe(currentId)
|
||||
await page.keyboard.press('Enter')
|
||||
await expect.poll(() => trigger.evaluate(node => node === node.ownerDocument.activeElement)).toBe(true)
|
||||
expect(await trigger.evaluate(node => getComputedStyle(node).boxShadow)).toBe('none')
|
||||
await page.getByRole('button', { name: '添加文件或调用指令', exact: true }).click()
|
||||
await page.getByRole('option', { name: /^模型/ }).click()
|
||||
const commandSearch = page.getByRole('textbox', { name: '筛选选项', exact: true })
|
||||
await commandSearch.waitFor()
|
||||
expect(await commandSearch.getAttribute('placeholder')).toBe('搜索模型…')
|
||||
await commandSearch.press('Escape')
|
||||
await page.locator('[data-composer-input]').first().fill('')
|
||||
expect(await rows.count()).toBe(0)
|
||||
} finally {
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
await setModels(true)
|
||||
await trigger.click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
await expect.poll(() => page.getByRole('menuitemradio').count()).toBe(5)
|
||||
await search.waitFor()
|
||||
await search.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
}
|
||||
})
|
||||
|
||||
it('paints only pinned provider headings across themes, filtering, and reopening', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-model-sticky-headings'))
|
||||
const originalViewport = page.viewportSize()!
|
||||
const attributes = await page.evaluate(() => ({
|
||||
platform: document.documentElement.getAttribute('data-platform'),
|
||||
theme: document.body.getAttribute('data-ds-dark-theme'),
|
||||
}))
|
||||
const surface = page.getByRole('group', { name: '模型与推理等级', exact: true })
|
||||
const trigger = page.getByRole('button', { name: /^选择模型/ })
|
||||
const search = page.getByRole('searchbox', { name: '搜索模型…' })
|
||||
try {
|
||||
await page.setViewportSize({ width: 1680, height: 220 })
|
||||
await trigger.click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
const scroller = page.getByRole('menu', { name: '模型', exact: true })
|
||||
const headings = surface.locator('section[role="group"] > div')
|
||||
const readPinned = () => headings.evaluateAll(nodes => nodes.map(node => node.hasAttribute('data-stuck')))
|
||||
const resetScroll = async (): Promise<void> => {
|
||||
await scroller.evaluate((node) => { node.scrollTop = 0 })
|
||||
await expect.poll(readPinned).toEqual([false, false, false])
|
||||
}
|
||||
for (const platform of ['web', 'win32', 'darwin']) {
|
||||
for (const dark of [false, true]) {
|
||||
await page.evaluate(({ platform, dark }) => {
|
||||
if (platform === 'web') document.documentElement.removeAttribute('data-platform')
|
||||
else document.documentElement.setAttribute('data-platform', platform)
|
||||
document.body.toggleAttribute('data-ds-dark-theme', dark)
|
||||
}, { platform, dark })
|
||||
await resetScroll()
|
||||
expect(await headings.evaluateAll(nodes => [...new Set(nodes.map(node => getComputedStyle(node).backgroundColor))]))
|
||||
.toEqual(['rgba(0, 0, 0, 0)'])
|
||||
const secondTop = await scroller.evaluate((node) => {
|
||||
const second = node.querySelectorAll('section')[1]!
|
||||
return Math.ceil(second.getBoundingClientRect().top - node.getBoundingClientRect().top) + 1
|
||||
})
|
||||
expect(await scroller.evaluate(node => node.scrollHeight - node.clientHeight)).toBeGreaterThanOrEqual(secondTop)
|
||||
await scroller.evaluate((node) => { node.scrollTop = 10 })
|
||||
await expect.poll(readPinned).toEqual([true, false, false])
|
||||
expect(await headings.first().evaluate(node => getComputedStyle(node).backgroundColor))
|
||||
.toBe(dark ? 'rgba(48, 49, 54, 0.94)' : 'rgba(248, 249, 250, 0.94)')
|
||||
await scroller.evaluate((node, top) => { node.scrollTop = top }, secondTop)
|
||||
await expect.poll(readPinned).toEqual([false, true, false])
|
||||
await scroller.evaluate((node) => { node.scrollTop = 10 })
|
||||
await expect.poll(readPinned).toEqual([true, false, false])
|
||||
await resetScroll()
|
||||
}
|
||||
}
|
||||
await scroller.evaluate((node) => { node.scrollTop = 10 })
|
||||
await expect.poll(readPinned).toEqual([true, false, false])
|
||||
await search.fill('Origin Large')
|
||||
await expect.poll(() => headings.count()).toBe(1)
|
||||
await expect.poll(readPinned).toEqual([false])
|
||||
await page.getByRole('button', { name: '清除搜索', exact: true }).click()
|
||||
await expect.poll(readPinned).toEqual([false, false, false])
|
||||
await search.fill('zzzz')
|
||||
await expect.poll(() => surface.locator('[data-stuck]').count()).toBe(0)
|
||||
await page.getByRole('button', { name: '清除搜索', exact: true }).click()
|
||||
await expect.poll(readPinned).toEqual([false, false, false])
|
||||
await search.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
await trigger.click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
expect(await search.inputValue()).toBe('')
|
||||
await resetScroll()
|
||||
} finally {
|
||||
if (await surface.count()) await search.press('Escape')
|
||||
if (await trigger.getAttribute('aria-expanded') === 'true') await page.keyboard.press('Escape')
|
||||
await page.setViewportSize(originalViewport)
|
||||
await page.evaluate(({ platform, theme }) => {
|
||||
if (platform === null) document.documentElement.removeAttribute('data-platform')
|
||||
else document.documentElement.setAttribute('data-platform', platform)
|
||||
if (theme === null) document.body.removeAttribute('data-ds-dark-theme')
|
||||
else document.body.setAttribute('data-ds-dark-theme', theme)
|
||||
}, attributes)
|
||||
}
|
||||
})
|
||||
|
||||
it('writes the switched model as the default and leaves a logged session alone', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-default-model'))
|
||||
// A session that has already run a turn, spelled as the fact a turn
|
||||
@@ -106,7 +323,109 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
await trigger.waitFor({ timeout: 15_000 })
|
||||
expect(await trigger.evaluate(element => getComputedStyle(element).fontWeight)).toBe('400')
|
||||
await trigger.click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
const modelMenuBounds = await page.getByRole('menu', { name: '模型与推理等级', exact: true }).boundingBox()
|
||||
const composerBounds = await page.locator('[data-composer-card]').first().boundingBox()
|
||||
expect(modelMenuBounds!.width).toBeLessThan(composerBounds!.width)
|
||||
const modelCell = page.getByRole('menuitem', { name: /模型/ })
|
||||
await trigger.press('ArrowDown')
|
||||
expect(await modelCell.evaluate(element => element.matches(':focus-visible'))).toBe(true)
|
||||
expect(await modelCell.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none')
|
||||
const focusedBackground = await modelCell.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
await trigger.focus()
|
||||
await modelCell.hover()
|
||||
expect(await modelCell.evaluate(element => getComputedStyle(element).backgroundColor)).toBe(focusedBackground)
|
||||
await modelCell.click()
|
||||
const search = page.getByRole('searchbox', { name: '搜索模型…' })
|
||||
const rowIds = await page.getByRole('menuitemradio').evaluateAll(rows => rows.map(row => row.id))
|
||||
const initialHighlight = rowIds.indexOf(await search.getAttribute('aria-activedescendant') ?? '')
|
||||
expect(initialHighlight).toBeGreaterThanOrEqual(0)
|
||||
for (let step = 1; step <= rowIds.length + 1; step++) {
|
||||
await search.press('ArrowDown')
|
||||
expect(await search.getAttribute('aria-activedescendant')).toBe(rowIds[(initialHighlight + step) % rowIds.length])
|
||||
expect(await search.evaluate(input => input === input.ownerDocument.activeElement)).toBe(true)
|
||||
}
|
||||
await search.press('ArrowUp')
|
||||
expect(await search.getAttribute('aria-activedescendant')).toBe(rowIds[initialHighlight])
|
||||
const searchStyle = await search.evaluate((input) => {
|
||||
const wrapper = input.parentElement!
|
||||
const caption = input.ownerDocument.createElement('span')
|
||||
caption.style.color = 'var(--dsw-alias-label-caption)'
|
||||
wrapper.append(caption)
|
||||
try {
|
||||
return {
|
||||
placeholderColor: getComputedStyle(input, '::placeholder').color,
|
||||
captionColor: getComputedStyle(caption).color,
|
||||
iconCount: wrapper.querySelectorAll('svg').length,
|
||||
background: getComputedStyle(wrapper).backgroundColor,
|
||||
radius: getComputedStyle(wrapper).borderRadius,
|
||||
padding: getComputedStyle(wrapper).padding,
|
||||
fontSize: getComputedStyle(input).fontSize,
|
||||
}
|
||||
} finally {
|
||||
caption.remove()
|
||||
}
|
||||
})
|
||||
expect(searchStyle.placeholderColor).toBe(searchStyle.captionColor)
|
||||
expect(searchStyle.iconCount).toBe(0)
|
||||
expect(searchStyle.background).toBe('rgba(0, 0, 0, 0)')
|
||||
expect(searchStyle.radius).toBe('12px')
|
||||
expect(searchStyle.padding).toBe('5px 7px')
|
||||
expect(searchStyle.fontSize).toBe('12px')
|
||||
const modelWeights = await page.getByRole('menuitemradio').evaluateAll(rows => rows.map(row =>
|
||||
getComputedStyle(row.querySelector('span span')!).fontWeight,
|
||||
))
|
||||
expect(new Set(modelWeights)).toEqual(new Set(['400']))
|
||||
const headingPalettes = await page.getByRole('group', { name: '模型与推理等级', exact: true }).evaluate((menu) => {
|
||||
const html = menu.ownerDocument.documentElement
|
||||
const body = menu.ownerDocument.body
|
||||
const previousPlatform = html.getAttribute('data-platform')
|
||||
const previousTheme = body.getAttribute('data-ds-dark-theme')
|
||||
try {
|
||||
return ['web', 'win32', 'darwin'].flatMap(platform => [false, true].map((dark) => {
|
||||
if (platform === 'web') html.removeAttribute('data-platform')
|
||||
else html.setAttribute('data-platform', platform)
|
||||
body.toggleAttribute('data-ds-dark-theme', dark)
|
||||
const headings = [...menu.querySelectorAll('section[role="group"] > div')]
|
||||
return {
|
||||
platform, dark,
|
||||
headingFills: [...new Set(headings.map(heading => getComputedStyle(heading).backgroundColor))],
|
||||
positions: [...new Set(headings.map(heading => getComputedStyle(heading).position))],
|
||||
headingRadii: [...new Set(headings.map(heading => getComputedStyle(heading).borderRadius))],
|
||||
optionRadius: getComputedStyle(menu.querySelector('[role="menuitemradio"]')!).borderRadius,
|
||||
menuFill: getComputedStyle(menu.querySelector(':scope > [aria-hidden="true"]')!).backgroundColor,
|
||||
searchBorder: getComputedStyle(menu.querySelector('input')!.parentElement!).borderColor,
|
||||
}
|
||||
}))
|
||||
} finally {
|
||||
if (previousPlatform === null) html.removeAttribute('data-platform')
|
||||
else html.setAttribute('data-platform', previousPlatform)
|
||||
if (previousTheme === null) body.removeAttribute('data-ds-dark-theme')
|
||||
else body.setAttribute('data-ds-dark-theme', previousTheme)
|
||||
}
|
||||
})
|
||||
for (const palette of headingPalettes) {
|
||||
expect(palette.headingFills, `${palette.platform}, dark=${String(palette.dark)}`)
|
||||
.toEqual(['rgba(0, 0, 0, 0)'])
|
||||
expect(palette.menuFill).toBe(palette.dark ? 'rgba(67, 69, 74, 0.45)' : 'rgba(248, 249, 250, 0.58)')
|
||||
expect(palette.searchBorder).toBe('rgba(0, 0, 0, 0)')
|
||||
expect(palette.positions).toEqual(['sticky'])
|
||||
expect(palette.headingRadii).toEqual([palette.platform === 'darwin' ? '0px' : palette.optionRadius])
|
||||
}
|
||||
await search.fill('zzzz')
|
||||
await page.getByText('没有匹配的模型。', { exact: true }).waitFor()
|
||||
await page.getByRole('button', { name: '清除搜索', exact: true }).click()
|
||||
expect(await search.inputValue()).toBe('')
|
||||
expect(await search.evaluate(input => input === input.ownerDocument.activeElement)).toBe(true)
|
||||
expect(await page.getByRole('menuitemradio').count()).toBe(modelWeights.length)
|
||||
expect(await page.getByRole('button', { name: '清除搜索', exact: true }).count()).toBe(0)
|
||||
await search.fill(' ACMLG ')
|
||||
expect(await page.getByRole('menuitemradio').allTextContents()).toEqual(['Acme Large'])
|
||||
expect(await page.getByRole('group', { name: 'Origin Gateway', exact: true }).count()).toBe(0)
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL('./expected/default-model/search.expected.md', import.meta.url)),
|
||||
await captureStableAria(page, '[role="group"][aria-label="模型与推理等级"]', scaffold.workspaceCwd),
|
||||
webSnapshotMode(),
|
||||
)
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const blocked = scaffold.ctx.hmr.runExclusive(async () => {
|
||||
@@ -115,9 +434,15 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
})
|
||||
try {
|
||||
await entered.promise
|
||||
await page.getByRole('menuitemradio', { name: 'Acme Large' }).click()
|
||||
await search.press('ArrowDown')
|
||||
await expect.poll(() => search.evaluate(input => input === input.ownerDocument.activeElement)).toBe(true)
|
||||
expect(await search.getAttribute('aria-activedescendant'))
|
||||
.toBe(await page.getByRole('menuitemradio', { name: 'Acme Large' }).getAttribute('id'))
|
||||
await page.keyboard.press('Enter')
|
||||
await expect.poll(() => trigger.getAttribute('aria-busy')).toBe('false')
|
||||
await expect.poll(() => trigger.textContent()).toContain('Acme Large')
|
||||
await expect.poll(() => trigger.evaluate(element => element === element.ownerDocument.activeElement)).toBe(true)
|
||||
expect(await trigger.evaluate(element => getComputedStyle(element).boxShadow)).toBe('none')
|
||||
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: START_ROUTE, model: START_MODEL })
|
||||
const aria = await captureStableAria(page, '[data-composer-card]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/default-model/background-save.expected.md', import.meta.url)), aria, webSnapshotMode())
|
||||
@@ -143,6 +468,10 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
.toEqual({ provider: ROUTE, model: MODEL })
|
||||
// ...while the one holding a logged route keeps deriving from its log.
|
||||
expect(await currentOf(loggedId)).toEqual({ provider: START_ROUTE, model: START_MODEL })
|
||||
await trigger.press('Tab')
|
||||
await trigger.focus()
|
||||
expect(await trigger.evaluate(element => element.matches(':focus-visible'))).toBe(true)
|
||||
expect(await trigger.evaluate(element => getComputedStyle(element).boxShadow)).not.toBe('none')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
- tab "Trajectory"
|
||||
- text: "Assemble the link gallery: write the report and styles, inspect the sources, and summarize. {{clock}}"
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [expanded]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [expanded]
|
||||
- button "Wrote files, called tools, searched code, etc." [expanded]
|
||||
- button "Write site/report.html +1 -0":
|
||||
- text: Write
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
- button "Jump to turn 3"
|
||||
- text: "Use only Cordis tools. First call cordis_inspect_self with no arguments. Then call cordis_define with plugin kind \"new\", idPrefix \"snap\", name \"snapshot noop\", purpose \"does nothing, for the snapshot\", code.host exactly \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\" and code.client exactly \"return { inject: [\\\"slots\\\"], apply(ctx) { ctx.slots.register({ name: \\\"shell.overlay\\\", id: \\\"snapshot-probe\\\" }, () => React.createElement(\\\"div\\\", { \\\"data-snapshot-probe\\\": \\\"loaded\\\" })) } }\". Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode \"run\". After the run request returns, reply exactly CORDIS_UI_READY and stop. 9/1 {{clock}}"
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [expanded]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [expanded]
|
||||
- button "Called tools" [expanded]
|
||||
- button "Think I will inspect the current Session's dynamic Cordis Plugins before defining the snapshot Package."
|
||||
- button "Inspect plugins 0 dynamic plugins"
|
||||
@@ -42,8 +42,8 @@
|
||||
- button "Plugin status updated 9/1 {{clock}}":
|
||||
- text: Plugin status updated
|
||||
- time: 9/1 {{clock}}
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: The Cordis Plugin is running.
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -51,8 +51,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 9/1 {{clock}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. 9/1 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [expanded]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [expanded]
|
||||
- button "Called tools" [expanded]
|
||||
- text: Stop Cordis Plugin snap-1
|
||||
- button "Inspect"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
- menu "模型与推理等级":
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- menuitemradio "DeepSeek-V4-Flash"
|
||||
- menuitemradio "DeepSeek-V4-Flash-Vision-Exp"
|
||||
- group "Acme Gateway":
|
||||
- text: Acme Gateway
|
||||
- menuitemradio "Acme Think"
|
||||
- menuitemradio "Acme Swift" [checked]
|
||||
- group "模型与推理等级":
|
||||
- searchbox "搜索模型…"
|
||||
- menu "模型":
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- menuitemradio "DeepSeek-V4-Flash"
|
||||
- menuitemradio "DeepSeek-V4-Flash-Vision-Exp"
|
||||
- group "Acme Gateway":
|
||||
- text: Acme Gateway
|
||||
- menuitemradio "Acme Think"
|
||||
- menuitemradio "Acme Swift" [checked]
|
||||
- menuitemradio "Acme Lite"
|
||||
@@ -1,5 +1,6 @@
|
||||
- menu "模型与推理等级":
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- menuitemradio "Messages Flash" [checked]
|
||||
- menuitemradio "DeepSeek-V4-Pro"
|
||||
- group "模型与推理等级":
|
||||
- menu "模型":
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- menuitemradio "Messages Flash" [checked]
|
||||
- menuitemradio "DeepSeek-V4-Pro"
|
||||
@@ -0,0 +1,14 @@
|
||||
- textbox "筛选选项":
|
||||
- /placeholder: 搜索模型…
|
||||
- listbox "/model 匹配项":
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- option "DeepSeek-V4-Flash"
|
||||
- option "DeepSeek-V4-Flash-Vision-Exp"
|
||||
- group "Origin Gateway":
|
||||
- text: Origin Gateway
|
||||
- option "Origin Large"
|
||||
- group "Acme Gateway":
|
||||
- text: Acme Gateway
|
||||
- option "Acme Large" [selected]
|
||||
- option "Acme Small"
|
||||
@@ -0,0 +1,7 @@
|
||||
- group "模型与推理等级":
|
||||
- searchbox "搜索模型…": ACMLG
|
||||
- button "清除搜索"
|
||||
- menu "模型":
|
||||
- group "Acme Gateway":
|
||||
- text: Acme Gateway
|
||||
- menuitemradio "Acme Large"
|
||||
@@ -20,8 +20,6 @@
|
||||
- button "减小字号"
|
||||
- text: px 工作步骤展示 选择希望看到多少工具调用细节
|
||||
- button "标准"
|
||||
- text: 性能与用量 选择性能与用量信息展示的详细程度
|
||||
- button "详细"
|
||||
- text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换
|
||||
- switch "代码工作工具" [checked]
|
||||
- text: 快捷键
|
||||
@@ -29,6 +27,8 @@
|
||||
- button "编辑快捷键"
|
||||
- text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为
|
||||
- button "排队发送"
|
||||
- text: 性能与用量 选择性能与用量信息展示的详细程度
|
||||
- button "详细"
|
||||
- text: 在使用官方模型 API 时上传 Session Log 帮助改进 DeepSeek 模型与产品
|
||||
- switch "在使用官方模型 API 时上传 Session Log"
|
||||
- text: 当前版本:{{version}}
|
||||
@@ -20,8 +20,6 @@
|
||||
- button "Decrease font size"
|
||||
- text: px Work details Choose how much detail to show for tool calls
|
||||
- button "Standard"
|
||||
- text: Performance & usage Choose how much performance and usage information to show
|
||||
- button "Detailed"
|
||||
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
|
||||
- switch "Coding Tools" [checked]
|
||||
- text: Keyboard shortcuts
|
||||
@@ -29,6 +27,8 @@
|
||||
- button "Edit shortcuts"
|
||||
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
|
||||
- button "Queue"
|
||||
- text: Performance & usage Choose how much performance and usage information to show
|
||||
- button "Detailed"
|
||||
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
|
||||
- switch "Upload Session Log when using the official model API"
|
||||
- text: "Current version: {{version}}"
|
||||
@@ -1,11 +1,11 @@
|
||||
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}"
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}"
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}"
|
||||
- text: FORK_BRANCH_USER Summarize what this branch knows about the reads. {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}"
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}"
|
||||
- paragraph: FORK_BRANCH_FIRST this branch has no read results, so I would check what the parent completed before retrying. FORK_BRANCH_DONE.
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
- button "GitHub event received {{clock}}":
|
||||
- text: GitHub event received
|
||||
- time: {{clock}}
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: "Review complete: no actionable findings."
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
- button "GitHub event received {{clock}}":
|
||||
- text: GitHub event received
|
||||
- time: {{clock}}
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: "Review complete: no actionable findings."
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
- tab "Trajectory"
|
||||
- text: Render adjacent CJK strong emphasis. {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- heading "CJK strong emphasis" [level=2]
|
||||
- paragraph:
|
||||
- strong: 注意:
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
- tab "Trajectory"
|
||||
- text: Show the Markdown image policy. {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- heading "Markdown images" [level=2]
|
||||
- paragraph:
|
||||
- 'button "View full image: Remote test image"':
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
- tab "Trajectory"
|
||||
- text: Show the local preview URL. {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- heading "Inline code links" [level=2]
|
||||
- paragraph:
|
||||
- text: "Preview:"
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
- tab "Trajectory"
|
||||
- text: Render this mathematical proof. {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- heading "Math rendering" [level=2]
|
||||
- paragraph:
|
||||
- text: Inline dollar
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
- tab "Trajectory"
|
||||
- text: Research notes what changed? Referenced session · Research notes {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- textbox "Message or run a task, / commands, @ files or sessions"
|
||||
- button "Add files or run commands"
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
- button "Decrease font size"
|
||||
- text: px Work details Choose how much detail to show for tool calls
|
||||
- button "Detailed"
|
||||
- text: Performance & usage Choose how much performance and usage information to show
|
||||
- button "Detailed"
|
||||
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
|
||||
- switch "Coding Tools"
|
||||
- text: Keyboard shortcuts
|
||||
@@ -29,6 +27,8 @@
|
||||
- button "Edit shortcuts"
|
||||
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
|
||||
- button "Queue"
|
||||
- text: Performance & usage Choose how much performance and usage information to show
|
||||
- button "Detailed"
|
||||
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
|
||||
- switch "Upload Session Log when using the official model API"
|
||||
- text: "Current version: {{version}}"
|
||||
@@ -20,8 +20,6 @@
|
||||
- button "Decrease font size"
|
||||
- text: px Work details Choose how much detail to show for tool calls
|
||||
- button "Detailed"
|
||||
- text: Performance & usage Choose how much performance and usage information to show
|
||||
- button "Detailed"
|
||||
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
|
||||
- switch "Coding Tools"
|
||||
- text: Keyboard shortcuts
|
||||
@@ -29,6 +27,8 @@
|
||||
- button "Edit shortcuts"
|
||||
- text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior
|
||||
- button "Queue"
|
||||
- text: Performance & usage Choose how much performance and usage information to show
|
||||
- button "Detailed"
|
||||
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
|
||||
- switch "Upload Session Log when using the official model API"
|
||||
- text: "Current version: {{version}}"
|
||||
@@ -20,8 +20,6 @@
|
||||
- button "减小字号"
|
||||
- text: px 工作步骤展示 选择希望看到多少工具调用细节
|
||||
- button "详细"
|
||||
- text: 性能与用量 选择性能与用量信息展示的详细程度
|
||||
- button "详细"
|
||||
- text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换
|
||||
- switch "代码工作工具"
|
||||
- text: 快捷键
|
||||
@@ -29,6 +27,8 @@
|
||||
- button "编辑快捷键"
|
||||
- text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为
|
||||
- button "排队发送"
|
||||
- text: 性能与用量 选择性能与用量信息展示的详细程度
|
||||
- button "详细"
|
||||
- text: 在使用官方模型 API 时上传 Session Log 帮助改进 DeepSeek 模型与产品
|
||||
- switch "在使用官方模型 API 时上传 Session Log"
|
||||
- text: 当前版本:{{version}}
|
||||
@@ -23,7 +23,7 @@
|
||||
- text: Add workspace ⌥ ⌘ O
|
||||
- listitem:
|
||||
- button "Edit shortcut for Rename session"
|
||||
- text: Rename session ⇧ ⌘ R
|
||||
- text: Rename session ⌥ ⌘ G
|
||||
- listitem:
|
||||
- button "Edit shortcut for Fork session"
|
||||
- text: Fork session ⇧ ⌘ F
|
||||
@@ -32,7 +32,7 @@
|
||||
- text: Archive session ⌥ ⌘ A
|
||||
- listitem:
|
||||
- button "Edit shortcut for Open settings"
|
||||
- text: Open settings ⌘ ,
|
||||
- text: Open settings ⌥ ⌘ ,
|
||||
- listitem:
|
||||
- button "Edit shortcut for Toggle right sidebar"
|
||||
- text: Toggle right sidebar ⇧ ⌘ B
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- text: Add workspace Ctrl + Alt + O
|
||||
- listitem:
|
||||
- button "Edit shortcut for Rename session"
|
||||
- text: Rename session Ctrl + Shift + R
|
||||
- text: Rename session Ctrl + Alt + G
|
||||
- listitem:
|
||||
- button "Edit shortcut for Fork session"
|
||||
- text: Fork session Ctrl + Shift + F
|
||||
@@ -32,7 +32,7 @@
|
||||
- text: Archive session Ctrl + Alt + A
|
||||
- listitem:
|
||||
- button "Edit shortcut for Open settings"
|
||||
- text: Open settings Ctrl + ,
|
||||
- text: Open settings Ctrl + Alt + ,
|
||||
- listitem:
|
||||
- button "Edit shortcut for Toggle right sidebar"
|
||||
- text: Toggle right sidebar Ctrl + Shift + B
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- text: 添加工作区 ⌥ ⌘ O
|
||||
- listitem:
|
||||
- button "修改重命名会话快捷键"
|
||||
- text: 重命名会话 ⇧ ⌘ R
|
||||
- text: 重命名会话 ⌥ ⌘ G
|
||||
- listitem:
|
||||
- button "修改分叉会话快捷键"
|
||||
- text: 分叉会话 ⇧ ⌘ F
|
||||
@@ -32,7 +32,7 @@
|
||||
- text: 归档会话 ⌥ ⌘ A
|
||||
- listitem:
|
||||
- button "修改打开设置快捷键"
|
||||
- text: 打开设置 ⌘ ,
|
||||
- text: 打开设置 ⌥ ⌘ ,
|
||||
- listitem:
|
||||
- button "修改展开/收起右侧栏快捷键"
|
||||
- text: 展开/收起右侧栏 ⇧ ⌘ B
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
- button "meeting notes-this-is-a-very-long-filename-for-testing-user-message-file-references-and-preview-layout.md"
|
||||
- text: and confirm the fixture wiring {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: USER_INVOKE_REPLY acknowledged; following the injected skill.
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
- button "meeting notes-this-is-a-very-long-filename-for-testing-user-message-file-references-and-preview-layout.md"
|
||||
- text: and confirm the fixture wiring {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: USER_INVOKE_REPLY acknowledged; following the injected skill.
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
- button "Jump to turn 28"
|
||||
- text: m1 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r1
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -45,8 +45,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m2 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r2
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -54,8 +54,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m3 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r3
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -63,8 +63,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m4 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r4
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -72,8 +72,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m5 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r5
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -81,8 +81,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m6 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r6
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -90,8 +90,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m7 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r7
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -99,8 +99,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m8 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r8
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -108,8 +108,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m9 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r9
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -117,8 +117,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m10 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r10
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -126,8 +126,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m11 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r11
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -135,8 +135,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m12 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r12
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -144,8 +144,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m13 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r13
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -153,8 +153,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m14 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r14
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -162,8 +162,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m15 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r15
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -171,8 +171,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m16 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r16
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -180,8 +180,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m17 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r17
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -189,8 +189,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m18 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r18
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -198,8 +198,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m19 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r19
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -207,8 +207,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m20 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r20
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -216,8 +216,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m21 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r21
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -225,8 +225,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m22 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r22
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -234,8 +234,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m23 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r23
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -243,8 +243,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m24 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r24
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -252,8 +252,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m25 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r25
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -261,8 +261,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m26 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r26
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -270,8 +270,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m27 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r27
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
@@ -279,8 +279,8 @@
|
||||
- button "Branch into a new conversation"
|
||||
- text: 7/25 {{clock}} m28 7/25 {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- paragraph: r28
|
||||
- button "Copy"
|
||||
- button "Good response"
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
- button "Copy"
|
||||
- text: "Interjection: include the word ORANGE in your final reply."
|
||||
- button "Copy"
|
||||
- status: Deep diving...
|
||||
- text: Deep diving for {{duration}}...
|
||||
- status: Deep diving
|
||||
- text: Deep diving for {{duration}} ···
|
||||
- textbox "Message or run a task, / commands, @ files or sessions"
|
||||
- button "Add files or run commands"
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
- tab "Trajectory"
|
||||
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- button "Asked questions" [expanded]
|
||||
- button "Think The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that."
|
||||
- button "Ask question 1/1 answered"
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
- tab "Trajectory"
|
||||
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
|
||||
- button "Copy"
|
||||
- status: Worked
|
||||
- button "Took {{duration}}" [disabled]
|
||||
- status: Completed
|
||||
- button "Completed in {{duration}}" [disabled]
|
||||
- button "Asked questions"
|
||||
- text: "Interjection: include the word BANANA in your final reply. {{clock}}"
|
||||
- button "Copy"
|
||||
|
||||
@@ -155,6 +155,32 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () =
|
||||
expect(sessionEvents.flatMap(event =>
|
||||
event.type === 'request/header' ? [event.data.reason] : [])).toEqual(['initial', 'series'])
|
||||
await expect.poll(() => page.locator('[data-turn-process]').count(), { timeout: 15_000 }).toBe(2)
|
||||
const trigger = page.locator('[data-turn-trigger]').first()
|
||||
const process = page.locator('[data-turn-process]').first()
|
||||
const originalFontSize = await page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size'))
|
||||
await process.click()
|
||||
try {
|
||||
for (const fontSize of [10, 14, 22]) {
|
||||
await scaffold!.ctx.settings.update('ui-theme', { fontSize })
|
||||
await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size')))
|
||||
.toBe(`${String(fontSize)}px`)
|
||||
const group = page.locator('[data-process-activity]:visible').first()
|
||||
const groupFontSize = await group.evaluate(element => getComputedStyle(element).fontSize)
|
||||
expect(groupFontSize).toBe(`${String(fontSize)}px`)
|
||||
expect(await process.evaluate(element => getComputedStyle(element).fontSize)).toBe(groupFontSize)
|
||||
expect(await trigger.getByText('Continuing goal', { exact: true })
|
||||
.evaluate(element => getComputedStyle(element).fontSize)).toBe(groupFontSize)
|
||||
expect(await process.evaluate(element => element.getBoundingClientRect().top))
|
||||
.toBe(await trigger.evaluate(element => element.getBoundingClientRect().bottom) + 16)
|
||||
expect(await group.evaluate(element => element.getBoundingClientRect().top))
|
||||
.toBe(await process.evaluate(element => element.getBoundingClientRect().bottom) + 16)
|
||||
}
|
||||
} finally {
|
||||
await scaffold!.ctx.settings.update('ui-theme', { fontSize: Number.parseFloat(originalFontSize) })
|
||||
await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size')))
|
||||
.toBe(originalFontSize)
|
||||
await process.click()
|
||||
}
|
||||
expect(await page.getByRole('button', { name: 'System prompt' }).count()).toBe(0)
|
||||
expect(await page.locator(
|
||||
'[data-chat-flow-kind="system-prompt"]',
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
() => page.getByRole('status').filter({ hasText: 'Deep diving' }).isVisible(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(true)
|
||||
await page.locator('[data-streaming="true"]')
|
||||
await page.locator('[data-streaming="true"]:not([inert] *)')
|
||||
.getByText('partial', { exact: true })
|
||||
.waitFor({ timeout: 30_000 })
|
||||
const loadingSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
@@ -195,7 +195,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
// (awaited above), but the abort frame reaches the browser over SSE — the
|
||||
// frozen-partial swap is eventually consistent, so poll rather than count.
|
||||
await expect.poll(() => page.locator('[data-composer-input]').first().isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect.poll(() => page.locator('[data-streaming="true"]:not([inert] *)').count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect.poll(() => page.getByRole('tooltip').count()).toBe(0)
|
||||
// Golden of the aborted end-state: the prompt bubble plus the frozen
|
||||
// partial ('partial' is the hang entry's replayed prefix) and no more.
|
||||
@@ -222,7 +222,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
// AUTH is outside llm-retry's retryable set: no retry record.
|
||||
expect(sessionEvents.filter(e => e.type === 'llm/retry').length).toBe(0)
|
||||
await expect.poll(() => page.locator('[data-composer-input]').first().isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
expect(await page.locator('[data-streaming="true"]').count()).toBe(0)
|
||||
expect(await page.locator('[data-streaming="true"]:not([inert] *)').count()).toBe(0)
|
||||
const errorStatus = page.getByRole('status').filter({ hasText: 'This turn failed' })
|
||||
await errorStatus.waitFor({ timeout: 10_000 })
|
||||
expect(await errorStatus.textContent()).toContain('API key is invalid')
|
||||
@@ -292,6 +292,40 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('wraps an active retry label inside a narrow conversation column', async () => {
|
||||
await launch(() => ({ patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: 'upstream 503', code: 'SERVER' } }] }),
|
||||
{ mode: 'normal', maxRetries: 1, retryableCodes: ['SERVER'],
|
||||
backoff: { initialDelayMs: 60_000, maxDelayMs: 60_000, jitterRatio: 0 } })
|
||||
await scaffold!.ctx.settings.update('ui-theme', { fontSize: 17 })
|
||||
const { settled } = await sendPrompt(90_000)
|
||||
const retry = page.locator('details[data-active="true"]')
|
||||
await retry.waitFor()
|
||||
const originalStyle = await retry.getAttribute('style')
|
||||
try {
|
||||
const geometry = await retry.evaluate((element) => {
|
||||
element.style.width = '200px'
|
||||
const summary = element.querySelector('summary')!
|
||||
const status = summary.querySelector('[role="status"]')!
|
||||
return { width: summary.getBoundingClientRect().width,
|
||||
scrollWidth: summary.scrollWidth, clientWidth: summary.clientWidth,
|
||||
height: status.getBoundingClientRect().height,
|
||||
lineHeight: Number.parseFloat(getComputedStyle(status).lineHeight) }
|
||||
})
|
||||
expect(geometry.width).toBeLessThanOrEqual(200)
|
||||
expect(geometry.scrollWidth).toBe(geometry.clientWidth)
|
||||
expect(geometry.height).toBeGreaterThan(geometry.lineHeight)
|
||||
expect(await retry.getByRole('status').textContent()).toContain('Retrying')
|
||||
} finally {
|
||||
await retry.evaluate((element, style) => {
|
||||
if (style === null) element.removeAttribute('style')
|
||||
else element.setAttribute('style', style)
|
||||
}, originalStyle)
|
||||
await page.getByRole('button', { name: 'Stop generating', exact: true }).click()
|
||||
await settled
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('recovers a transient SERVER failure through llm-retry and completes', async () => {
|
||||
const derived = deriveReplayScript(parseSessionLog(await readFile(FIXTURE, 'utf8')))
|
||||
expect(derived).toHaveLength(1)
|
||||
@@ -366,7 +400,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
expect(turnEndReasons(sessionEvents).at(-1)).toBe('error')
|
||||
expect(sessionEvents.filter(e => e.type === 'llm/retry').length).toBe(2)
|
||||
await expect.poll(() => page.locator('[data-composer-input]').first().isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
expect(await page.locator('[data-streaming="true"]').count()).toBe(0)
|
||||
expect(await page.locator('[data-streaming="true"]:not([inert] *)').count()).toBe(0)
|
||||
// The terminal error row must render even though the turn owns a retry
|
||||
// chain: exhausted recovery shares the failing turn, so suppressing the
|
||||
// row by retry history would leave the failure invisible.
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
/** Built-profile browser coverage for sticky model groups; counts only their synchronous rect reads, not FPS or latency. */
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium, webkit } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/** Test-private count for MenuGroup sections and their direct scroll viewport. */
|
||||
__modelMenuRectReads: number
|
||||
}
|
||||
}
|
||||
|
||||
const OVERLAY = fileURLToPath(new URL('./default-model.overlay.yml', import.meta.url))
|
||||
const PROVIDERS = [
|
||||
{ id: 'origin-gateway', name: 'Origin Gateway', prefix: 'Origin' },
|
||||
{ id: 'amber-gateway', name: 'Amber Gateway', prefix: 'Amber' },
|
||||
{ id: 'cedar-gateway', name: 'Cedar Gateway', prefix: 'Cedar' },
|
||||
{ id: 'quartz-gateway', name: 'Quartz Gateway', prefix: 'Quartz' },
|
||||
].map((provider, providerIndex) => ({
|
||||
...provider,
|
||||
models: Array.from({ length: 7 }, (_, index) => providerIndex === 0 && index === 0
|
||||
? { id: 'origin-large', name: 'Origin Large' }
|
||||
: { id: `${provider.prefix.toLowerCase()}-model-${index + 1}`, name: `${provider.prefix} Model ${index + 1}` }),
|
||||
}))
|
||||
const EXPECTED_GROUPS = [
|
||||
{ label: 'DeepSeek', rows: ['DeepSeek-V4-Flash', 'DeepSeek-V4-Flash-Vision-Exp'] },
|
||||
...PROVIDERS.map(provider => ({ label: provider.name, rows: provider.models.map(model => model.name) })),
|
||||
]
|
||||
|
||||
// Native IO/RO callbacks need rendering opportunities, not elapsed-time sleeps.
|
||||
async function frames(page: Page): Promise<void> {
|
||||
await page.evaluate(() => new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => { requestAnimationFrame(() => { resolve() }) })
|
||||
}))
|
||||
}
|
||||
|
||||
function readGroups(viewport: Locator) {
|
||||
return viewport.locator('[data-menu-group]').evaluateAll(groups => groups.map(group => ({
|
||||
label: group.querySelector('[data-menu-group-heading]')!.textContent,
|
||||
rows: [...group.querySelectorAll('[role="option"], [role="menuitemradio"]')].map(row => row.textContent),
|
||||
})))
|
||||
}
|
||||
|
||||
function readPinned(viewport: Locator) {
|
||||
return viewport.locator('[data-menu-group-heading]').evaluateAll(headings =>
|
||||
headings.map(heading => heading.hasAttribute('data-stuck')))
|
||||
}
|
||||
|
||||
async function assertNoRectReads(page: Page): Promise<void> {
|
||||
await frames(page)
|
||||
expect(await page.evaluate(() => window.__modelMenuRectReads)).toBe(0)
|
||||
}
|
||||
|
||||
async function assertPinned(page: Page, viewport: Locator, index: number, count = 5): Promise<void> {
|
||||
const expected = Array.from({ length: count }, (_, i) => i === index)
|
||||
await expect.poll(() => readPinned(viewport)).toEqual(expected)
|
||||
await assertNoRectReads(page)
|
||||
expect(await readPinned(viewport)).toEqual(expected)
|
||||
}
|
||||
|
||||
async function scrollToGroup(viewport: Locator, index: number, inside = 1): Promise<void> {
|
||||
// Sections share an offset parent. Their normal-flow offsets are test positioning, not rect instrumentation.
|
||||
await viewport.evaluate((node, target) => new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
const groups = node.querySelectorAll<HTMLElement>(':scope > [data-menu-group]')
|
||||
node.scrollTop = groups[target.index]!.offsetTop - groups[0]!.offsetTop + target.inside
|
||||
resolve()
|
||||
})
|
||||
}), { index, inside })
|
||||
}
|
||||
|
||||
async function headingMaterial(viewport: Locator) {
|
||||
return viewport.locator('[data-menu-group-heading]').first().evaluate((heading) => {
|
||||
const body = heading.ownerDocument.body
|
||||
const theme = body.getAttribute('data-ds-dark-theme')
|
||||
try {
|
||||
return [false, true].map((dark) => {
|
||||
body.toggleAttribute('data-ds-dark-theme', dark)
|
||||
const style = getComputedStyle(heading)
|
||||
return {
|
||||
fill: style.backgroundColor, radius: style.borderRadius, position: style.position,
|
||||
font: style.fontSize, weight: style.fontWeight, padding: style.padding,
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
if (theme === null) body.removeAttribute('data-ds-dark-theme')
|
||||
else body.setAttribute('data-ds-dark-theme', theme)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe.skipIf(webSnapshotMode() === 'record').each([
|
||||
{ name: 'Chromium', engine: chromium },
|
||||
{ name: 'WebKit', engine: webkit },
|
||||
])('web e2e: model menu observers ($name)', ({ engine }) => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let crashed = false
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY })
|
||||
// Catalog-only settings traffic: no prompt, selection, replay fixture, or model request.
|
||||
await scaffold.ctx.settings.update('llm-pi-ai', {
|
||||
providers: Object.fromEntries(PROVIDERS.map(provider => [provider.id, {
|
||||
displayName: provider.name,
|
||||
api: 'openai-completions',
|
||||
baseURL: `https://${provider.id}.example/v1`,
|
||||
models: provider.models,
|
||||
}])),
|
||||
})
|
||||
browser = await engine.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
await page.addInitScript(() => {
|
||||
window.__modelMenuRectReads = 0
|
||||
// oxlint-disable-next-line typescript/unbound-method -- The wrapper supplies the original Element receiver below.
|
||||
const original = Element.prototype.getBoundingClientRect
|
||||
Element.prototype.getBoundingClientRect = function (): DOMRect {
|
||||
if (this.hasAttribute('data-menu-group') || [...this.children].some(child => child.hasAttribute('data-menu-group'))) {
|
||||
window.__modelMenuRectReads++
|
||||
}
|
||||
return original.call(this)
|
||||
}
|
||||
})
|
||||
page.on('crash', () => { crashed = true; console.error('observer test page crashed') })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await browser?.close()
|
||||
} finally {
|
||||
await scaffold?.close()
|
||||
}
|
||||
})
|
||||
|
||||
// FIXME: WebKit 26.5 crashes while filtering the grouped model rows in this scenario.
|
||||
it.skipIf(engine === webkit)('keeps both pickers ordered, sticky and keyboard-operable without synchronous group rect reads', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, `web-e2e-model-menu-observer-${engine.name()}`))
|
||||
let buttonMaterial: Awaited<ReturnType<typeof headingMaterial>> | undefined
|
||||
for (const entry of ['button', 'command'] as const) {
|
||||
const composer = page.locator('[data-composer-input]').first()
|
||||
const surface = entry === 'button'
|
||||
? page.getByRole('group', { name: '模型与推理等级', exact: true })
|
||||
: page.locator('[aria-label="/model 选项"]')
|
||||
const viewport = entry === 'button'
|
||||
? surface.getByRole('menu', { name: '模型', exact: true })
|
||||
: surface.getByRole('listbox')
|
||||
const search = entry === 'button'
|
||||
? page.getByRole('searchbox', { name: '搜索模型…', exact: true })
|
||||
: page.getByRole('textbox', { name: '筛选选项', exact: true })
|
||||
const open = async (): Promise<void> => {
|
||||
if (entry === 'button') {
|
||||
await page.getByRole('button', { name: /^选择模型/ }).click()
|
||||
await page.getByRole('menuitem', { name: /^模型/ }).click()
|
||||
} else {
|
||||
await page.getByRole('button', { name: '添加文件或调用指令', exact: true }).click()
|
||||
await page.getByRole('option', { name: /^模型/ }).click()
|
||||
}
|
||||
await expect.poll(() => readGroups(viewport)).toEqual(EXPECTED_GROUPS)
|
||||
await expect.poll(() => search.evaluate(node => node === node.ownerDocument.activeElement)).toBe(true)
|
||||
await assertNoRectReads(page)
|
||||
await page.mouse.move(0, 0)
|
||||
await viewport.evaluate((node) => { node.style.maxHeight = '80px'; node.scrollTop = 0 })
|
||||
await assertPinned(page, viewport, -1)
|
||||
}
|
||||
const close = async (): Promise<void> => {
|
||||
if (crashed || page.isClosed() || await surface.count() === 0) return
|
||||
await search.press('Escape')
|
||||
if (entry === 'button') await page.keyboard.press('Escape')
|
||||
await surface.waitFor({ state: 'detached' })
|
||||
await composer.fill('')
|
||||
}
|
||||
|
||||
await open()
|
||||
try {
|
||||
expect(await viewport.locator('[role="option"], [role="menuitemradio"]').count()).toBe(30)
|
||||
expect(await viewport.evaluate(node => node.scrollTop)).toBe(0)
|
||||
expect(await viewport.locator('[data-menu-group-heading]').evaluateAll(headings =>
|
||||
[...new Set(headings.map(heading => getComputedStyle(heading).backgroundColor))]))
|
||||
.toEqual(['rgba(0, 0, 0, 0)'])
|
||||
await scrollToGroup(viewport, 0)
|
||||
await assertPinned(page, viewport, 0)
|
||||
const material = await headingMaterial(viewport)
|
||||
expect(material.map(style => style.fill)).toEqual(['rgba(248, 249, 250, 0.94)', 'rgba(48, 49, 54, 0.94)'])
|
||||
for (const style of material) {
|
||||
expect(style).toMatchObject({ position: 'sticky', font: '11px', weight: '500', padding: '4px 7px 2px' })
|
||||
}
|
||||
if (entry === 'button') buttonMaterial = material
|
||||
else expect(material).toEqual(buttonMaterial)
|
||||
|
||||
for (const index of [2, 4, 1]) {
|
||||
await scrollToGroup(viewport, index)
|
||||
await assertPinned(page, viewport, index)
|
||||
const height = await viewport.evaluate(node => node.clientHeight)
|
||||
expect(height).toBeGreaterThan(0)
|
||||
expect(height).toBeLessThanOrEqual(80)
|
||||
expect(await viewport.locator('[data-menu-group]').nth(index).evaluate(node => node.clientHeight))
|
||||
.toBeGreaterThan(height * 2)
|
||||
await scrollToGroup(viewport, index, height + 1)
|
||||
await assertPinned(page, viewport, index)
|
||||
expect(await viewport.evaluate((node, groupIndex) => {
|
||||
const headings = node.querySelectorAll('[data-menu-group-heading]')
|
||||
const firstStart = node.querySelector('[data-menu-group-start]')!
|
||||
const viewportTop = firstStart.getBoundingClientRect().top + node.scrollTop
|
||||
return Math.abs(headings[groupIndex]!.getBoundingClientRect().top - viewportTop)
|
||||
}, index)).toBeLessThan(1)
|
||||
}
|
||||
await scrollToGroup(viewport, 0, 0)
|
||||
await assertPinned(page, viewport, -1)
|
||||
|
||||
for (const [key, steps] of [['ArrowDown', 18], ['ArrowUp', 10]] as const) {
|
||||
const visited = new Set<number>()
|
||||
let observedPinnedHighlight = false
|
||||
for (let step = 0; step < steps; step++) {
|
||||
const rows = viewport.locator('[role="option"], [role="menuitemradio"]')
|
||||
const before = await rows.evaluateAll(nodes => nodes.findIndex(node =>
|
||||
node.hasAttribute('data-highlighted') || node.getAttribute('aria-selected') === 'true'))
|
||||
await search.press(key)
|
||||
const expected = (before + (key === 'ArrowDown' ? 1 : -1) + 30) % 30
|
||||
await expect.poll(() => rows.evaluateAll(nodes => nodes.findIndex(node =>
|
||||
node.hasAttribute('data-highlighted') || node.getAttribute('aria-selected') === 'true'))).toBe(expected)
|
||||
await frames(page)
|
||||
await expect.poll(() => viewport.evaluate((node) => {
|
||||
const groups = [...node.querySelectorAll<HTMLElement>(':scope > [data-menu-group]')]
|
||||
const base = groups[0]!.offsetTop
|
||||
return groups.every((group) => {
|
||||
const top = group.offsetTop - base
|
||||
const shouldStick = top < node.scrollTop && top + group.offsetHeight > node.scrollTop
|
||||
return group.querySelector('[data-menu-group-heading]')!.hasAttribute('data-stuck') === shouldStick
|
||||
})
|
||||
})).toBe(true)
|
||||
const highlight = await viewport.evaluate((node) => {
|
||||
const groups = [...node.querySelectorAll<HTMLElement>(':scope > [data-menu-group]')]
|
||||
const index = groups.findIndex(group => group.querySelector('[data-highlighted], [aria-selected="true"]') !== null)
|
||||
const group = groups[index]!
|
||||
return { index, above: group.offsetTop - groups[0]!.offsetTop < node.scrollTop,
|
||||
stuck: group.querySelector('[data-menu-group-heading]')!.hasAttribute('data-stuck') }
|
||||
})
|
||||
visited.add(highlight.index)
|
||||
if (highlight.above) {
|
||||
expect(highlight.stuck).toBe(true)
|
||||
observedPinnedHighlight = true
|
||||
}
|
||||
expect(await search.evaluate(node => node === node.ownerDocument.activeElement)).toBe(true)
|
||||
await assertNoRectReads(page)
|
||||
}
|
||||
expect(visited.size).toBeGreaterThan(1)
|
||||
expect(observedPinnedHighlight).toBe(true)
|
||||
}
|
||||
|
||||
await scrollToGroup(viewport, 0)
|
||||
await assertPinned(page, viewport, 0)
|
||||
await search.fill('Quartz Model 1')
|
||||
await expect.poll(() => readGroups(viewport)).toEqual([{ label: 'Quartz Gateway', rows: ['Quartz Model 1'] }])
|
||||
await assertPinned(page, viewport, -1, 1)
|
||||
expect(await viewport.evaluate(node => node.scrollTop)).toBe(0)
|
||||
await search.fill('')
|
||||
await expect.poll(() => readGroups(viewport)).toEqual(EXPECTED_GROUPS)
|
||||
await assertPinned(page, viewport, -1)
|
||||
await close()
|
||||
await open()
|
||||
expect(await search.inputValue()).toBe('')
|
||||
|
||||
await scrollToGroup(viewport, 3, 90)
|
||||
await assertPinned(page, viewport, 3)
|
||||
await viewport.evaluate((node) => { node.style.maxHeight = '64px' })
|
||||
await expect.poll(() => viewport.evaluate(node => node.clientHeight)).toBeLessThanOrEqual(64)
|
||||
await assertPinned(page, viewport, 3)
|
||||
await scrollToGroup(viewport, 4, 100)
|
||||
await assertPinned(page, viewport, 4)
|
||||
await scrollToGroup(viewport, 1, 100)
|
||||
await assertPinned(page, viewport, 1)
|
||||
await scrollToGroup(viewport, 0, 0)
|
||||
await assertPinned(page, viewport, -1)
|
||||
} finally {
|
||||
await close()
|
||||
}
|
||||
}
|
||||
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'origin-gateway', model: 'origin-large' })
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -16,9 +16,9 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import {
|
||||
assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
|
||||
compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
@@ -264,6 +264,130 @@ describe('web e2e: plan review takeover round trip', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: pending plan review across Sidebar seat changes', () => {
|
||||
// Two recorded Sessions seeded side by side; opening one in the page gives it
|
||||
// a live Agent, so a review can be delivered to it while it is off screen.
|
||||
const OTHER_FIXTURE = fileURLToPath(new URL('../../../snapshots/web/fresh-round-trip/session.v3.jsonl', import.meta.url))
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let planned: SessionId
|
||||
let other: SessionId
|
||||
const crashes: string[] = []
|
||||
const row = (session: SessionId) => page.locator(`[data-row-key="session:${session}"]`)
|
||||
|
||||
async function showSessions(): Promise<void> {
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
// Expand the Workspace group by its state, not by whether its rows have rendered yet.
|
||||
const group = page.locator('[data-row-key^="workspace:"]').first()
|
||||
await group.waitFor({ timeout: 15_000 })
|
||||
if (await group.getAttribute('aria-expanded') !== 'true') await group.click()
|
||||
await row(planned).waitFor({ timeout: 15_000 })
|
||||
}
|
||||
|
||||
async function open(session: SessionId): Promise<void> {
|
||||
await row(session).click()
|
||||
await page.locator(`[data-conversation-session="${session}"]`).waitFor({ timeout: 15_000 })
|
||||
await page.locator(`[data-sidebar-right-session="${session}"]:not([hidden])`).first().waitFor({ state: 'attached', timeout: 15_000 })
|
||||
// `expect.poll` is test-scoped and this also runs from beforeAll, so poll by hand.
|
||||
const deadline = Date.now() + 15_000
|
||||
while (scaffold.ctx.agents.get(session) === undefined) {
|
||||
if (Date.now() > deadline) throw new Error(`opening Session ${session} published no live Agent`)
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
|
||||
async function showPlugins(): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Plugins', exact: true }).click()
|
||||
await expect.poll(() => page.locator('[data-composer-input]').count(), { timeout: 10_000 }).toBe(0)
|
||||
}
|
||||
|
||||
/** Deliver a plan review to one Session, run the scenario, and withdraw the review. */
|
||||
async function whileReviewing(session: SessionId, detail: string, scenario: () => Promise<void>): Promise<void> {
|
||||
const agent = scaffold.ctx.agents.get(session)
|
||||
if (agent === undefined) throw new Error(`Session ${session} has no live Agent`)
|
||||
const controller = new AbortController()
|
||||
const asked = scaffold.ctx.userQuestions.ask({
|
||||
agent, signal: controller.signal,
|
||||
questions: [{ id: 'seat-change', question: 'Approve this plan?', detail,
|
||||
options: [{ label: 'Approve' }, { label: 'Keep planning' }], intent: { kind: 'plan-review', approve: 'Approve' },
|
||||
}],
|
||||
})
|
||||
const outcome = asked.then(value => value, (error: unknown) => ({ error }))
|
||||
try {
|
||||
await row(session).locator('[data-state="warning"]').waitFor({ timeout: 10_000 })
|
||||
await scenario()
|
||||
} finally {
|
||||
controller.abort()
|
||||
await outcome
|
||||
}
|
||||
}
|
||||
|
||||
/** The review opened itself in the Sidebar and kept its manual opener. */
|
||||
async function expectOpened(text: string): Promise<void> {
|
||||
const card = page.locator('[data-plan-review-key]')
|
||||
await card.waitFor({ timeout: 10_000 })
|
||||
const preview = page.locator('[data-plan-preview^="dsh-resource://plan-review/"]')
|
||||
await preview.waitFor({ state: 'visible', timeout: 10_000 })
|
||||
expect(await preview.getByText(text).isVisible()).toBe(true)
|
||||
expect(await card.getByRole('button', { name: 'Open plan in sidebar' }).count()).toBe(1)
|
||||
expect(await page.locator('[data-slot-error]').count()).toBe(0)
|
||||
expect(crashes).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
planned = await seedSession(scaffold, await readFile(FIXTURE, 'utf8'), 'plan-review-seat-planned')
|
||||
other = await seedSession(scaffold, await readFile(OTHER_FIXTURE, 'utf8'), 'plan-review-seat-other')
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
page.on('console', (message: ConsoleMessage) => {
|
||||
if (message.type() === 'error' && /slot entry crashed/i.test(message.text())) crashes.push(message.text())
|
||||
})
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await showSessions()
|
||||
await open(planned)
|
||||
await open(other)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('opens a review that arrives while another Session is on screen', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plan-review-session-return'))
|
||||
// Returning mounts the review in the commit that brings its Session back on
|
||||
// screen and swaps the Sidebar seats; the review's automatic open runs
|
||||
// before either seat's effects.
|
||||
await whileReviewing(planned, '# Session review\n\nSubmitted while another Session was on screen.', async () => {
|
||||
await row(planned).click()
|
||||
await expectOpened('Submitted while another Session was on screen.')
|
||||
})
|
||||
})
|
||||
|
||||
it('opens a review for a Session whose Sidebar layout was never saved', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plan-review-first-layout'))
|
||||
// The layout is stored per browser. Without it the arriving seat creates
|
||||
// one, and that store commit lands in the commit where the review opens.
|
||||
await open(other)
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.evaluate((session) => { localStorage.removeItem(`dsh.sidebar-right.v1.${session}`) }, planned)
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await showSessions()
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await showPlugins()
|
||||
await whileReviewing(planned, '# Layout review\n\nSubmitted to a Session without a saved Sidebar layout.', async () => {
|
||||
await row(planned).click()
|
||||
await expectOpened('Submitted to a Session without a saved Sidebar layout.')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('web e2e: dismissed plan history', () => {
|
||||
it.skipIf(MODE === 'record')('reopens the permanent plan card after dismissing its review', async () => {
|
||||
// Replay supplies a finite continuation; the real question rejection and retained document are the assertions.
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium, type Locator, type Page } from 'playwright'
|
||||
import { chromium, type Locator, type Page, type Route } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import { launchWebScaffold, captureStableAria, compareOrRefreshGolden, webSnapshotMode, watchConsole, type WebScaffold } from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE } from './support.ts'
|
||||
@@ -87,6 +87,24 @@ it('cancels installation, retries and highlights the enabled plugin at 40% alpha
|
||||
`)
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
const tripwire = watchConsole(page)
|
||||
// Hold request delivery and the real Host's cancellation reply independently.
|
||||
// Cancellation reaches the Host before the install it names.
|
||||
const cancellationArrived = Promise.withResolvers<undefined>()
|
||||
let installBundleRoute = async (route: Route): Promise<void> => {
|
||||
await delivery.promise
|
||||
await route.continue()
|
||||
}
|
||||
let cancelInstallRoute = async (route: Route): Promise<void> => {
|
||||
const response = await route.fetch()
|
||||
cancellationArrived.resolve(undefined)
|
||||
await cancellationReply.promise
|
||||
await route.fulfill({ response })
|
||||
}
|
||||
// Host invalidations keep reading the directory between install phases.
|
||||
// Interception stays enabled from before navigation until page closure,
|
||||
// so phase changes cannot strand those requests.
|
||||
await page.route('**/api/pluginManager/installBundle', route => installBundleRoute(route))
|
||||
await page.route('**/api/pluginManager/cancelInstall', route => cancelInstallRoute(route))
|
||||
await page.clock.install()
|
||||
await page.goto(scaffold.authenticatedUrl)
|
||||
await page.waitForSelector('[class*="frame"]')
|
||||
@@ -120,19 +138,6 @@ it('cancels installation, retries and highlights the enabled plugin at 40% alpha
|
||||
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/plugin-install-cancel/invalid-registry.expected.md', import.meta.url)),
|
||||
`${invalidRegistryAria}\n\n${JSON.stringify({ registry: registryStyles, packageName: packageStyles }, null, 2)}`, webSnapshotMode())
|
||||
await packageName.fill('slow-package')
|
||||
// Hold request delivery and the real Host's cancellation reply independently.
|
||||
// Cancellation reaches the Host before the install it names.
|
||||
const cancellationArrived = Promise.withResolvers<undefined>()
|
||||
await page.route('**/api/pluginManager/installBundle', async (route) => {
|
||||
await delivery.promise
|
||||
await route.continue()
|
||||
})
|
||||
await page.route('**/api/pluginManager/cancelInstall', async (route) => {
|
||||
const response = await route.fetch()
|
||||
cancellationArrived.resolve(undefined)
|
||||
await cancellationReply.promise
|
||||
await route.fulfill({ response })
|
||||
})
|
||||
await dialog.getByRole('button', { name: '安装', exact: true }).click()
|
||||
await dialog.getByText('正在准备安装…', { exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/plugin-install-cancel/preparing.expected.md', import.meta.url)),
|
||||
@@ -156,10 +161,10 @@ it('cancels installation, retries and highlights the enabled plugin at 40% alpha
|
||||
await page.getByText('已取消安装,插件未启用,下载的文件可能保留', { exact: true }).waitFor()
|
||||
expect(await readFile(manifestPath, 'utf8')).toBe(manifest)
|
||||
expect(await readFile(lockPath, 'utf8')).toBe('original lockfile\n')
|
||||
await page.unrouteAll({ behavior: 'wait' })
|
||||
cancelInstallRoute = route => route.continue()
|
||||
// Drop the browser's response while the real Host still owns the child process.
|
||||
const activeReplySettled = Promise.withResolvers<undefined>()
|
||||
await page.route('**/api/pluginManager/installBundle', async (route) => {
|
||||
installBundleRoute = async (route) => {
|
||||
const response = route.fetch().then(() => undefined, (error: unknown) => error)
|
||||
try {
|
||||
await loseActiveReply.promise
|
||||
@@ -168,7 +173,7 @@ it('cancels installation, retries and highlights the enabled plugin at 40% alpha
|
||||
} finally {
|
||||
activeReplySettled.resolve(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
await panel.getByRole('button', { name: '添加插件', exact: true }).click()
|
||||
await dialog.getByRole('textbox').fill('slow-package')
|
||||
await dialog.getByRole('button', { name: '安装', exact: true }).click()
|
||||
@@ -186,7 +191,7 @@ it('cancels installation, retries and highlights the enabled plugin at 40% alpha
|
||||
expect(await readFile(lockPath, 'utf8')).toBe('original lockfile\n')
|
||||
expect(await dialog.getByRole('textbox').inputValue()).toBe('slow-package')
|
||||
await activeReplySettled.promise
|
||||
await page.unrouteAll({ behavior: 'wait' })
|
||||
installBundleRoute = route => route.continue()
|
||||
const snapshot = (await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd))
|
||||
.split(process.execPath).join('{{node}}')
|
||||
.split(scaffold.harnessHome).join('{{harnessHome}}')
|
||||
@@ -257,10 +262,10 @@ it('cancels installation, retries and highlights the enabled plugin at 40% alpha
|
||||
}
|
||||
await panel.getByRole('button', { name: '添加插件', exact: true }).click()
|
||||
await dialog.getByRole('textbox').fill('recovered-package')
|
||||
await page.route('**/api/pluginManager/installBundle', async (route) => {
|
||||
installBundleRoute = async (route) => {
|
||||
await route.fetch()
|
||||
await route.abort('failed')
|
||||
})
|
||||
}
|
||||
await dialog.getByRole('button', { name: '安装', exact: true }).click()
|
||||
await dialog.getByText('未能获取安装结果', { exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/plugin-install-cancel/unknown.expected.md', import.meta.url)),
|
||||
|
||||
@@ -122,6 +122,8 @@ describe('web e2e: requested SVG is explicitly delivered', () => {
|
||||
expect(await page.getByText('产物', { exact: true }).count()).toBe(0)
|
||||
// The scaffold workspace is not a git repository; the write call supplies its single changed file.
|
||||
const changes = page.locator('[data-changed-files]')
|
||||
// Changed-file cards wait for a separate summary request after the presentation events arrive.
|
||||
await changes.waitFor({ state: 'visible' })
|
||||
expect(await changes.count()).toBe(1)
|
||||
expect(await changes.getByText(`已编辑 ${FILE}`, { exact: true }).count()).toBe(1)
|
||||
expect(await changes.getByRole('list').count()).toBe(0)
|
||||
|
||||
@@ -15,6 +15,7 @@ import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { TIMED_WAIT_PARAMETER } from '@deepseek-ai/dsh-user-questions'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
@@ -33,8 +34,14 @@ const COMPOSED_EXPECTED = join(SNAPSHOT_DIR, 'composed.expected.md')
|
||||
const ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'answered.expected.md')
|
||||
const CANCELLED_EXPECTED = join(SNAPSHOT_DIR, 'cancelled.expected.md')
|
||||
const ANSWERED_EXPANDED_EXPECTED = join(SNAPSHOT_DIR, 'answered-expanded.expected.md')
|
||||
// The timed tool's second settlement: the wait expired, and the answer arrived
|
||||
// afterwards. Its golden owns the state the reported symptom lands in.
|
||||
const LATE_ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'late-answered.expected.md')
|
||||
const QUEUED_EXPECTED = join(SNAPSHOT_DIR, 'queued.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const CANCELLED_SEED_ID = 'ask-question-cancelled-row-web-e2e'
|
||||
const LATE_ANSWERED_SEED_ID = 'ask-question-late-answered-row-web-e2e'
|
||||
const QUEUED_SEED_ID = 'ask-question-queued-row-web-e2e'
|
||||
|
||||
// The composer's own growth cap, in text lines (QuestionComposer.module.css
|
||||
// .fieldMirror). Asserted as TEXT lines, not as a box height: the two variants
|
||||
@@ -70,8 +77,100 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/** Replace the successful Tool settlement and omit the answer-dependent model step. */
|
||||
function cancelledFixture(fixture: string): string {
|
||||
/** The batch the derived late reply submits, distinct from the in-time answer. */
|
||||
const LATE_ANSWERS = [{ id: 'color', selected: ['Green'], custom: 'Answered after the timeout' }]
|
||||
|
||||
/**
|
||||
* The `ask_user_question` schema as the timed tool declares it, in the shape a
|
||||
* request header records. A late reply can only follow a timed call, and the
|
||||
* `userQuestions` projection tells a timed call from a legacy one by exactly
|
||||
* this schema in the header in effect; the recorded fixture's header carries
|
||||
* the placeholder token of a legacy recording, so the derived fixture must
|
||||
* carry the timed schema for its call to be tracked at all.
|
||||
*/
|
||||
const TIMED_ASK_USER_QUESTION_SCHEMA = {
|
||||
name: 'ask_user_question',
|
||||
description: 'Ask brief, direct, self-contained questions about missing information, preferences, or decisions.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
questions: { type: 'array', items: { type: 'object' } },
|
||||
[TIMED_WAIT_PARAMETER]: { type: 'integer' },
|
||||
},
|
||||
required: ['questions'],
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* What the timed tool records when its foreground wait expires. Both readers of
|
||||
* a recorded result take `pending` and `callId` out of this one JSON object —
|
||||
* the `userQuestions` projection to keep the call answerable, the question row
|
||||
* to tell a timeout from an answer batch; the `message` is the model's own
|
||||
* continuation instruction and no reader parses it.
|
||||
* @param callId - the call whose questions stay answerable.
|
||||
* @returns the recorded result text.
|
||||
*/
|
||||
function pendingResultText(callId: string): string {
|
||||
return JSON.stringify({
|
||||
pending: true,
|
||||
callId,
|
||||
message: 'No answer batch arrived before the timeout. This is pending, not a skipped answer.',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The message a late answer lands as: the timed question's `callId` names it,
|
||||
* and it carries both the questions it answers and the batch itself.
|
||||
* @param callId - the call the reply settles.
|
||||
* @param questions - the call's questions, echoed from its recorded arguments.
|
||||
* @returns the reply, as the inbox splice and the durable message both record it.
|
||||
*/
|
||||
function lateReplyMessage(callId: string, questions: unknown): Record<string, unknown> {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
kind: 'answer_to_pending_question', tool: 'ask_user_question', callId, questions, answers: LATE_ANSWERS,
|
||||
}),
|
||||
}],
|
||||
source: { kind: 'user-question-reply', callId, outcome: 'answered' },
|
||||
role: 'user',
|
||||
id: '{{message:7}}',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The turn a late reply opens: the reply enters the agent inbox, the next turn
|
||||
* claims it, and it lands as this Session's durable user message.
|
||||
* @param callId - the call the reply settles.
|
||||
* @param questions - the call's questions, echoed from its recorded arguments.
|
||||
* @returns the appended fixture lines, ending in the closing `turn/end`.
|
||||
*/
|
||||
function lateReplyTail(callId: string, questions: unknown): string[] {
|
||||
const message = lateReplyMessage(callId, questions)
|
||||
return [
|
||||
{ type: 'agent/inbox/spliced', data: { target: 'next-turn', start: 0, inserted: [message] } },
|
||||
{ type: 'turn/start', data: { turn: 2 } },
|
||||
{ type: 'agent/inbox/spliced', data: { target: 'next-turn', start: 0, removedCount: 1, inserted: [] } },
|
||||
{ type: 'step/start', data: { turn: 2, step: 1 } },
|
||||
{ type: 'user/message', data: message, surfaceOp: 'append' },
|
||||
{ type: 'step/end', data: { turn: 2, step: 1 } },
|
||||
{ type: 'turn/end', data: { turn: 2, reason: { kind: 'completed' } } },
|
||||
].map(event => JSON.stringify(event))
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive one settlement other than the recorded answer batch. The model step
|
||||
* that follows the Tool settlement reads the answer, so every derivation drops
|
||||
* it and keeps the recorded turn closed.
|
||||
* @param fixture - the recorded round trip.
|
||||
* @param outcome - `cancelled` settles the call as the user's own dismissal;
|
||||
* `queued` and `late-answered` settle the expired wait and append its reply
|
||||
* before or after the next turn admits it.
|
||||
* @returns the derived fixture text.
|
||||
*/
|
||||
function resettledFixture(fixture: string, outcome: 'cancelled' | 'queued' | 'late-answered'): string {
|
||||
let asked: { callId: string; questions: unknown } | undefined
|
||||
let replaced = false
|
||||
const lines: string[] = []
|
||||
for (const line of fixture.trimEnd().split('\n')) {
|
||||
@@ -90,6 +189,26 @@ function cancelledFixture(fixture: string): string {
|
||||
|| event.type === 'turn/end') lines.push(line)
|
||||
continue
|
||||
}
|
||||
if (event.type === 'request/header' && outcome !== 'cancelled') {
|
||||
// The recording was made under the blocking legacy tool; the derived
|
||||
// timed settlement needs the header that tool's timed sibling records.
|
||||
const data = event.data
|
||||
if (!isRecord(data) || !isRecord(data.header)) throw new Error('question fixture request/header is invalid')
|
||||
data.header.tools = [TIMED_ASK_USER_QUESTION_SCHEMA]
|
||||
lines.push(JSON.stringify(event))
|
||||
continue
|
||||
}
|
||||
if (event.type === 'tool/call') {
|
||||
const data = event.data
|
||||
if (!isRecord(data) || typeof data.callId !== 'string' || typeof data.arguments !== 'string') {
|
||||
throw new Error('question fixture tool/call is invalid')
|
||||
}
|
||||
const args: unknown = JSON.parse(data.arguments)
|
||||
if (!isRecord(args)) throw new Error('question fixture tool/call arguments are invalid')
|
||||
asked = { callId: data.callId, questions: args.questions }
|
||||
lines.push(line)
|
||||
continue
|
||||
}
|
||||
if (event.type !== 'tool/result') {
|
||||
lines.push(line)
|
||||
continue
|
||||
@@ -100,20 +219,70 @@ function cancelledFixture(fixture: string): string {
|
||||
if (!isRecord(message) || !Array.isArray(message.content) || !isRecord(message.content[0])) {
|
||||
throw new Error('question fixture tool/result message is invalid')
|
||||
}
|
||||
message.content[0].content = [{
|
||||
type: 'text',
|
||||
text: 'Error: the user cancelled ask_user_question',
|
||||
}]
|
||||
message.content[0].isError = true
|
||||
data.error = {
|
||||
name: 'UserQuestionError',
|
||||
code: 'ASK_CANCELLED',
|
||||
if (asked === undefined) throw new Error('question fixture settles a call it never made')
|
||||
if (outcome === 'cancelled') {
|
||||
message.content[0].content = [{
|
||||
type: 'text',
|
||||
text: 'Error: the user cancelled ask_user_question',
|
||||
}]
|
||||
message.content[0].isError = true
|
||||
data.error = {
|
||||
name: 'UserQuestionError',
|
||||
code: 'ASK_CANCELLED',
|
||||
}
|
||||
} else {
|
||||
message.content[0].content = [{ type: 'text', text: pendingResultText(asked.callId) }]
|
||||
}
|
||||
replaced = true
|
||||
lines.push(JSON.stringify(event))
|
||||
}
|
||||
if (!replaced) throw new Error('question fixture has no tool/result event')
|
||||
return `${lines.join('\n')}\n`
|
||||
if (!replaced || asked === undefined) throw new Error('question fixture has no settled ask_user_question call')
|
||||
if (outcome === 'queued') {
|
||||
const end = lines.pop()
|
||||
const final: unknown = end === undefined ? undefined : JSON.parse(end)
|
||||
if (!isRecord(final) || final.type !== 'turn/end') throw new Error('question fixture does not end its first turn')
|
||||
return `${[...lines, lateReplyTail(asked.callId, asked.questions)[0], end].join('\n')}\n`
|
||||
}
|
||||
const tail = outcome === 'cancelled' ? [] : lateReplyTail(asked.callId, asked.questions)
|
||||
return `${[...lines, ...tail].join('\n')}\n`
|
||||
}
|
||||
|
||||
/** One derived transcript, seeded into its own scaffold and opened in the browser. */
|
||||
interface SeededTranscript {
|
||||
readonly scaffold: WebScaffold
|
||||
readonly browser: Browser
|
||||
readonly page: Page
|
||||
readonly tripwire: ReturnType<typeof watchConsole>
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed one derived fixture and open its Session in a fresh browser.
|
||||
* @param fixtureText - the derived fixture to persist.
|
||||
* @param seedId - the seeded Session id, which its golden tokenizes.
|
||||
* @returns the scaffold, browser, page, and console tripwire teardown closes.
|
||||
*/
|
||||
async function openSeededTranscript(fixtureText: string, seedId: string): Promise<SeededTranscript> {
|
||||
const scaffold = await launchWebScaffold({})
|
||||
let browser: Browser | undefined
|
||||
try {
|
||||
await seedSession(scaffold, fixtureText, seedId)
|
||||
browser = await chromium.launch()
|
||||
const page = await newEnglishPage(browser)
|
||||
const tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
const groupRow = page.locator('[role="treeitem"]').first()
|
||||
await groupRow.waitFor({ timeout: 15_000 })
|
||||
await groupRow.click()
|
||||
const sessionRow = page.locator('[role="treeitem"]').nth(1)
|
||||
await sessionRow.waitFor({ timeout: 10_000 })
|
||||
await sessionRow.click()
|
||||
return { scaffold, browser, page, tripwire }
|
||||
} catch (error) {
|
||||
await Promise.allSettled([browser?.close(), scaffold.close()])
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
describe('web e2e: resident question composer round trip', () => {
|
||||
@@ -252,6 +421,15 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
expect(await blue.getAttribute('aria-checked')).toBe('true')
|
||||
expect(await custom.inputValue()).toBe('Include accessibility notes')
|
||||
if (MODE !== 'record') {
|
||||
// Replay only, like the measurements above: record mode must reach the
|
||||
// recording write. This fixture composes the shipped legacy tool, whose
|
||||
// request carries no call id and whose row offers no pill: the projection
|
||||
// never lists a legacy call, so the transcript reads exactly as it did
|
||||
// before timed questions existed. Reopening a hidden panel belongs to the
|
||||
// timed tool; the late-answered scenario below and the ui-user-questions
|
||||
// unit specs cover it.
|
||||
expect(await page.getByRole('button', { name: 'Answer', exact: true }).count()).toBe(0)
|
||||
|
||||
// A strict Session-slot switch remounts the composer. Open a fresh blank
|
||||
// Session, then return to the still-waiting request and require its
|
||||
// Session-scoped store to restore both option and free-text drafts.
|
||||
@@ -302,12 +480,15 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
// Keep the ask_user_question card's readable answer in the expanded golden
|
||||
// even though Compact mode hides the process by default.
|
||||
await expandTurnProcesses(page)
|
||||
// A legacy call's answered row carries no pill and no read-only panel; its
|
||||
// accessible name is the bare summary, as before timed questions existed.
|
||||
const answeredRow = page.getByRole('button', { name: 'Ask question 1/1 answered', exact: true })
|
||||
await answeredRow.click()
|
||||
await page.getByText('Which color do you prefer?', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.getByText('Blue', { exact: true }).count()).toBeGreaterThanOrEqual(1)
|
||||
expect(await page.getByText('Include accessibility notes', { exact: true }).count()).toBe(1)
|
||||
expect(await page.getByText(/"answers"/).count()).toBe(0)
|
||||
expect(await page.getByRole('button', { name: 'View answers', exact: true }).count()).toBe(0)
|
||||
const expanded = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(ANSWERED_EXPANDED_EXPECTED, expanded, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
@@ -358,38 +539,22 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
})
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', () => {
|
||||
let cancelledScaffold: WebScaffold
|
||||
let cancelledBrowser: Browser
|
||||
let cancelledPage: Page
|
||||
let cancelledTripwire: ReturnType<typeof watchConsole>
|
||||
let seeded: SeededTranscript
|
||||
|
||||
beforeAll(async () => {
|
||||
cancelledScaffold = await launchWebScaffold({})
|
||||
await seedSession(
|
||||
cancelledScaffold,
|
||||
cancelledFixture(await readFile(FIXTURE, 'utf8')),
|
||||
seeded = await openSeededTranscript(
|
||||
resettledFixture(await readFile(FIXTURE, 'utf8'), 'cancelled'),
|
||||
CANCELLED_SEED_ID,
|
||||
)
|
||||
cancelledBrowser = await chromium.launch()
|
||||
cancelledPage = await newEnglishPage(cancelledBrowser)
|
||||
cancelledTripwire = watchConsole(cancelledPage)
|
||||
await cancelledPage.goto(cancelledScaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await cancelledPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
|
||||
const groupRow = cancelledPage.locator('[role="treeitem"]').first()
|
||||
await groupRow.waitFor({ timeout: 15_000 })
|
||||
await groupRow.click()
|
||||
const sessionRow = cancelledPage.locator('[role="treeitem"]').nth(1)
|
||||
await sessionRow.waitFor({ timeout: 10_000 })
|
||||
await sessionRow.click()
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await cancelledBrowser?.close()
|
||||
await cancelledScaffold?.close()
|
||||
await seeded?.browser.close()
|
||||
await seeded?.scaffold.close()
|
||||
})
|
||||
|
||||
it('expands to the cancellation verdict and original questions', async () => {
|
||||
const { page: cancelledPage, scaffold, tripwire } = seeded
|
||||
onTestFailed(() => saveFailureShot(cancelledPage, 'web-e2e-question-cancelled-row'))
|
||||
await expandTurnProcesses(cancelledPage)
|
||||
const row = cancelledPage.getByRole('button', { name: 'Ask question cancelled', exact: true })
|
||||
@@ -407,11 +572,118 @@ describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', ()
|
||||
const snapshot = (await captureStableAria(
|
||||
cancelledPage,
|
||||
'[class*="centerCol"]',
|
||||
cancelledScaffold.workspaceCwd,
|
||||
scaffold.workspaceCwd,
|
||||
)).split(CANCELLED_SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(CANCELLED_EXPECTED, snapshot, MODE)
|
||||
expect(cancelledTripwire.pageErrors).toEqual([])
|
||||
expect(cancelledTripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 60_000)
|
||||
})
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: queued question reply', () => {
|
||||
let seeded: SeededTranscript
|
||||
|
||||
beforeAll(async () => {
|
||||
seeded = await openSeededTranscript(
|
||||
resettledFixture(await readFile(FIXTURE, 'utf8'), 'queued'),
|
||||
QUEUED_SEED_ID,
|
||||
)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await seeded?.browser.close()
|
||||
await seeded?.scaffold.close()
|
||||
})
|
||||
|
||||
it('keeps a submitted reply read-only across a browser reload', async () => {
|
||||
const { page, scaffold, tripwire } = seeded
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-question-queued-row'))
|
||||
await expandTurnProcesses(page)
|
||||
const row = page.locator('[data-tool="ask_user_question"]')
|
||||
await row.waitFor()
|
||||
await page.getByRole('button', { name: 'View answers', exact: true }).waitFor()
|
||||
expect(await page.locator('[data-question-key]').count()).toBe(0)
|
||||
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await expandTurnProcesses(page)
|
||||
await row.waitFor()
|
||||
await page.getByRole('button', { name: 'View answers', exact: true }).waitFor()
|
||||
expect(await page.locator('[data-question-key]').count()).toBe(0)
|
||||
await page.getByRole('button', { name: 'View answers', exact: true }).click()
|
||||
await page.getByText('Answered after the timeout', { exact: true }).waitFor()
|
||||
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
.split(QUEUED_SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(QUEUED_EXPECTED, snapshot, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 60_000)
|
||||
})
|
||||
|
||||
// The timed tool's own second settlement. The recorded result is the expired
|
||||
// wait, so the answers live in the reply that landed a turn later and reach the
|
||||
// row only through the `userQuestions` projection — the whole chain the reported
|
||||
// symptom broke on, where an answered question kept no way back to its answers.
|
||||
describe.skipIf(MODE === 'record')('web e2e: late-answered question transcript', () => {
|
||||
let seeded: SeededTranscript
|
||||
|
||||
beforeAll(async () => {
|
||||
seeded = await openSeededTranscript(
|
||||
resettledFixture(await readFile(FIXTURE, 'utf8'), 'late-answered'),
|
||||
LATE_ANSWERED_SEED_ID,
|
||||
)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await seeded?.browser.close()
|
||||
await seeded?.scaffold.close()
|
||||
})
|
||||
|
||||
it('reads a late answer as an answered question and reopens its answers', async () => {
|
||||
const { page: latePage, scaffold, tripwire } = seeded
|
||||
onTestFailed(() => saveFailureShot(latePage, 'web-e2e-question-late-answered-row'))
|
||||
const reply = latePage.locator('[data-question-reply]')
|
||||
await reply.waitFor({ state: 'attached', timeout: 15_000 })
|
||||
expect(await reply.count()).toBe(1)
|
||||
expect(await latePage.locator('[data-chat-flow-kind="turn-trigger"]').count()).toBe(0)
|
||||
expect(await reply.isVisible()).toBe(false)
|
||||
// Whether the answer beat the timeout is the agent's pacing, not something
|
||||
// a reader of the row has to reason about.
|
||||
await expandTurnProcesses(latePage)
|
||||
await reply.waitFor({ state: 'visible', timeout: 15_000 })
|
||||
const row = latePage.locator('[data-tool="ask_user_question"]')
|
||||
await row.waitFor({ timeout: 15_000 })
|
||||
await row.getByRole('button', { name: 'Ask question', exact: true }).click()
|
||||
await latePage.getByText('Which color do you prefer?', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
expect(await latePage.getByText('Green', { exact: true }).count()).toBeGreaterThanOrEqual(1)
|
||||
expect(await latePage.getByText('Answered after the timeout', { exact: true }).count()).toBeGreaterThanOrEqual(1)
|
||||
// The recorded timeout is the row's state, never its transcript copy.
|
||||
expect(await latePage.getByText(/"pending"/).count()).toBe(0)
|
||||
|
||||
// The same read-only panel an in-time answer reopens, over the answers the
|
||||
// late reply carried.
|
||||
await latePage.getByRole('button', { name: 'View answers', exact: true }).click()
|
||||
const review = latePage.locator('[data-question-key]')
|
||||
await review.waitFor({ timeout: 10_000 })
|
||||
await review.getByText('Answered', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
const recorded = review.getByRole('checkbox', { name: 'Green' })
|
||||
expect(await recorded.getAttribute('aria-checked')).toBe('true')
|
||||
expect(await recorded.isDisabled()).toBe(true)
|
||||
expect(await review.getByRole('checkbox', { name: 'Blue' }).getAttribute('aria-checked')).toBe('false')
|
||||
const recordedCustom = review.getByRole('textbox')
|
||||
expect(await recordedCustom.inputValue()).toBe('Answered after the timeout')
|
||||
expect(await recordedCustom.isDisabled()).toBe(true)
|
||||
expect(await review.getByRole('button', { name: 'Submit' }).count()).toBe(0)
|
||||
expect(await review.getByRole('button', { name: 'Skip' }).count()).toBe(0)
|
||||
|
||||
const snapshot = (await captureStableAria(
|
||||
latePage,
|
||||
'[class*="centerCol"]',
|
||||
scaffold.workspaceCwd,
|
||||
)).split(LATE_ANSWERED_SEED_ID).join('{{seededId}}')
|
||||
await compareOrRefreshGolden(LATE_ANSWERED_EXPECTED, snapshot, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('keeps the fixture inventory closed', async () => {
|
||||
@@ -423,6 +695,8 @@ describe.skipIf(MODE === 'record')('web e2e: cancelled question transcript', ()
|
||||
'answered.expected.md',
|
||||
'cancelled.expected.md',
|
||||
'answered-expanded.expected.md',
|
||||
'late-answered.expected.md',
|
||||
'queued.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -372,7 +372,6 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
const processBottom = await process.evaluate(element => element.getBoundingClientRect().bottom)
|
||||
const answerTop = await page.getByText('DONE', { exact: true }).evaluate(element =>
|
||||
element.getBoundingClientRect().top)
|
||||
// Collapsed control row keeps its own 8px margin plus the 8px flow gap.
|
||||
expect(answerTop).toBe(processBottom + 16)
|
||||
expect(await page.getByText('Context compacted', { exact: true }).count()).toBe(0)
|
||||
// Tool cards render from logged tool/call + tool/result alone (views are
|
||||
|
||||
@@ -76,6 +76,11 @@ it.each([
|
||||
if (await group.getAttribute('aria-expanded') !== 'true') await group.click()
|
||||
await page.getByRole('treeitem').nth(1).click()
|
||||
await page.getByText('DONE', { exact: true }).waitFor()
|
||||
const renameDialog = page.getByRole('dialog', { name: 'Rename session', exact: true })
|
||||
await deliverPrimary('KeyG', true)
|
||||
await renameDialog.getByLabel('Session name').waitFor()
|
||||
await page.keyboard.press('Escape')
|
||||
await renameDialog.waitFor({ state: 'hidden' })
|
||||
const composer = page.locator('[data-composer-input]').first()
|
||||
await composer.focus()
|
||||
await page.keyboard.insertText('Desktop focus draft')
|
||||
@@ -272,6 +277,8 @@ it.each([
|
||||
} satisfies DesktopShortcutInput)
|
||||
await page.getByText('DONE', { exact: true }).waitFor({ state: 'hidden' })
|
||||
await expect.poll(() => page.getByRole('treeitem', { selected: true }).count()).toBe(0)
|
||||
await deliverPrimary('KeyG', true)
|
||||
expect(await renameDialog.count()).toBe(0)
|
||||
await page.getByRole('treeitem').nth(1).click()
|
||||
await page.getByText('DONE', { exact: true }).waitFor()
|
||||
await openReference()
|
||||
|
||||
@@ -249,6 +249,29 @@ it.skipIf(mode === 'record').each([
|
||||
if (await group.getAttribute('aria-expanded') !== 'true') await group.click()
|
||||
await page.getByRole('treeitem').nth(1).click()
|
||||
await page.getByText('DONE', { exact: true }).waitFor()
|
||||
// Synthetic input observes browser ownership without navigating the fixture away.
|
||||
expect(await page.evaluate((primary) => {
|
||||
return [{ key: 'R', code: 'KeyR', shiftKey: true }, { key: ',', code: 'Comma', shiftKey: false }].map((key) => {
|
||||
const event = new KeyboardEvent('keydown', { ...key,
|
||||
metaKey: primary === 'Meta', ctrlKey: primary === 'Control', bubbles: true, cancelable: true })
|
||||
document.activeElement!.dispatchEvent(event)
|
||||
return event.defaultPrevented
|
||||
})
|
||||
}, primary)).toEqual([false, false])
|
||||
expect(await page.getByRole('dialog').count()).toBe(0)
|
||||
const rename = page.getByRole('dialog', { name: 'Rename session', exact: true })
|
||||
expect(await rename.count()).toBe(0)
|
||||
await page.keyboard.press(`${primary}+Alt+G`)
|
||||
await rename.getByLabel('Session name').waitFor()
|
||||
await page.keyboard.press('Escape')
|
||||
await rename.waitFor({ state: 'hidden' })
|
||||
await page.keyboard.press(`${primary}+Alt+Comma`)
|
||||
const settings = page.getByRole('dialog', { name: 'Settings', exact: true })
|
||||
await settings.waitFor()
|
||||
await page.keyboard.press(`${primary}+Alt+G`)
|
||||
expect(await rename.count()).toBe(0)
|
||||
await page.keyboard.press('Escape')
|
||||
await settings.waitFor({ state: 'hidden' })
|
||||
await page.keyboard.press(`${primary}+Slash`)
|
||||
const reference = page.getByRole('dialog', { name: 'Keyboard shortcuts', exact: true })
|
||||
await reference.waitFor()
|
||||
@@ -312,6 +335,8 @@ it.skipIf(mode === 'record').each([
|
||||
await page.getByText('DONE', { exact: true }).waitFor({ state: 'hidden' })
|
||||
await expect.poll(() => page.getByRole('treeitem', { selected: true }).count()).toBe(0)
|
||||
await page.locator('[data-composer-input]').first().waitFor()
|
||||
await page.keyboard.press(`${primary}+Alt+G`)
|
||||
expect(await rename.count()).toBe(0)
|
||||
expect(console.pageErrors).toEqual([])
|
||||
} finally {
|
||||
await browser?.close()
|
||||
|
||||
@@ -32,8 +32,8 @@ describe('web e2e: shortcut reference', () => {
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
const trigger = page.getByRole('button', { name: settings, exact: true })
|
||||
expect(await trigger.getAttribute('aria-keyshortcuts')).toBe(`${key}+,`)
|
||||
const settingsHint = `${settings} ${key === 'Meta' ? '⌘ ,' : 'Ctrl + ,'}`
|
||||
expect(await trigger.getAttribute('aria-keyshortcuts')).toBe(key === 'Meta' ? 'Alt+Meta+,' : 'Control+Alt+,')
|
||||
const settingsHint = `${settings} ${key === 'Meta' ? '⌥ ⌘ ,' : 'Ctrl + Alt + ,'}`
|
||||
await trigger.hover()
|
||||
await page.getByRole('tooltip', { name: settingsHint, exact: true }).waitFor()
|
||||
await trigger.click()
|
||||
@@ -193,7 +193,7 @@ describe('web e2e: shortcut reference', () => {
|
||||
expect(await dialog.getByRole('listitem').count()).toBe(1)
|
||||
expect(await dialog.getByRole('listitem').textContent()).toContain(locale === 'zh-CN' ? '展开/收起左侧栏' : 'Toggle left sidebar')
|
||||
}
|
||||
await page.keyboard.press(`${key}+Comma`)
|
||||
await page.keyboard.press(`${key}+Alt+Comma`)
|
||||
expect(await referenceNode!.evaluate(element => element.isConnected)).toBe(true)
|
||||
expect(await page.getByRole('dialog').count()).toBe(2)
|
||||
expect(await page.getByRole('searchbox').inputValue()).toBe('toggle left sidebar')
|
||||
@@ -205,7 +205,7 @@ describe('web e2e: shortcut reference', () => {
|
||||
expect(await page.getByRole('button', { name: view, exact: true }).evaluate(element => element === document.activeElement)).toBe(true)
|
||||
expect(await page.getByRole('button', { name: view, exact: true }).getAttribute('data-dsh-automatic-focus')).toBe('')
|
||||
expect(await page.getByRole('button', { name: view, exact: true }).evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none')
|
||||
await page.keyboard.press(`${key}+Comma`)
|
||||
await page.keyboard.press(`${key}+Alt+Comma`)
|
||||
await page.getByRole('dialog', { name: settings, exact: true }).waitFor({ state: 'hidden' })
|
||||
expect(await page.getByRole('dialog').count()).toBe(0)
|
||||
expect(await trigger.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
@@ -213,7 +213,7 @@ describe('web e2e: shortcut reference', () => {
|
||||
expect(await trigger.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none')
|
||||
await page.keyboard.press(referenceKey)
|
||||
await dialog.waitFor()
|
||||
await page.keyboard.press(`${key}+Comma`)
|
||||
await page.keyboard.press(`${key}+Alt+Comma`)
|
||||
expect(await page.getByRole('dialog', { name: settings, exact: true }).isVisible()).toBe(false)
|
||||
expect(await page.getByRole('dialog').count()).toBe(1)
|
||||
expect(await searchInput.evaluate(element => element === document.activeElement)).toBe(true)
|
||||
@@ -308,7 +308,7 @@ describe('web e2e: shortcut reference', () => {
|
||||
await dialog.getByRole('button', { name: locale === 'zh-CN' ? '恢复默认' : 'Restore default', exact: true }).click()
|
||||
await dialog.getByRole('group').waitFor({ state: 'hidden' })
|
||||
await page.keyboard.press('Escape')
|
||||
expect(await trigger.getAttribute('aria-keyshortcuts')).toBe(`${key}+,`)
|
||||
expect(await trigger.getAttribute('aria-keyshortcuts')).toBe(key === 'Meta' ? 'Alt+Meta+,' : 'Control+Alt+,')
|
||||
await trigger.hover()
|
||||
await page.getByRole('tooltip', { name: settingsHint, exact: true }).waitFor()
|
||||
const sidebar = page.getByRole('button', { name: locale === 'zh-CN' ? '收起侧边栏' : 'Collapse sidebar', exact: true })
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
- button "Wrap lines" [pressed]
|
||||
- button "Copy"
|
||||
- code: "const first: number = 1 const second = \"two\" let tail"
|
||||
- status: Deep diving...
|
||||
- text: Deep diving for {{duration}}...
|
||||
- status: Deep diving
|
||||
- text: Deep diving for {{duration}} ···
|
||||
- textbox "Message or run a task, / commands, @ files or sessions"
|
||||
- button "Add files or run commands"
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
|
||||
@@ -136,7 +136,7 @@ export async function expandTurnProcesses(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the Turn process and secondary group containing a hidden descendant.
|
||||
* Expand the settled Turn process and secondary group containing a hidden descendant.
|
||||
* @param page - page containing the Chat view.
|
||||
* @param target - descendant whose outer process disclosures should open.
|
||||
*/
|
||||
@@ -145,7 +145,10 @@ export async function expandOwningTurnProcess(page: Page, target: Locator): Prom
|
||||
const turn = await target.evaluate(element => element.closest<HTMLElement>('[data-chat-turn]')?.dataset.chatTurn)
|
||||
if (turn !== undefined) {
|
||||
const control = page.locator(`[data-turn-process="${turn}"]`)
|
||||
if (await control.count() > 0 && await control.getAttribute('aria-expanded') === 'false') await control.click()
|
||||
if (await control.count() > 0) {
|
||||
await control.waitFor({ state: 'visible', timeout: 10_000 })
|
||||
if (await control.getAttribute('aria-expanded') === 'false') await control.click()
|
||||
}
|
||||
}
|
||||
const group = target.locator('xpath=ancestor::*[@data-chat-group-key][1]')
|
||||
const header = group.locator('[data-process-activity]').first()
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"tests/deepseek-messages-chat.e2e.ts",
|
||||
"tests/models-settings-recovery.e2e.ts",
|
||||
"tests/default-model.e2e.ts",
|
||||
"tests/model-menu-observer.e2e.ts",
|
||||
"tests/default-workspace.e2e.ts",
|
||||
"tests/new-session-refused.e2e.ts",
|
||||
"tests/github-ready-review.e2e.ts",
|
||||
|
||||
+19
-1
@@ -3455,6 +3455,25 @@ export type TokenMeterConfig = Record<string, never>
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-token-meter -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-ask-user -->
|
||||
<a id="deepseek-aidsh-tool-ask-user"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-ask-user`
|
||||
|
||||
- `inject`: `tools` · `userQuestions`
|
||||
- `source`: [`packages/interaction/tool-ask-user/src/index.ts:16`](../packages/interaction/tool-ask-user/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Cordis row selecting the tool schema and its default foreground wait. */
|
||||
export interface Config {
|
||||
/** Tool definition selected by this Cordis row. Defaults to the blocking legacy tool. */
|
||||
mode?: 'legacy' | 'timed'
|
||||
/** Foreground wait before automatic continuation. Defaults to 120 seconds. */
|
||||
timeout?: number
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-ask-user -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-bash -->
|
||||
<a id="deepseek-aidsh-tool-bash"></a>
|
||||
|
||||
@@ -4399,7 +4418,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
| `@deepseek-ai/dsh-subprocess-local` | — | [`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subprocess-ssh` | `ssh` | [`packages/ssh/subprocess-ssh/src/index.ts`](../packages/ssh/subprocess-ssh/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | `tools` · `userQuestions` | [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
|
||||
|
||||
@@ -3457,6 +3457,25 @@ export type TokenMeterConfig = Record<string, never>
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-token-meter -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-ask-user -->
|
||||
<a id="deepseek-aidsh-tool-ask-user"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-tool-ask-user`
|
||||
|
||||
- `inject`: `tools` · `userQuestions`
|
||||
- `source`: [`packages/interaction/tool-ask-user/src/index.ts:16`](../packages/interaction/tool-ask-user/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Cordis row selecting the tool schema and its default foreground wait. */
|
||||
export interface Config {
|
||||
/** Tool definition selected by this Cordis row. Defaults to the blocking legacy tool. */
|
||||
mode?: 'legacy' | 'timed'
|
||||
/** Foreground wait before automatic continuation. Defaults to 120 seconds. */
|
||||
timeout?: number
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-tool-ask-user -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-tool-bash -->
|
||||
<a id="deepseek-aidsh-tool-bash"></a>
|
||||
|
||||
@@ -4401,7 +4420,6 @@ export interface Config {
|
||||
| `@deepseek-ai/dsh-subprocess-local` | — | [`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-subprocess-ssh` | `ssh` | [`packages/ssh/subprocess-ssh/src/index.ts`](../packages/ssh/subprocess-ssh/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-terminal` | — | [`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | `tools` · `userQuestions` | [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-call-timeout-policy` | `tools` | [`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-cordis` | `tools` · `cordisInspect` | [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts) |
|
||||
| `@deepseek-ai/dsh-tool-subagent-control` | `tools` · `subagents` | [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts) |
|
||||
|
||||
@@ -14,8 +14,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
|
||||
@@ -63,7 +63,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:77`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:77`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:86`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
|
||||
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
|
||||
@@ -79,7 +79,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:153`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:88`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
|
||||
@@ -65,7 +65,7 @@
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:77`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:77`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:86`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
|
||||
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
|
||||
@@ -81,7 +81,7 @@
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:153`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:88`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
|
||||
@@ -666,6 +666,9 @@ flowchart TD
|
||||
pkg_user_questions --> pkg_agent
|
||||
pkg_user_questions --> pkg_llm
|
||||
pkg_user_questions --> pkg_scope
|
||||
pkg_user_questions --> pkg_session
|
||||
pkg_user_questions --> pkg_session_projection
|
||||
pkg_user_questions --> pkg_typert_protocol
|
||||
pkg_jobs --> pkg_agent
|
||||
pkg_jobs --> pkg_brand
|
||||
pkg_jobs --> pkg_invariants
|
||||
@@ -1574,7 +1577,7 @@ flowchart TD
|
||||
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
|
||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
|
||||
|
||||
@@ -668,6 +668,9 @@ flowchart TD
|
||||
pkg_user_questions --> pkg_agent
|
||||
pkg_user_questions --> pkg_llm
|
||||
pkg_user_questions --> pkg_scope
|
||||
pkg_user_questions --> pkg_session
|
||||
pkg_user_questions --> pkg_session_projection
|
||||
pkg_user_questions --> pkg_typert_protocol
|
||||
pkg_jobs --> pkg_agent
|
||||
pkg_jobs --> pkg_brand
|
||||
pkg_jobs --> pkg_invariants
|
||||
@@ -1576,7 +1579,7 @@ flowchart TD
|
||||
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
|
||||
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`jobs`](../packages/jobs/jobs) | `jobs` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`workspace`](../packages/workspace/workspace) |
|
||||
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: e7c80d1de4d328b4
|
||||
zh: d9f368c27fd9f998
|
||||
/session-persistence-event-catalog/persistence-type-fingerprints:
|
||||
en: 0254fc43535dfaad
|
||||
zh: 2f592cfae60e10ac
|
||||
en: 33dd28b1c768cd8b
|
||||
zh: 7256442baf2faed1
|
||||
/session-persistence-event-catalog/event-envelope:
|
||||
en: 7ebb1a2e0dd1f916
|
||||
zh: 8946f415dda5c1b8
|
||||
@@ -318,8 +318,11 @@
|
||||
en: 6c3effe2abc1aa53
|
||||
zh: 6c3effe2abc1aa53
|
||||
/session-persistence-event-catalog/resolved-persistence-types/always:
|
||||
en: e318559064b455fb
|
||||
zh: e318559064b455fb
|
||||
en: 4cdaf9853769f3da
|
||||
zh: 4cdaf9853769f3da
|
||||
/session-persistence-event-catalog/resolved-persistence-types/answered:
|
||||
en: 9b5e8719c6d3d7ff
|
||||
zh: 9b5e8719c6d3d7ff
|
||||
/session-persistence-event-catalog/resolved-persistence-types/append:
|
||||
en: 02f63fd38e94120b
|
||||
zh: 02f63fd38e94120b
|
||||
@@ -864,8 +867,11 @@
|
||||
en: f01476bd214bb78f
|
||||
zh: f01476bd214bb78f
|
||||
/session-persistence-event-catalog/resolved-persistence-types/user-approval:
|
||||
en: 7c6de40d31cf20a5
|
||||
zh: 7c6de40d31cf20a5
|
||||
en: 1c8f591ed011f359
|
||||
zh: 1c8f591ed011f359
|
||||
/session-persistence-event-catalog/resolved-persistence-types/user-question-reply:
|
||||
en: ce169abc5bc0ad1f
|
||||
zh: ce169abc5bc0ad1f
|
||||
/session-persistence-event-catalog/resolved-persistence-types/user-message:
|
||||
en: ecc3b25a07772288
|
||||
zh: ecc3b25a07772288
|
||||
@@ -951,11 +957,11 @@
|
||||
en: 586743dfeab78c55
|
||||
zh: 26ccef4a02dd7a58
|
||||
/session-persistence-event-catalog/resolved-persistence-types/deepseeksearchllmrequest:
|
||||
en: b3df6dce576b01a2
|
||||
zh: f9c67e338cc6e449
|
||||
en: ea25cd944b4b4020
|
||||
zh: 87f8944f766d3a85
|
||||
/session-persistence-event-catalog/resolved-persistence-types/developermessage:
|
||||
en: 3df6e6aaeb92c0fa
|
||||
zh: 29988ebcdd9e62e1
|
||||
en: af7ce240db9e3463
|
||||
zh: cd15cd0cd369c2c0
|
||||
/session-persistence-event-catalog/resolved-persistence-types/epochheader:
|
||||
en: d31020fa1fda6fa4
|
||||
zh: 77984c4bf09443fb
|
||||
@@ -1053,11 +1059,11 @@
|
||||
en: 6dbe099c62f7c226
|
||||
zh: 6fd788f63c038c91
|
||||
/session-persistence-event-catalog/resolved-persistence-types/llmretrystartedeventdata:
|
||||
en: 7854b9694cc84c3a
|
||||
zh: 319ec2e2e6ffdaec
|
||||
en: f4202ef53de39006
|
||||
zh: f8f1d0bfc0451c31
|
||||
/session-persistence-event-catalog/resolved-persistence-types/message:
|
||||
en: 45a296c31664de87
|
||||
zh: 8f235285b2f985f9
|
||||
en: a3d2a5be822c5a86
|
||||
zh: 871cc3cafabe99be
|
||||
/session-persistence-event-catalog/resolved-persistence-types/messagefeedbackdelete:
|
||||
en: c582953270d4ca44
|
||||
zh: 966ccfe2e7660a2a
|
||||
@@ -1068,14 +1074,14 @@
|
||||
en: f58575a1fc96df42
|
||||
zh: 9f0c6ed9033e5c9d
|
||||
/session-persistence-event-catalog/resolved-persistence-types/messagefeedbackrating:
|
||||
en: 490a91f1da1fc870
|
||||
zh: 2f2b9e972e22d51f
|
||||
en: ef7b940d5654ce5a
|
||||
zh: 72d4c5b3f6e2ff17
|
||||
/session-persistence-event-catalog/resolved-persistence-types/messagesource:
|
||||
en: 7d0eda01d2c3076f
|
||||
zh: ce3dbfc1bd7fd176
|
||||
en: f1789e91fd4338e3
|
||||
zh: 8250eb33cda749b5
|
||||
/session-persistence-event-catalog/resolved-persistence-types/message~2:
|
||||
en: 8e53b2a339808ffc
|
||||
zh: d453e93e45f9e752
|
||||
en: 412cc1296786d489
|
||||
zh: f457e8629515a526
|
||||
/session-persistence-event-catalog/resolved-persistence-types/modelmessagesource:
|
||||
en: 3b677f021d7dd4d3
|
||||
zh: 2a875742b22879a2
|
||||
@@ -1134,11 +1140,11 @@
|
||||
en: 9a54c4478ad3b620
|
||||
zh: 02c4c1fd7e252770
|
||||
/session-persistence-event-catalog/resolved-persistence-types/sessiontitleeventdata:
|
||||
en: 415ce281c096b4c3
|
||||
zh: 39c5bf9dfaa95579
|
||||
en: 2c684a3db9219c45
|
||||
zh: 202000f582ca1ec8
|
||||
/session-persistence-event-catalog/resolved-persistence-types/sessiontitlellmrequesteventdata:
|
||||
en: 0e539718e4bc8ed8
|
||||
zh: 060007f55759f065
|
||||
en: 7b180deead48738e
|
||||
zh: 35d43b7ef489f97b
|
||||
/session-persistence-event-catalog/resolved-persistence-types/sessiontitlemodelidentity:
|
||||
en: 9531468264499498
|
||||
zh: 9da23c0ad2776563
|
||||
@@ -1245,14 +1251,14 @@
|
||||
en: 544292eddeff5faa
|
||||
zh: 4e36e575a5030fb2
|
||||
/session-persistence-event-catalog/resolved-persistence-types/turnendreason:
|
||||
en: 083f0fef130bb976
|
||||
zh: dafd261a95708bec
|
||||
en: 7fd4741c641f9252
|
||||
zh: be115df1a8ee59ce
|
||||
/session-persistence-event-catalog/resolved-persistence-types/usermessage:
|
||||
en: 88b294a9b0bdf0e4
|
||||
zh: 0597f465b9d45b27
|
||||
en: c9090b73ac4ce05b
|
||||
zh: beb1f58404b0a2b8
|
||||
/session-persistence-event-catalog/resolved-persistence-types/usermessage~2:
|
||||
en: 31aaab9c9f53ed9c
|
||||
zh: c5cd866b3f2b27e0
|
||||
en: a3974d05a5d357d3
|
||||
zh: 99e73822f538c99d
|
||||
/session-persistence-event-catalog/resolved-persistence-types/workflowagentoutcome:
|
||||
en: d0a3f9aab18c3de5
|
||||
zh: 76ac69a835ca4e29
|
||||
@@ -1422,11 +1428,11 @@
|
||||
en: 51af619ed8532a43
|
||||
zh: a5ad878c63d0e046
|
||||
/session-persistence-event-catalog/resolved-persistence-types/header-reason-startsseries:
|
||||
en: 62ca405eba4d2757
|
||||
zh: c6a8861533363ae9
|
||||
en: 2b549aa36fe3ab7b
|
||||
zh: 069ef4498112c4e7
|
||||
/session-persistence-event-catalog/resolved-persistence-types/headerseq-message-step-turn:
|
||||
en: 76e7be9a80a4082c
|
||||
zh: c06aaa42a8488aea
|
||||
en: 97dc4446fd0b02ac
|
||||
zh: 57ec4b27d0221957
|
||||
/session-persistence-event-catalog/resolved-persistence-types/height-width:
|
||||
en: a5df50710083ac25
|
||||
zh: aad3ccf6f6de70e4
|
||||
@@ -1443,11 +1449,11 @@
|
||||
en: 41f73c58a77dfa28
|
||||
zh: af77f4aee3cb4a40
|
||||
/session-persistence-event-catalog/resolved-persistence-types/inherited:
|
||||
en: f538b60f99becc0a
|
||||
zh: 0c8efa059a1e6569
|
||||
en: 7dcf0c1ade316952
|
||||
zh: 3c9cd7564659ec3d
|
||||
/session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start:
|
||||
en: 3985827b3ead1d92
|
||||
zh: c535166ccae6c8a3
|
||||
en: f2167c4cf3aa9b4d
|
||||
zh: b4ed04a9a526f49e
|
||||
/session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream:
|
||||
en: f460572284c92755
|
||||
zh: bae7561ce887b040
|
||||
@@ -1776,8 +1782,11 @@
|
||||
en: eaffa616c5b9dc9a
|
||||
zh: 2eb138fcf5936e20
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-user-approval-form-snapshot:
|
||||
en: 58c4ff5b5d70cc02
|
||||
zh: 080be72a31b3205e
|
||||
en: 993991e8f05665bb
|
||||
zh: a03931839e647bac
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-user-question-reply:
|
||||
en: 77a8fc6a2f3a9319
|
||||
zh: 4dc1de1358b51a8d
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-webhook-form-notice:
|
||||
en: 4cd5dc7523fefebb
|
||||
zh: f467561fbd0ae394
|
||||
@@ -1836,11 +1845,11 @@
|
||||
en: a79b2ab16b9ed215
|
||||
zh: 764cd1400ee8db63
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-agent-preset-selected:
|
||||
en: d59bb505610b862f
|
||||
zh: e2637fbbb8365bdf
|
||||
en: c8b6e46c8e161688
|
||||
zh: 3f45f3e70ce9f600
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-agent-inbox-spliced:
|
||||
en: 109d942de30d9227
|
||||
zh: 6c6fc06634ad7945
|
||||
en: d73cb120a4aa138e
|
||||
zh: 1f5ce9f1bb430aec
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-approval-asked:
|
||||
en: 9c77f18b0dad22f0
|
||||
zh: 8894cf2878fe90db
|
||||
@@ -1884,11 +1893,11 @@
|
||||
en: ba73fb10b7e2ffc9
|
||||
zh: 8d5c97e63e5f16da
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-deliverables-presented:
|
||||
en: 49de6f547778d916
|
||||
zh: ce2a1af1ae934f68
|
||||
en: ec7e9f13864db847
|
||||
zh: 694e948cd52387ea
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-developer-message:
|
||||
en: 686d26ced9945d4b
|
||||
zh: 535b9e92eb7535fb
|
||||
en: 73514ab7122858e8
|
||||
zh: 335ebf8ba5454d7e
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-feedback-message-delete:
|
||||
en: 204561e2311db97e
|
||||
zh: 77264396bd5e25c4
|
||||
@@ -1953,11 +1962,11 @@
|
||||
en: acf7e21e4d21ebff
|
||||
zh: fb83a853634d204a
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-session-title:
|
||||
en: ba2f901c9981835f
|
||||
zh: ce0358c7d16b0beb
|
||||
en: 8dd08982ac65646b
|
||||
zh: 3f22850e3160ed36
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-session-title-llm-request:
|
||||
en: 7515d391badb4d0a
|
||||
zh: 9eb19def7a3ed752
|
||||
en: 48c9d78076942c91
|
||||
zh: 6cafb7732d3a1258
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-step-end:
|
||||
en: 7fc233c4a569dae2
|
||||
zh: bc2b3fed4a397c00
|
||||
@@ -2034,11 +2043,11 @@
|
||||
en: 1b045de96172be4b
|
||||
zh: 8a15c6f35b576b37
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-usage:
|
||||
en: a01e61480accb34b
|
||||
zh: 6387a3cc7666aa7e
|
||||
en: 5e70d314feb1b86b
|
||||
zh: b98f69d4d8442c14
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-user-message:
|
||||
en: 58d3e616db84f840
|
||||
zh: 9aed64a03347caad
|
||||
en: 4f793420e83a748e
|
||||
zh: 70a4cb82e27965dd
|
||||
/session-persistence-event-catalog/resolved-persistence-types/type-web-deepseek-search-llm-request:
|
||||
en: f109997e295af37d
|
||||
zh: b2dcfd599f73b61c
|
||||
|
||||
+77
-46
@@ -21,7 +21,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
|
||||
| `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) |
|
||||
| `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) |
|
||||
| `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) |
|
||||
| `event:agent/inbox/spliced` | event | `1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c) |
|
||||
| `event:agent/inbox/spliced` | event | `6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178) |
|
||||
| `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) |
|
||||
| `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) |
|
||||
| `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) |
|
||||
@@ -34,7 +34,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
|
||||
| `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) |
|
||||
| `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) |
|
||||
| `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) |
|
||||
| `event:developer/message` | event | `eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80` | [`{ type: "developer/message" }`](#persistence-type-sha256-eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80) |
|
||||
| `event:developer/message` | event | `186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467` | [`{ type: "developer/message" }`](#persistence-type-sha256-186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467) |
|
||||
| `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) |
|
||||
| `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) |
|
||||
| `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) |
|
||||
@@ -54,7 +54,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
|
||||
| `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) |
|
||||
| `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) |
|
||||
| `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) |
|
||||
| `event:session/title-llm-request` | event | `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d) |
|
||||
| `event:session/title-llm-request` | event | `ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46) |
|
||||
| `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) |
|
||||
| `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) |
|
||||
| `event:subagent/catalog` | event | `3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604) |
|
||||
@@ -76,7 +76,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
|
||||
| `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) |
|
||||
| `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) |
|
||||
| `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) |
|
||||
| `event:user/message` | event | `3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761` | [`{ type: "user/message" }`](#persistence-type-sha256-3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761) |
|
||||
| `event:user/message` | event | `b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411` | [`{ type: "user/message" }`](#persistence-type-sha256-b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411) |
|
||||
| `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331) |
|
||||
| `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) |
|
||||
|
||||
@@ -1357,6 +1357,14 @@ SHA-256: `142c37d302aa22a0a57915b5255939d3ca91c1811ca13fddcf84cf76ea00758a`
|
||||
|
||||
`"always"`
|
||||
|
||||
<a id="persistence-type-sha256-85d6d96ff884297410ec0026bdacba835f10e56cbcc5a3b9d670739f4aa0494b"></a>
|
||||
|
||||
### `"answered"`
|
||||
|
||||
SHA-256: `85d6d96ff884297410ec0026bdacba835f10e56cbcc5a3b9d670739f4aa0494b`
|
||||
|
||||
`"answered"`
|
||||
|
||||
<a id="persistence-type-sha256-e438a9d4fb883b3e4a8d9fc8913aa1550ed50eb384bfbf943100a5605b537498"></a>
|
||||
|
||||
### `"append"`
|
||||
@@ -2819,6 +2827,14 @@ SHA-256: `f68c335306535a001127dae6685ece493a0ee7f4e2a851197b6054d8a9ff7394`
|
||||
|
||||
`"user-approval"`
|
||||
|
||||
<a id="persistence-type-sha256-9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114"></a>
|
||||
|
||||
### `"user-question-reply"`
|
||||
|
||||
SHA-256: `9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114`
|
||||
|
||||
`"user-question-reply"`
|
||||
|
||||
<a id="persistence-type-sha256-de25352d2dc4196309bd84b5bac6319f6d5c898ea0e82bb5ae66c5738d0d426f"></a>
|
||||
|
||||
### `"user/message"`
|
||||
@@ -3207,7 +3223,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web
|
||||
| `body` | required | [`{ max_tokens, messages, model, tools }`](#persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731) |
|
||||
| `endpoint` | required | `string` |
|
||||
|
||||
<a id="persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b"></a>
|
||||
<a id="persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200"></a>
|
||||
|
||||
<a id="persistence-type-developermessage"></a>
|
||||
|
||||
@@ -3215,7 +3231,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web
|
||||
|
||||
### `DeveloperMessage`
|
||||
|
||||
SHA-256: `f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b`
|
||||
SHA-256: `1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200`
|
||||
|
||||
Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts)
|
||||
|
||||
@@ -3224,11 +3240,11 @@ Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message
|
||||
| `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) |
|
||||
| `id` | required | `string` |
|
||||
| `role` | required | `"developer"` |
|
||||
| `source` | required | [`MessageSource`](#persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945) |
|
||||
| `source` | required | [`MessageSource`](#persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116) |
|
||||
|
||||
Source compatibility: `source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`.
|
||||
|
||||
Attribution-only additions: `tmux-context`.
|
||||
Attribution-only additions: `tmux-context`, `user-question-reply`.
|
||||
|
||||
<a id="persistence-type-sha256-9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7"></a>
|
||||
|
||||
@@ -3841,7 +3857,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr
|
||||
| `step` | required | `number` |
|
||||
| `turn` | required | `number` |
|
||||
|
||||
<a id="persistence-type-sha256-691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6"></a>
|
||||
<a id="persistence-type-sha256-cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab"></a>
|
||||
|
||||
<a id="persistence-type-message"></a>
|
||||
|
||||
@@ -3849,17 +3865,17 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr
|
||||
|
||||
### `Message`
|
||||
|
||||
SHA-256: `691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6`
|
||||
SHA-256: `cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab`
|
||||
|
||||
Sources: [`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts)
|
||||
|
||||
One of:
|
||||
|
||||
- [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731)
|
||||
- [`DeveloperMessage`](#persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b)
|
||||
- [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586)
|
||||
- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be)
|
||||
- [`DeveloperMessage`](#persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200)
|
||||
- [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd)
|
||||
- [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363)
|
||||
- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be)
|
||||
|
||||
<a id="persistence-type-sha256-7c6129b6961cf4b2a030f36c78d2b0a5be38617651a9b81322e054ee512bee06"></a>
|
||||
|
||||
@@ -3934,7 +3950,7 @@ One of:
|
||||
- `"negative"`
|
||||
- `"positive"`
|
||||
|
||||
<a id="persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945"></a>
|
||||
<a id="persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116"></a>
|
||||
|
||||
<a id="persistence-type-messagesource"></a>
|
||||
|
||||
@@ -3942,7 +3958,7 @@ One of:
|
||||
|
||||
### `MessageSource`
|
||||
|
||||
SHA-256: `40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945`
|
||||
SHA-256: `26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116`
|
||||
|
||||
Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts)
|
||||
|
||||
@@ -4056,15 +4072,16 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message
|
||||
| `user-approval` | `"relay"` | none | [`{ kind: "user-approval", form: "relay" }`](#persistence-type-sha256-5cdbf342b7748d152bc987b68f58e565e5bba4b227e076e2d2120d87662e45cd) |
|
||||
| `user-approval` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | [`{ kind: "user-approval", form: "snapshot" }`](#persistence-type-sha256-5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c) |
|
||||
| `user-approval` | not declared | none | [`{ kind: "user-approval" }`](#persistence-type-sha256-6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f) |
|
||||
| `user-question-reply` | not declared | `callId`: `string`; `outcome`: `"answered"` | [`{ kind: "user-question-reply" }`](#persistence-type-sha256-c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005) |
|
||||
| `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) |
|
||||
|
||||
<a id="persistence-type-sha256-fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83"></a>
|
||||
<a id="persistence-type-sha256-3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc"></a>
|
||||
|
||||
### `Message[]`
|
||||
|
||||
SHA-256: `fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83`
|
||||
SHA-256: `3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc`
|
||||
|
||||
Array of [`Message`](#persistence-type-sha256-691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6).
|
||||
Array of [`Message`](#persistence-type-sha256-cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab).
|
||||
|
||||
<a id="persistence-type-sha256-719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91"></a>
|
||||
|
||||
@@ -4431,7 +4448,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/
|
||||
| `source` | required | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) |
|
||||
| `title` | required | `string` |
|
||||
|
||||
<a id="persistence-type-sha256-9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd"></a>
|
||||
<a id="persistence-type-sha256-fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996"></a>
|
||||
|
||||
<a id="persistence-type-packagessessionsession-title-llmsrcindextssessiontitlellmrequesteventdata"></a>
|
||||
|
||||
@@ -4439,7 +4456,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/
|
||||
|
||||
### `SessionTitleLlmRequestEventData`
|
||||
|
||||
SHA-256: `9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd`
|
||||
SHA-256: `fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996`
|
||||
|
||||
Sources: [`packages/session/session-title-llm/src/index.ts:34`](../packages/session/session-title-llm/src/index.ts)
|
||||
|
||||
@@ -4447,7 +4464,7 @@ Sources: [`packages/session/session-title-llm/src/index.ts:34`](../packages/sess
|
||||
|---|---|---|
|
||||
| `maxTokens` | required | `number` |
|
||||
| `messageSeqs` | required | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
|
||||
| `messages` | required | [`Message[]`](#persistence-type-sha256-fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83) |
|
||||
| `messages` | required | [`Message[]`](#persistence-type-sha256-3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc) |
|
||||
| `route` | required | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) |
|
||||
| `system` | required | `string` |
|
||||
| `titleProvider` | required | `string` |
|
||||
@@ -5101,7 +5118,7 @@ Sources: [`packages/core/session/src/types.ts:232`](../packages/core/session/src
|
||||
| `interrupted` | not declared | none | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) |
|
||||
| `max-tokens` | not declared | none | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) |
|
||||
|
||||
<a id="persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586"></a>
|
||||
<a id="persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd"></a>
|
||||
|
||||
<a id="persistence-type-packagesllmllmsrcmessagetsusermessage"></a>
|
||||
|
||||
@@ -5109,7 +5126,7 @@ Sources: [`packages/core/session/src/types.ts:232`](../packages/core/session/src
|
||||
|
||||
### `UserMessage`
|
||||
|
||||
SHA-256: `64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586`
|
||||
SHA-256: `cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd`
|
||||
|
||||
Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts)
|
||||
|
||||
@@ -5118,19 +5135,19 @@ Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message
|
||||
| `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) |
|
||||
| `id` | required | `string` |
|
||||
| `role` | required | `"user"` |
|
||||
| `source` | required | [`MessageSource`](#persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945) |
|
||||
| `source` | required | [`MessageSource`](#persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116) |
|
||||
|
||||
Source compatibility: `source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`.
|
||||
|
||||
Attribution-only additions: `tmux-context`.
|
||||
Attribution-only additions: `tmux-context`, `user-question-reply`.
|
||||
|
||||
<a id="persistence-type-sha256-d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621"></a>
|
||||
<a id="persistence-type-sha256-5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f"></a>
|
||||
|
||||
### `UserMessage[]`
|
||||
|
||||
SHA-256: `d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621`
|
||||
SHA-256: `5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f`
|
||||
|
||||
Array of [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586).
|
||||
Array of [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd).
|
||||
|
||||
<a id="persistence-type-sha256-c2deb7c9183735d4e0fffddf5c623f4e34daa1c8e96b111982e957dfcbbcf896"></a>
|
||||
|
||||
@@ -5973,18 +5990,18 @@ Sources: [`packages/core/session/src/types.ts:390`](../packages/core/session/src
|
||||
| `reason` | required | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) |
|
||||
| `startsSeries` | optional | `true` |
|
||||
|
||||
<a id="persistence-type-sha256-51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f"></a>
|
||||
<a id="persistence-type-sha256-a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26"></a>
|
||||
|
||||
### `{ headerSeq?, message, step, turn }`
|
||||
|
||||
SHA-256: `51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f`
|
||||
SHA-256: `a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26`
|
||||
|
||||
Sources: [`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `headerSeq` | optional | `number` |
|
||||
| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b) |
|
||||
| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200) |
|
||||
| `step` | required | `number` |
|
||||
| `turn` | required | `number` |
|
||||
|
||||
@@ -6068,17 +6085,17 @@ Sources: [`packages/core/session/src/types.ts:427`](../packages/core/session/src
|
||||
|---|---|---|
|
||||
| `inherited` | optional | `true` |
|
||||
|
||||
<a id="persistence-type-sha256-dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130"></a>
|
||||
<a id="persistence-type-sha256-149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846"></a>
|
||||
|
||||
### `{ inserted, outcome?, removedCount?, start, … }`
|
||||
|
||||
SHA-256: `dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130`
|
||||
SHA-256: `149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846`
|
||||
|
||||
Sources: [`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621) |
|
||||
| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f) |
|
||||
| `outcome` | optional | `"canceled"` |
|
||||
| `removedCount` | optional | `number` |
|
||||
| `start` | required | `number` |
|
||||
@@ -7337,6 +7354,20 @@ SHA-256: `5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c`
|
||||
| `kind` | required | `"user-approval"` |
|
||||
| `sections` | required | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) |
|
||||
|
||||
<a id="persistence-type-sha256-c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005"></a>
|
||||
|
||||
### `{ kind: "user-question-reply" }`
|
||||
|
||||
SHA-256: `c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005`
|
||||
|
||||
Sources: [`packages/interaction/user-questions/src/types.ts:117`](../packages/interaction/user-questions/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `callId` | required | `string` |
|
||||
| `kind` | required | `"user-question-reply"` |
|
||||
| `outcome` | required | `"answered"` |
|
||||
|
||||
<a id="persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94"></a>
|
||||
|
||||
### `{ kind: "webhook", form: "notice" }`
|
||||
@@ -7612,17 +7643,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b`
|
||||
| `time` | required | `number` |
|
||||
| `type` | required | `"agent-preset/selected"` |
|
||||
|
||||
<a id="persistence-type-sha256-1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c"></a>
|
||||
<a id="persistence-type-sha256-6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178"></a>
|
||||
|
||||
<a id="persistence-type-eventagentinboxspliced"></a>
|
||||
|
||||
### `{ type: "agent/inbox/spliced" }`
|
||||
|
||||
SHA-256: `1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c`
|
||||
SHA-256: `6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178`
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130) |
|
||||
| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846) |
|
||||
| `ignorable` | optional | `true` |
|
||||
| `seq` | required | `number` |
|
||||
| `time` | required | `number` |
|
||||
@@ -7863,17 +7894,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176`
|
||||
| `time` | required | `number` |
|
||||
| `type` | required | `"deliverables/presented"` |
|
||||
|
||||
<a id="persistence-type-sha256-eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80"></a>
|
||||
<a id="persistence-type-sha256-186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467"></a>
|
||||
|
||||
<a id="persistence-type-eventdevelopermessage"></a>
|
||||
|
||||
### `{ type: "developer/message" }`
|
||||
|
||||
SHA-256: `eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80`
|
||||
SHA-256: `186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467`
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f) |
|
||||
| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26) |
|
||||
| `ignorable` | optional | `true` |
|
||||
| `seq` | required | `number` |
|
||||
| `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
|
||||
@@ -8229,17 +8260,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5`
|
||||
| `time` | required | `number` |
|
||||
| `type` | required | `"session/title"` |
|
||||
|
||||
<a id="persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d"></a>
|
||||
<a id="persistence-type-sha256-ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46"></a>
|
||||
|
||||
<a id="persistence-type-eventsessiontitle-llm-request"></a>
|
||||
|
||||
### `{ type: "session/title-llm-request" }`
|
||||
|
||||
SHA-256: `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d`
|
||||
SHA-256: `ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46`
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd) |
|
||||
| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996) |
|
||||
| `ignorable` | optional | `true` |
|
||||
| `seq` | required | `number` |
|
||||
| `time` | required | `number` |
|
||||
@@ -8662,17 +8693,17 @@ Sources: [`packages/llm/llm/src/types.ts:458`](../packages/llm/llm/src/types.ts)
|
||||
| `type` | required | `"usage"` |
|
||||
| `usage` | required | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) |
|
||||
|
||||
<a id="persistence-type-sha256-3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761"></a>
|
||||
<a id="persistence-type-sha256-b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411"></a>
|
||||
|
||||
<a id="persistence-type-eventusermessage"></a>
|
||||
|
||||
### `{ type: "user/message" }`
|
||||
|
||||
SHA-256: `3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761`
|
||||
SHA-256: `b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411`
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
| `data` | required | [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586) |
|
||||
| `data` | required | [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd) |
|
||||
| `ignorable` | optional | `true` |
|
||||
| `seq` | required | `number` |
|
||||
| `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
| `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) |
|
||||
| `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) |
|
||||
| `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) |
|
||||
| `event:agent/inbox/spliced` | event | `1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c) |
|
||||
| `event:agent/inbox/spliced` | event | `6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178) |
|
||||
| `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) |
|
||||
| `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) |
|
||||
| `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) |
|
||||
@@ -36,7 +36,7 @@
|
||||
| `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) |
|
||||
| `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) |
|
||||
| `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) |
|
||||
| `event:developer/message` | event | `eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80` | [`{ type: "developer/message" }`](#persistence-type-sha256-eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80) |
|
||||
| `event:developer/message` | event | `186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467` | [`{ type: "developer/message" }`](#persistence-type-sha256-186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467) |
|
||||
| `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) |
|
||||
| `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) |
|
||||
| `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) |
|
||||
@@ -56,7 +56,7 @@
|
||||
| `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) |
|
||||
| `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) |
|
||||
| `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) |
|
||||
| `event:session/title-llm-request` | event | `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d) |
|
||||
| `event:session/title-llm-request` | event | `ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46) |
|
||||
| `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) |
|
||||
| `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) |
|
||||
| `event:subagent/catalog` | event | `3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-3abae7324356f155cb42450c00b806d134ec93bd6439d2063b8d724162d58604) |
|
||||
@@ -78,7 +78,7 @@
|
||||
| `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) |
|
||||
| `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) |
|
||||
| `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) |
|
||||
| `event:user/message` | event | `3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761` | [`{ type: "user/message" }`](#persistence-type-sha256-3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761) |
|
||||
| `event:user/message` | event | `b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411` | [`{ type: "user/message" }`](#persistence-type-sha256-b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411) |
|
||||
| `event:web/deepseek-search-llm-request` | event | `cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-cf6e3aaf1e2de6480aa0157730a41b9a492108a55304100b0f7e112711dd4331) |
|
||||
| `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) |
|
||||
|
||||
@@ -1359,6 +1359,14 @@ SHA-256: `142c37d302aa22a0a57915b5255939d3ca91c1811ca13fddcf84cf76ea00758a`
|
||||
|
||||
`"always"`
|
||||
|
||||
<a id="persistence-type-sha256-85d6d96ff884297410ec0026bdacba835f10e56cbcc5a3b9d670739f4aa0494b"></a>
|
||||
|
||||
### `"answered"`
|
||||
|
||||
SHA-256: `85d6d96ff884297410ec0026bdacba835f10e56cbcc5a3b9d670739f4aa0494b`
|
||||
|
||||
`"answered"`
|
||||
|
||||
<a id="persistence-type-sha256-e438a9d4fb883b3e4a8d9fc8913aa1550ed50eb384bfbf943100a5605b537498"></a>
|
||||
|
||||
### `"append"`
|
||||
@@ -2821,6 +2829,14 @@ SHA-256: `f68c335306535a001127dae6685ece493a0ee7f4e2a851197b6054d8a9ff7394`
|
||||
|
||||
`"user-approval"`
|
||||
|
||||
<a id="persistence-type-sha256-9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114"></a>
|
||||
|
||||
### `"user-question-reply"`
|
||||
|
||||
SHA-256: `9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114`
|
||||
|
||||
`"user-question-reply"`
|
||||
|
||||
<a id="persistence-type-sha256-de25352d2dc4196309bd84b5bac6319f6d5c898ea0e82bb5ae66c5738d0d426f"></a>
|
||||
|
||||
### `"user/message"`
|
||||
@@ -3209,7 +3225,7 @@ SHA-256: `2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9`
|
||||
| `body` | 必需 | [`{ max_tokens, messages, model, tools }`](#persistence-type-sha256-930a6567a10bb62ddd157bd8abdf4b182810c8c3b49d91309b3b009a5fed9731) |
|
||||
| `endpoint` | 必需 | `string` |
|
||||
|
||||
<a id="persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b"></a>
|
||||
<a id="persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200"></a>
|
||||
|
||||
<a id="persistence-type-developermessage"></a>
|
||||
|
||||
@@ -3217,7 +3233,7 @@ SHA-256: `2517ba143a271508d3ca35126d5aca8f7f1facf5aaddce4adf011832042fa2b9`
|
||||
|
||||
### `DeveloperMessage`
|
||||
|
||||
SHA-256: `f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b`
|
||||
SHA-256: `1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200`
|
||||
|
||||
来源:[`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts)
|
||||
|
||||
@@ -3226,11 +3242,11 @@ SHA-256: `f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b`
|
||||
| `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) |
|
||||
| `id` | 必需 | `string` |
|
||||
| `role` | 必需 | `"developer"` |
|
||||
| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945) |
|
||||
| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116) |
|
||||
|
||||
消息源兼容策略:`source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`.
|
||||
|
||||
仅表示归属的新增 kind:`tmux-context`.
|
||||
仅表示归属的新增 kind:`tmux-context`, `user-question-reply`.
|
||||
|
||||
<a id="persistence-type-sha256-9a930283b91d493d6347146f2475bb107294b379415ca02162354f46a64164f7"></a>
|
||||
|
||||
@@ -3843,7 +3859,7 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20`
|
||||
| `step` | 必需 | `number` |
|
||||
| `turn` | 必需 | `number` |
|
||||
|
||||
<a id="persistence-type-sha256-691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6"></a>
|
||||
<a id="persistence-type-sha256-cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab"></a>
|
||||
|
||||
<a id="persistence-type-message"></a>
|
||||
|
||||
@@ -3851,17 +3867,17 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20`
|
||||
|
||||
### `Message`
|
||||
|
||||
SHA-256: `691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6`
|
||||
SHA-256: `cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab`
|
||||
|
||||
来源:[`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts)
|
||||
|
||||
以下类型之一:
|
||||
|
||||
- [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731)
|
||||
- [`DeveloperMessage`](#persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b)
|
||||
- [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586)
|
||||
- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be)
|
||||
- [`DeveloperMessage`](#persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200)
|
||||
- [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd)
|
||||
- [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363)
|
||||
- [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be)
|
||||
|
||||
<a id="persistence-type-sha256-7c6129b6961cf4b2a030f36c78d2b0a5be38617651a9b81322e054ee512bee06"></a>
|
||||
|
||||
@@ -3936,7 +3952,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2`
|
||||
- `"negative"`
|
||||
- `"positive"`
|
||||
|
||||
<a id="persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945"></a>
|
||||
<a id="persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116"></a>
|
||||
|
||||
<a id="persistence-type-messagesource"></a>
|
||||
|
||||
@@ -3944,7 +3960,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2`
|
||||
|
||||
### `MessageSource`
|
||||
|
||||
SHA-256: `40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945`
|
||||
SHA-256: `26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116`
|
||||
|
||||
来源:[`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts)
|
||||
|
||||
@@ -4058,15 +4074,16 @@ SHA-256: `40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945`
|
||||
| `user-approval` | `"relay"` | 无 | [`{ kind: "user-approval", form: "relay" }`](#persistence-type-sha256-5cdbf342b7748d152bc987b68f58e565e5bba4b227e076e2d2120d87662e45cd) |
|
||||
| `user-approval` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | [`{ kind: "user-approval", form: "snapshot" }`](#persistence-type-sha256-5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c) |
|
||||
| `user-approval` | 未声明 | 无 | [`{ kind: "user-approval" }`](#persistence-type-sha256-6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f) |
|
||||
| `user-question-reply` | 未声明 | `callId`: `string`; `outcome`: `"answered"` | [`{ kind: "user-question-reply" }`](#persistence-type-sha256-c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005) |
|
||||
| `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) |
|
||||
|
||||
<a id="persistence-type-sha256-fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83"></a>
|
||||
<a id="persistence-type-sha256-3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc"></a>
|
||||
|
||||
### `Message[]`
|
||||
|
||||
SHA-256: `fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83`
|
||||
SHA-256: `3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc`
|
||||
|
||||
[`Message`](#persistence-type-sha256-691c0f641cd3e8072fdacd6fa8c400253cf75a0ce9285d8ce89141bbb24f2fb6) 的数组。
|
||||
[`Message`](#persistence-type-sha256-cb7b863ddb7b3eab5f18f216602feba25f47b456e73e4e72b6bcc259875c6dab) 的数组。
|
||||
|
||||
<a id="persistence-type-sha256-719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91"></a>
|
||||
|
||||
@@ -4433,7 +4450,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4`
|
||||
| `source` | 必需 | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) |
|
||||
| `title` | 必需 | `string` |
|
||||
|
||||
<a id="persistence-type-sha256-9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd"></a>
|
||||
<a id="persistence-type-sha256-fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996"></a>
|
||||
|
||||
<a id="persistence-type-packagessessionsession-title-llmsrcindextssessiontitlellmrequesteventdata"></a>
|
||||
|
||||
@@ -4441,7 +4458,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4`
|
||||
|
||||
### `SessionTitleLlmRequestEventData`
|
||||
|
||||
SHA-256: `9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd`
|
||||
SHA-256: `fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996`
|
||||
|
||||
来源:[`packages/session/session-title-llm/src/index.ts:34`](../packages/session/session-title-llm/src/index.ts)
|
||||
|
||||
@@ -4449,7 +4466,7 @@ SHA-256: `9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd`
|
||||
|---|---|---|
|
||||
| `maxTokens` | 必需 | `number` |
|
||||
| `messageSeqs` | 必需 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
|
||||
| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-fc8242857caf7450d7f68a1f509dce83e237bd21571666006895124cb2246a83) |
|
||||
| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-3f42f45cbb8a06586d247e5f3763dc02c8d31a145a9a3b82fabcd101e7e224cc) |
|
||||
| `route` | 必需 | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) |
|
||||
| `system` | 必需 | `string` |
|
||||
| `titleProvider` | 必需 | `string` |
|
||||
@@ -5103,7 +5120,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402`
|
||||
| `interrupted` | 未声明 | 无 | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) |
|
||||
| `max-tokens` | 未声明 | 无 | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) |
|
||||
|
||||
<a id="persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586"></a>
|
||||
<a id="persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd"></a>
|
||||
|
||||
<a id="persistence-type-packagesllmllmsrcmessagetsusermessage"></a>
|
||||
|
||||
@@ -5111,7 +5128,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402`
|
||||
|
||||
### `UserMessage`
|
||||
|
||||
SHA-256: `64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586`
|
||||
SHA-256: `cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd`
|
||||
|
||||
来源:[`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts)
|
||||
|
||||
@@ -5120,19 +5137,19 @@ SHA-256: `64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586`
|
||||
| `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) |
|
||||
| `id` | 必需 | `string` |
|
||||
| `role` | 必需 | `"user"` |
|
||||
| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-40e314bd88730b4bff04a2083cc540ef8659ee2240eeaae0132cac974784b945) |
|
||||
| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-26df091b55c8a9b58004931ce5425b441201032cfab0b75e2ba03f428ecbd116) |
|
||||
|
||||
消息源兼容策略:`source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`.
|
||||
|
||||
仅表示归属的新增 kind:`tmux-context`.
|
||||
仅表示归属的新增 kind:`tmux-context`, `user-question-reply`.
|
||||
|
||||
<a id="persistence-type-sha256-d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621"></a>
|
||||
<a id="persistence-type-sha256-5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f"></a>
|
||||
|
||||
### `UserMessage[]`
|
||||
|
||||
SHA-256: `d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621`
|
||||
SHA-256: `5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f`
|
||||
|
||||
[`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586) 的数组。
|
||||
[`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd) 的数组。
|
||||
|
||||
<a id="persistence-type-sha256-c2deb7c9183735d4e0fffddf5c623f4e34daa1c8e96b111982e957dfcbbcf896"></a>
|
||||
|
||||
@@ -5975,18 +5992,18 @@ SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585`
|
||||
| `reason` | 必需 | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) |
|
||||
| `startsSeries` | 可选 | `true` |
|
||||
|
||||
<a id="persistence-type-sha256-51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f"></a>
|
||||
<a id="persistence-type-sha256-a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26"></a>
|
||||
|
||||
### `{ headerSeq?, message, step, turn }`
|
||||
|
||||
SHA-256: `51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f`
|
||||
SHA-256: `a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26`
|
||||
|
||||
来源:[`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `headerSeq` | 可选 | `number` |
|
||||
| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-f61313bd96ed95b72eb16a4a5a77ad94a0da1756d428d3d660ff6e23c9e6342b) |
|
||||
| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-1020bbc3c744c060cd31c0891ae79b9e4c3e0adfa3f0be49869309b34e9ef200) |
|
||||
| `step` | 必需 | `number` |
|
||||
| `turn` | 必需 | `number` |
|
||||
|
||||
@@ -6070,17 +6087,17 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3`
|
||||
|---|---|---|
|
||||
| `inherited` | 可选 | `true` |
|
||||
|
||||
<a id="persistence-type-sha256-dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130"></a>
|
||||
<a id="persistence-type-sha256-149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846"></a>
|
||||
|
||||
### `{ inserted, outcome?, removedCount?, start, … }`
|
||||
|
||||
SHA-256: `dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130`
|
||||
SHA-256: `149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846`
|
||||
|
||||
来源:[`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-d1f27cf1650e5f59d5d6e95ce6dfd92b31757af076a8ab6418a70b6dbdd53621) |
|
||||
| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-5a92f12db2059c282cdea45b6845c65e6f4a95dd3d649aeddbde6f9ae03c561f) |
|
||||
| `outcome` | 可选 | `"canceled"` |
|
||||
| `removedCount` | 可选 | `number` |
|
||||
| `start` | 必需 | `number` |
|
||||
@@ -7339,6 +7356,20 @@ SHA-256: `5654685d7557ec747e74887237065e626a5bdfa426ca43f7fbb0b6fee9b0916c`
|
||||
| `kind` | 必需 | `"user-approval"` |
|
||||
| `sections` | 必需 | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) |
|
||||
|
||||
<a id="persistence-type-sha256-c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005"></a>
|
||||
|
||||
### `{ kind: "user-question-reply" }`
|
||||
|
||||
SHA-256: `c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005`
|
||||
|
||||
来源:[`packages/interaction/user-questions/src/types.ts:117`](../packages/interaction/user-questions/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `callId` | 必需 | `string` |
|
||||
| `kind` | 必需 | `"user-question-reply"` |
|
||||
| `outcome` | 必需 | `"answered"` |
|
||||
|
||||
<a id="persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94"></a>
|
||||
|
||||
### `{ kind: "webhook", form: "notice" }`
|
||||
@@ -7614,17 +7645,17 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b`
|
||||
| `time` | 必需 | `number` |
|
||||
| `type` | 必需 | `"agent-preset/selected"` |
|
||||
|
||||
<a id="persistence-type-sha256-1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c"></a>
|
||||
<a id="persistence-type-sha256-6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178"></a>
|
||||
|
||||
<a id="persistence-type-eventagentinboxspliced"></a>
|
||||
|
||||
### `{ type: "agent/inbox/spliced" }`
|
||||
|
||||
SHA-256: `1506a9b8224986c83015ae99d2cb5ede705538c58c063d6a48ef6d761a31ba6c`
|
||||
SHA-256: `6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178`
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-dc4bd7ac827f4b1f9236f9a157a4b4cc171a0588064a611e43edd95f62c68130) |
|
||||
| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-149c5a77529cb0dc06f5ceb6273069b7606174d1e7893b5f9b1071c15306b846) |
|
||||
| `ignorable` | 可选 | `true` |
|
||||
| `seq` | 必需 | `number` |
|
||||
| `time` | 必需 | `number` |
|
||||
@@ -7865,17 +7896,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176`
|
||||
| `time` | 必需 | `number` |
|
||||
| `type` | 必需 | `"deliverables/presented"` |
|
||||
|
||||
<a id="persistence-type-sha256-eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80"></a>
|
||||
<a id="persistence-type-sha256-186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467"></a>
|
||||
|
||||
<a id="persistence-type-eventdevelopermessage"></a>
|
||||
|
||||
### `{ type: "developer/message" }`
|
||||
|
||||
SHA-256: `eef4ef54dc7a133d47448a4ee822e45a351314923ef5f66db34c8b24e4b32d80`
|
||||
SHA-256: `186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467`
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-51c78ccc9023ce6642d07fb9a10cbf942ff7e313d5b304ddc77218df71da708f) |
|
||||
| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-a5d80b5a2f3a6bbaf8f0334126632b78f94d8346122c68e8122ea89906d63d26) |
|
||||
| `ignorable` | 可选 | `true` |
|
||||
| `seq` | 必需 | `number` |
|
||||
| `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
|
||||
@@ -8231,17 +8262,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5`
|
||||
| `time` | 必需 | `number` |
|
||||
| `type` | 必需 | `"session/title"` |
|
||||
|
||||
<a id="persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d"></a>
|
||||
<a id="persistence-type-sha256-ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46"></a>
|
||||
|
||||
<a id="persistence-type-eventsessiontitle-llm-request"></a>
|
||||
|
||||
### `{ type: "session/title-llm-request" }`
|
||||
|
||||
SHA-256: `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d`
|
||||
SHA-256: `ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46`
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-9bc85364d1f37d9c837db6a3bdd3d1d007d923cdc7401a3322be9f12fdc225fd) |
|
||||
| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-fe3cdc6f9050584cea4a6d6998c31334bff7899f5ff1095082a44fdf3b47e996) |
|
||||
| `ignorable` | 可选 | `true` |
|
||||
| `seq` | 必需 | `number` |
|
||||
| `time` | 必需 | `number` |
|
||||
@@ -8664,17 +8695,17 @@ SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515`
|
||||
| `type` | 必需 | `"usage"` |
|
||||
| `usage` | 必需 | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) |
|
||||
|
||||
<a id="persistence-type-sha256-3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761"></a>
|
||||
<a id="persistence-type-sha256-b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411"></a>
|
||||
|
||||
<a id="persistence-type-eventusermessage"></a>
|
||||
|
||||
### `{ type: "user/message" }`
|
||||
|
||||
SHA-256: `3f72db3d87a0c5c43e68be467b4cca728eaf5adc1d5d2b6975ff42bfbd961761`
|
||||
SHA-256: `b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411`
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586) |
|
||||
| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-cfa4409ab0ef198a50a1121ad5013a740760444adc225b9ea816b4667c0158fd) |
|
||||
| `ignorable` | 可选 | `true` |
|
||||
| `seq` | 必需 | `number` |
|
||||
| `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) |
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Bilingual-pair consistency record for 2026-09-21-user-question-reply.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-09-21-user-question-reply.md
|
||||
/:
|
||||
en: 3d5a424ae7d7e46b
|
||||
zh: c22a8e4a62d8cc1a
|
||||
/2026-09-21-user-question-reply:
|
||||
en: de475e4778124528
|
||||
zh: 2b619b0c0f3a6a6e
|
||||
/2026-09-21-user-question-reply/summary:
|
||||
en: bf6a131a55aaa3bc
|
||||
zh: b781b8c78d0dd7a8
|
||||
/2026-09-21-user-question-reply/table-of-contents:
|
||||
en: 6cf314a069ceb451
|
||||
zh: 9adff678e9ecab3b
|
||||
/2026-09-21-user-question-reply/declaration:
|
||||
en: 6907136f28e48c90
|
||||
zh: 02f8298485f3d8e7
|
||||
/2026-09-21-user-question-reply/compatibility:
|
||||
en: 473d2dd57ff5b150
|
||||
zh: 9a8c5ee49b9f862e
|
||||
/2026-09-21-user-question-reply/verification:
|
||||
en: 523c0abe85cc0061
|
||||
zh: c38e1e107bdd15a1
|
||||
/2026-09-21-user-question-reply/dev-note:
|
||||
en: b75c8458928b72b9
|
||||
zh: 003ba4e17b4b5269
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: "Records a persistence type transition and its compatibility acknowledgement."
|
||||
kind: persistence-change
|
||||
---
|
||||
|
||||
# 2026-09-21-user-question-reply
|
||||
|
||||
English | [中文](2026-09-21-user-question-reply.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Adds a qualified user-question-reply message source for a late answer to a continued ask_user_question call.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Declaration](#declaration)
|
||||
- [Compatibility](#compatibility)
|
||||
- [Verification](#verification)
|
||||
- [Dev Note](#dev-note)
|
||||
|
||||
<a id="declaration"></a>
|
||||
## Declaration
|
||||
|
||||
```yaml persistence-change
|
||||
schemaVersion: 1
|
||||
id: 2026-09-21-user-question-reply
|
||||
baseline: false
|
||||
changes:
|
||||
- root: "event:agent/inbox/spliced"
|
||||
previous: "2026-09-16-session-format-v4"
|
||||
after: "6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178"
|
||||
decision: same-version
|
||||
- root: "event:developer/message"
|
||||
previous: "2026-09-16-session-format-v4"
|
||||
after: "186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467"
|
||||
decision: same-version
|
||||
- root: "event:session/title-llm-request"
|
||||
previous: "2026-09-16-session-format-v4"
|
||||
after: "ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46"
|
||||
decision: same-version
|
||||
- root: "event:user/message"
|
||||
previous: "2026-09-16-session-format-v4"
|
||||
after: "b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411"
|
||||
decision: same-version
|
||||
```
|
||||
|
||||
<a id="compatibility"></a>
|
||||
## Compatibility
|
||||
|
||||
Existing logs contain no such source and remain valid. The new source is qualified attribution on an ordinary user message; readers without dsh-user-questions preserve the message and derive history from its content. Only the userQuestions projection reads this source to close the named question and record its answers. The answer RPC is its only producer and writes outcome answered; closing the Client panel persists no reply. No event type or Session header changes.
|
||||
|
||||
<a id="verification"></a>
|
||||
## Verification
|
||||
|
||||
pnpm exec vitest run packages/interaction/user-questions/tests packages/interaction/tool-ask-user/tests: 81 tests passed. The focused projection, reply, view, and process-group suite passed 70 tests. pnpm run typecheck passed. pnpm run doc-sync passed all 42 gates, including persistence history and translation pairing.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
## Dev Note
|
||||
|
||||
None.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,60 @@
|
||||
---
|
||||
description: "记录持久化类型更改及其兼容性确认。"
|
||||
kind: persistence-change
|
||||
---
|
||||
|
||||
# 2026-09-21-user-question-reply
|
||||
|
||||
[English](2026-09-21-user-question-reply.md) | 中文
|
||||
|
||||
## 概述
|
||||
|
||||
新增受限定的 user-question-reply 消息来源,将已继续的 ask_user_question 调用的迟到回答送入 agent inbox。
|
||||
|
||||
## 目录
|
||||
|
||||
- [声明](#declaration)
|
||||
- [兼容性](#compatibility)
|
||||
- [验证](#verification)
|
||||
- [开发备注](#dev-note)
|
||||
|
||||
<a id="declaration"></a>
|
||||
## 声明
|
||||
|
||||
```yaml persistence-change
|
||||
schemaVersion: 1
|
||||
id: 2026-09-21-user-question-reply
|
||||
baseline: false
|
||||
changes:
|
||||
- root: "event:agent/inbox/spliced"
|
||||
previous: "2026-09-16-session-format-v4"
|
||||
after: "6178f1edcb23f361f2a6cb2c859b1c2187220d5695ece4a3400e0d92845a7178"
|
||||
decision: same-version
|
||||
- root: "event:developer/message"
|
||||
previous: "2026-09-16-session-format-v4"
|
||||
after: "186159f5f6f67a0b8cd095b8fe55bef42d4f25ca1a1c248f859867af2ece0467"
|
||||
decision: same-version
|
||||
- root: "event:session/title-llm-request"
|
||||
previous: "2026-09-16-session-format-v4"
|
||||
after: "ae84c5e493f94acc63cfb70389073ba616ed7ae7aa4fadde048bdcc64d49bb46"
|
||||
decision: same-version
|
||||
- root: "event:user/message"
|
||||
previous: "2026-09-16-session-format-v4"
|
||||
after: "b83ed1b1cfffbd7bd5cca06ea72e44be57beb68b39e5a96660ce42a9e21aa411"
|
||||
decision: same-version
|
||||
```
|
||||
|
||||
<a id="compatibility"></a>
|
||||
## 兼容性
|
||||
|
||||
已有日志不含该来源,仍然有效。新来源是普通用户消息上的受限定归属;没有 dsh-user-questions 的读取方保留消息,并从内容推导历史。只有 userQuestions projection 读取该来源,关闭指定的问题并记录答案。answer RPC 是唯一生产方,只写入 outcome answered;关闭 Client 面板不会持久化回复。不新增事件类型,也不改变 Session header。
|
||||
|
||||
<a id="verification"></a>
|
||||
## 验证
|
||||
|
||||
pnpm exec vitest run packages/interaction/user-questions/tests packages/interaction/tool-ask-user/tests:81 个测试通过。projection、reply、view 和 process-group 的定向测试共 70 个通过。pnpm run typecheck 通过。pnpm run doc-sync 的 42 项检查全部通过,包括持久化历史和翻译配对。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
## 开发备注
|
||||
|
||||
无。
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 14c57f30e676f6e3
|
||||
zh: b1a0ccf26fab3727
|
||||
/session-persistence-formats/format-references:
|
||||
en: 45dba01dd6d69c80
|
||||
zh: c366d1a3991118c9
|
||||
en: b19e6cfc84bfc46d
|
||||
zh: 5b1d86bcdab672ca
|
||||
/session-persistence-formats/scope-and-evidence:
|
||||
en: b220062139ddac1e
|
||||
zh: 7c4c0efe05dafa9c
|
||||
|
||||
@@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant.
|
||||
| 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 |
|
||||
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 |
|
||||
| 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 |
|
||||
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 587 |
|
||||
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 590 |
|
||||
|
||||
<!-- persistence-format-index:end -->
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式
|
||||
| 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 |
|
||||
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 |
|
||||
| 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 |
|
||||
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 587 |
|
||||
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 590 |
|
||||
|
||||
<!-- persistence-format-index:end -->
|
||||
|
||||
|
||||
Generated
+3469
-3327
File diff suppressed because it is too large.
Load diff
@@ -10,7 +10,7 @@
|
||||
zh: 8e1d2627823b1ffc
|
||||
/user-interaction/presentation-intent:
|
||||
en: aca3811ea3f0c2ef
|
||||
zh: 91f93dc33d12e343
|
||||
zh: 6cf2aea40c7dece8
|
||||
/user-interaction/question-item:
|
||||
en: 1ad4373e6fdfe4c7
|
||||
zh: efb8d41ebe7224f8
|
||||
@@ -18,8 +18,11 @@
|
||||
en: e10cf6c8c9c20962
|
||||
zh: 0b347b40c12b6b25
|
||||
/user-interaction/answer:
|
||||
en: 4235b687a9975881
|
||||
zh: 350d000d4bfa7400
|
||||
en: efd2dbb95a885d0e
|
||||
zh: 7a20c75c4ea6a9e0
|
||||
/user-interaction/timed-questions-and-late-replies:
|
||||
en: 74fa698d4a115e80
|
||||
zh: 4e9b03b5eab2ae5e
|
||||
/user-interaction/errors:
|
||||
en: 33cc3bf5e352e223
|
||||
zh: 143279cc3f355453
|
||||
en: 5762b1498a426ab6
|
||||
zh: bb9987ec82103620
|
||||
@@ -103,9 +103,96 @@ interface AskUserQuestionAnswer {
|
||||
}
|
||||
```
|
||||
|
||||
Timed answerers receive `wait: { callId, timed: true }`. The call id identifies the card and the foreground wait. The Client calls `attachWait` to claim the wait and obtain its remaining duration; transport forwards the original event without inspecting or rewriting these fields. A `wait` without `timed` identifies an indefinite card-keyed request. An absent `wait` identifies a blocking request without a card key.
|
||||
|
||||
```ts type-equiv
|
||||
/** Client-safe payload declared for the user-question answerer waterfall. */
|
||||
interface AskUserQuestionRequestEvent {
|
||||
/** Questions to display. */
|
||||
questions: AskUserQuestionItem[]
|
||||
/** Agent identity projected to the corresponding Client Context in transit. */
|
||||
agent?: Agent
|
||||
/** Cancellation lifetime of the pending request. */
|
||||
signal?: AbortSignal
|
||||
/**
|
||||
* Tool call the Client card is keyed by. Timed answerers attach to the
|
||||
* business wait stream before starting their local countdown.
|
||||
*/
|
||||
wait?: {
|
||||
/** Tool call the Client card is keyed by. */
|
||||
callId: ToolCallId
|
||||
/** True for a foreground wait that requires a Client claim; absent for indefinite waits. */
|
||||
timed?: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Timed questions and late replies
|
||||
|
||||
A timed question waits for a bounded foreground window and then lets the agent continue independent work. If the user answers inside the window, the tool returns the answer batch; otherwise `TimedUserQuestionResult` is `{ pending: true, callId }`. Pending means the question is still answerable; it is not an empty answer, a refusal, a confirmation, or permission.
|
||||
|
||||
```ts type-equiv
|
||||
/** Timed ask result returned when the foreground answer window closes. */
|
||||
type TimedUserQuestionResult = AskUserQuestionAnswer | { pending: true; callId: ToolCallId }
|
||||
```
|
||||
|
||||
A Client claim is a business Remote stream, not a Gateway delivery. `TimedQuestionWait` holds the original Host deadline and runs its timer only while no claim exists. Its `attachWait` stream yields the current remaining duration, and the Client counts down with its own clock. Focus, editing, and Take time stay local. Stream cancellation releases that claim; after the last claim leaves, the Host resumes the original deadline. An unattended timeout aborts the foreground waterfall's private signal and returns pending without cancelling the Turn. Gateway and `api-remotes` contain no question-specific timing policy.
|
||||
|
||||
The `userQuestions` Session projection folds existing events into the answerable question set and the settled one; no wait state, focus state, or deadline is logged. Each `request/header` records the exact tool schemas the model saw. Native calls are tracked only when that header declares the timed `ask_user_question` schema with a `timeout` parameter; the blocking legacy tool never declares it. A native `tool/call` opens a question. A `tool/result` marks it `continued` only when it carries the pending payload or is the synthetic `TOOL_OUTCOME_UNKNOWN` result Session resume repair appends; an answer batch moves it into `settled` with that batch, and any failure drops it. A `run_code` PTC sub-call enters the projection when its `tool/ptc-dispatch` result is pending, even though the request header exposes only `run_code`. A late reply moves a continued question into `settled` when its message is admitted as `user/message`, together with the answer batch read out of that message: the timed call's own result recorded the timeout, so a transcript row has nowhere else to read what the user finally chose. A queued reply can be canceled before admission and leaves the question answerable. A reply whose text carries no readable batch settles the call with no answers; one naming a call that is not active changes nothing.
|
||||
|
||||
```ts type-equiv
|
||||
/** `open` while the tool call may still return the answer; `continued` once the answer can only arrive as a new turn. */
|
||||
type UserQuestionState = 'open' | 'continued'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One unanswered timed `ask_user_question` call reconstructed from the Session log. */
|
||||
interface PendingUserQuestion {
|
||||
readonly callId: ToolCallId
|
||||
readonly questions: readonly AskUserQuestionItem[]
|
||||
readonly state: UserQuestionState
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One timed `ask_user_question` call and the answers it settled with: the
|
||||
* batch its own result carried when the user answered inside the window,
|
||||
* otherwise the batch its late reply carried, because that call's own result
|
||||
* recorded the timeout. A transcript row reads what the user finally answered
|
||||
* from here, and only a call listed here was a timed one.
|
||||
*/
|
||||
interface SettledUserQuestion {
|
||||
/** The settled call. */
|
||||
readonly callId: ToolCallId
|
||||
/** The recorded batch, one entry per question; empty when the late reply carried none. */
|
||||
readonly answers: readonly AskUserQuestionAnswerItem[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Both halves of one Session's timed `ask_user_question` state, as every
|
||||
* Client reads them. A call made while the blocking legacy tool was in
|
||||
* effect appears in neither half.
|
||||
*/
|
||||
interface UserQuestionProjectionView {
|
||||
/** Calls that can still take an answer, in ask order. */
|
||||
readonly active: readonly PendingUserQuestion[]
|
||||
/** Calls an answer settled, in settlement order. */
|
||||
readonly settled: readonly SettledUserQuestion[]
|
||||
}
|
||||
```
|
||||
|
||||
An open question accepts answers only through the waterfall. Once it is `continued`, the `answer` Remote method queues a user message for the owning agent and wakes it if idle. The message has source `user-question-reply`, outcome `answered`, and an `answer_to_pending_question` payload. The method returns `false` for an open or unknown call, rejects a second queued reply with `REPLY_QUEUED` even after a Host restart, and rejects incomplete or duplicate answer batches with `BAD_ANSWER`.
|
||||
|
||||
Closing the Client panel sends nothing. A foreground timeout ends only the tool wait. Continued questions have no deadline; they stay answerable until a reply is admitted.
|
||||
|
||||
After a Host restart, the Remote layer resumes the root agent when the Session reopens. A late reply then enters a new turn.
|
||||
|
||||
## Errors
|
||||
|
||||
`UserQuestionError` extends `HarnessError`, so `ctx.tools.execute()` preserves `{ name, code }` for model-facing tool failures such as `EMPTY_QUESTIONS`, `NO_PROVIDER`, `ASK_ABORTED`, or UI-side cancellation.
|
||||
`UserQuestionError` extends `HarnessError`, so `ctx.tools.execute()` preserves `{ name, code }` for model-facing tool failures such as `EMPTY_QUESTIONS`, `BAD_INTENT`, `NO_PROVIDER`, `ASK_ABORTED`, or UI-side cancellation.
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable error taxonomy for user-questions failures. */
|
||||
@@ -132,6 +219,44 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
`ctx.userQuestions`: validation plus the scoped answerer waterfall.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Answer a continued question. The reply is steered into the agent as a
|
||||
* user message whose source names the call; that message is also the
|
||||
* record that closes the question in the projection.
|
||||
* @param agent - Live root agent for the owning Session.
|
||||
* @param callId - Continued question identity.
|
||||
* @param answer - Complete structured answer batch, one item per question of the call.
|
||||
* @returns Whether the question is still continued; an accepted reply stays
|
||||
* queued until the agent admits its user message.
|
||||
* @throws {UserQuestionError} `BAD_ANSWER` when the batch does not name each
|
||||
* question of the call exactly once, or `REPLY_QUEUED` when a reply is
|
||||
* already waiting for admission.
|
||||
*/
|
||||
@Remote answer(agent: Agent, callId: ToolCallId, answer: AskUserQuestionAnswer): boolean
|
||||
|
||||
/**
|
||||
* Let one answer UI hold a live timed wait. Closing the stream releases its claim.
|
||||
* @param agent - Live root agent owning the question.
|
||||
* @param callId - Foreground tool call to attach to.
|
||||
* @param signal - Remote stream cancellation, including Client disconnect.
|
||||
* @returns One Host-computed remaining duration, or no frames once the wait ended.
|
||||
*/
|
||||
@Remote({ mode: 'stream' }) async *attachWait(agent: Agent, callId: ToolCallId, signal: AbortSignal): AsyncIterable<{ remainingMs: number }>
|
||||
|
||||
/**
|
||||
* Foreground wait whose first settlement the Client decides: the Client
|
||||
* rejects with `ASK_TIMED_OUT` when its countdown ends, and this method maps
|
||||
* that code to the pending result.
|
||||
* @param request - Questions, live owner agent, and abort signal.
|
||||
* @param callId - Tool call identity the Client card is keyed by.
|
||||
* @param timeoutMs - Positive foreground wait in milliseconds.
|
||||
* @returns The answer when it arrives inside the window, otherwise a pending
|
||||
* result, also when no connected Client claimed the request by the deadline.
|
||||
* @throws {UserQuestionError} `BAD_TIMEOUT` for a non-integer, non-positive,
|
||||
* or oversized wait.
|
||||
*/
|
||||
async askTimed( request: AskUserQuestionRequest & { agent: Agent }, callId: ToolCallId, timeoutMs: number, ): Promise<TimedUserQuestionResult>
|
||||
|
||||
/**
|
||||
* Ask the scoped answerer waterfall and wait for the user's answer.
|
||||
*
|
||||
@@ -151,6 +276,8 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
|
||||
```
|
||||
|
||||
Types: [Agent](core.md) · [ToolCallId](core.md)
|
||||
|
||||
Source: [`packages/interaction/user-questions/src/index.ts`](../../packages/interaction/user-questions/src/index.ts)
|
||||
|
||||
<a id="user-questions-events"></a>
|
||||
|
||||
@@ -22,7 +22,7 @@ interface AskUserQuestionOption {
|
||||
|
||||
## 呈现意图
|
||||
|
||||
`AskUserQuestionIntent` 可选地声明一种已知的决策类型。它按 `kind` 打标签,因此可以增加新的意图;不认识某个标签的 UI 渲染通用选项列表。意图只改变呈现方式——遵循它的 UI 回答的仍是通用 UI 会发送的那些选项标签,因此调用方两种情况下读到的回答字段相同。`approve` 指名肯定选项,而不依赖选项顺序。`ask()` 会拒绝两种无法由类型系统表达的情况:`approve` 未指向该问题自身的任何选项,以及为没有 `detail` 的问题指定意图。
|
||||
`AskUserQuestionIntent` 可选地声明一种已知的决策类型。它以 `kind` 作为标记,以便后续加入更多意图;不认识某个标记的 UI 会渲染通用选项列表。意图只改变呈现——遵循它的 UI 回答的选项标签与通用 UI 相同,因此调用方读到的答案字段一致。`approve` 指明表示同意的选项,而不依赖选项顺序。`ask()` 拒绝两种类型无法表达的断言:`approve` 未命名该问题自己的任何选项,以及没有 `detail` 的问题声明了意图。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -103,9 +103,96 @@ interface AskUserQuestionAnswer {
|
||||
}
|
||||
```
|
||||
|
||||
计时回答器收到 `wait: { callId, timed: true }`。调用 id 标识卡片及前台等待。Client 调用 `attachWait` 接手等待并取得剩余时长;传输层转发原事件,不检查或改写这些字段。不带 `timed` 的 `wait` 表示无限期等待的具名卡片请求;没有 `wait` 则表示不带卡片 key 的阻塞请求。
|
||||
|
||||
```ts type-equiv
|
||||
/** Client-safe payload declared for the user-question answerer waterfall. */
|
||||
interface AskUserQuestionRequestEvent {
|
||||
/** Questions to display. */
|
||||
questions: AskUserQuestionItem[]
|
||||
/** Agent identity projected to the corresponding Client Context in transit. */
|
||||
agent?: Agent
|
||||
/** Cancellation lifetime of the pending request. */
|
||||
signal?: AbortSignal
|
||||
/**
|
||||
* Tool call the Client card is keyed by. Timed answerers attach to the
|
||||
* business wait stream before starting their local countdown.
|
||||
*/
|
||||
wait?: {
|
||||
/** Tool call the Client card is keyed by. */
|
||||
callId: ToolCallId
|
||||
/** True for a foreground wait that requires a Client claim; absent for indefinite waits. */
|
||||
timed?: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 计时问题与迟到回复
|
||||
|
||||
计时问题在有界的前台窗口内等待,然后让 agent 继续独立工作。用户在窗口内回答,工具返回整批答案;否则 `TimedUserQuestionResult` 为 `{ pending: true, callId }`。pending 表示问题仍可回答,不是空答案、拒绝、确认或授权。
|
||||
|
||||
```ts type-equiv
|
||||
/** Timed ask result returned when the foreground answer window closes. */
|
||||
type TimedUserQuestionResult = AskUserQuestionAnswer | { pending: true; callId: ToolCallId }
|
||||
```
|
||||
|
||||
Client 接手通过业务 Remote stream 表达,不等同于 Gateway 投递。`TimedQuestionWait` 保存原 Host deadline,仅在没有接手记录时运行计时器。`attachWait` stream 发出当前剩余时长,Client 用本地时钟倒计时。聚焦、编辑和「慢慢回答」状态仍归本地所有。stream 取消释放相应接手记录;最后一个接手方离开后,Host 恢复原 deadline。无人接手时超时中止前台 waterfall 专属的 signal 并返回 pending,不取消 Turn。Gateway 与 `api-remotes` 不包含问答专属计时策略。
|
||||
|
||||
`userQuestions` Session 投影把现有事件折叠成可回答的问题集合与已结算的集合;不记录任何等待状态、聚焦状态或 deadline。每个 `request/header` 都记录了模型看到的确切工具 schema。只有请求头声明了带 `timeout` 参数的 timed `ask_user_question` schema,投影才跟踪原生调用;阻塞式 legacy 工具从不声明它。原生 `tool/call` 打开一个问题。`tool/result` 只在两种情况下把它标为 `continued`:带 pending 载荷,或是 Session resume 修复补写的 `TOOL_OUTCOME_UNKNOWN` 合成结果;回答批次把它连同该批次移入 `settled`,其他结果或失败都丢弃它。`run_code` 的 PTC 子调用若在 `tool/ptc-dispatch` 结果中返回 pending,也会进入投影,尽管请求头只列出 `run_code`。迟到回复在消息获准成为 `user/message` 时,才把已继续的问题连同消息中的回答批次移入 `settled`:计时调用自己的结果记录的是超时,transcript 行无处可读用户最终的选择。排队的回复可在准入前取消,此时问题仍可回答。回复正文不含可读批次时,该调用以零答案结算;回复指向的调用已不在可回答集合中时不改变任何内容。
|
||||
|
||||
```ts type-equiv
|
||||
/** `open` while the tool call may still return the answer; `continued` once the answer can only arrive as a new turn. */
|
||||
type UserQuestionState = 'open' | 'continued'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One unanswered timed `ask_user_question` call reconstructed from the Session log. */
|
||||
interface PendingUserQuestion {
|
||||
readonly callId: ToolCallId
|
||||
readonly questions: readonly AskUserQuestionItem[]
|
||||
readonly state: UserQuestionState
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One timed `ask_user_question` call and the answers it settled with: the
|
||||
* batch its own result carried when the user answered inside the window,
|
||||
* otherwise the batch its late reply carried, because that call's own result
|
||||
* recorded the timeout. A transcript row reads what the user finally answered
|
||||
* from here, and only a call listed here was a timed one.
|
||||
*/
|
||||
interface SettledUserQuestion {
|
||||
/** The settled call. */
|
||||
readonly callId: ToolCallId
|
||||
/** The recorded batch, one entry per question; empty when the late reply carried none. */
|
||||
readonly answers: readonly AskUserQuestionAnswerItem[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Both halves of one Session's timed `ask_user_question` state, as every
|
||||
* Client reads them. A call made while the blocking legacy tool was in
|
||||
* effect appears in neither half.
|
||||
*/
|
||||
interface UserQuestionProjectionView {
|
||||
/** Calls that can still take an answer, in ask order. */
|
||||
readonly active: readonly PendingUserQuestion[]
|
||||
/** Calls an answer settled, in settlement order. */
|
||||
readonly settled: readonly SettledUserQuestion[]
|
||||
}
|
||||
```
|
||||
|
||||
开放中的问题只能通过 waterfall 回答。问题变为 `continued` 后,`answer` Remote 方法向所属 agent 排入一条用户消息,并在其空闲时唤醒它。消息的 source 为 `user-question-reply`,outcome 为 `answered`,正文是 `answer_to_pending_question` 载荷。开放或未知调用返回 `false`;回复仍在排队时的第二次提交以 `REPLY_QUEUED` 拒绝,Host 重启后也一样;遗漏或重复问题的回答批次以 `BAD_ANSWER` 拒绝。
|
||||
|
||||
收起 Client 面板不会发送内容。前台超时只结束工具等待。已继续的问题没有期限;在回复进入会话前,问题仍可回答。
|
||||
|
||||
Host 重启后,Session 重开时 Remote 层会恢复根 agent。之后的迟到回复会作为新一轮进入。
|
||||
|
||||
## 错误
|
||||
|
||||
`UserQuestionError` 继承 `HarnessError`,因此 `ctx.tools.execute()` 会保留 `{ name, code }`,用于面向模型的工具失败,如 `EMPTY_QUESTIONS`、`NO_PROVIDER`、`ASK_ABORTED` 或 UI 侧取消。
|
||||
`UserQuestionError` 继承 `HarnessError`,因此 `ctx.tools.execute()` 会保留 `{ name, code }`,用于面向模型的工具失败,如 `EMPTY_QUESTIONS`、`BAD_INTENT`、`NO_PROVIDER`、`ASK_ABORTED` 或 UI 侧取消。
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable error taxonomy for user-questions failures. */
|
||||
@@ -132,6 +219,44 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
`ctx.userQuestions`: validation plus the scoped answerer waterfall.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Answer a continued question. The reply is steered into the agent as a
|
||||
* user message whose source names the call; that message is also the
|
||||
* record that closes the question in the projection.
|
||||
* @param agent - Live root agent for the owning Session.
|
||||
* @param callId - Continued question identity.
|
||||
* @param answer - Complete structured answer batch, one item per question of the call.
|
||||
* @returns Whether the question is still continued; an accepted reply stays
|
||||
* queued until the agent admits its user message.
|
||||
* @throws {UserQuestionError} `BAD_ANSWER` when the batch does not name each
|
||||
* question of the call exactly once, or `REPLY_QUEUED` when a reply is
|
||||
* already waiting for admission.
|
||||
*/
|
||||
@Remote answer(agent: Agent, callId: ToolCallId, answer: AskUserQuestionAnswer): boolean
|
||||
|
||||
/**
|
||||
* Let one answer UI hold a live timed wait. Closing the stream releases its claim.
|
||||
* @param agent - Live root agent owning the question.
|
||||
* @param callId - Foreground tool call to attach to.
|
||||
* @param signal - Remote stream cancellation, including Client disconnect.
|
||||
* @returns One Host-computed remaining duration, or no frames once the wait ended.
|
||||
*/
|
||||
@Remote({ mode: 'stream' }) async *attachWait(agent: Agent, callId: ToolCallId, signal: AbortSignal): AsyncIterable<{ remainingMs: number }>
|
||||
|
||||
/**
|
||||
* Foreground wait whose first settlement the Client decides: the Client
|
||||
* rejects with `ASK_TIMED_OUT` when its countdown ends, and this method maps
|
||||
* that code to the pending result.
|
||||
* @param request - Questions, live owner agent, and abort signal.
|
||||
* @param callId - Tool call identity the Client card is keyed by.
|
||||
* @param timeoutMs - Positive foreground wait in milliseconds.
|
||||
* @returns The answer when it arrives inside the window, otherwise a pending
|
||||
* result, also when no connected Client claimed the request by the deadline.
|
||||
* @throws {UserQuestionError} `BAD_TIMEOUT` for a non-integer, non-positive,
|
||||
* or oversized wait.
|
||||
*/
|
||||
async askTimed( request: AskUserQuestionRequest & { agent: Agent }, callId: ToolCallId, timeoutMs: number, ): Promise<TimedUserQuestionResult>
|
||||
|
||||
/**
|
||||
* Ask the scoped answerer waterfall and wait for the user's answer.
|
||||
*
|
||||
@@ -151,6 +276,8 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
|
||||
```
|
||||
|
||||
Types: [Agent](core.zh.md) · [ToolCallId](core.zh.md)
|
||||
|
||||
Source: [`packages/interaction/user-questions/src/index.ts`](../../packages/interaction/user-questions/src/index.ts)
|
||||
|
||||
<a id="user-questions-events"></a>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 6cc2cd9ced70bb09
|
||||
zh: 1c0e6cb4ba2ed637
|
||||
/tool-schema-catalog/tool-package-map:
|
||||
en: 72e2a375567f5850
|
||||
zh: b51b8ea5b52814fd
|
||||
en: 167287d095eee2f5
|
||||
zh: 18720d5e81379d2f
|
||||
/tool-schema-catalog/deepseek-ai-dsh-plugin-manager:
|
||||
en: e0143916bb634762
|
||||
zh: e0143916bb634762
|
||||
@@ -54,8 +54,8 @@
|
||||
en: e713a4a11a15d13c
|
||||
zh: e713a4a11a15d13c
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-ask-user/ask-user-question:
|
||||
en: f42b7f17ad59577a
|
||||
zh: 7e1eee22ff35d4b1
|
||||
en: 055268ba4dcd32f9
|
||||
zh: a1a3c4bfcbf698ed
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tools:
|
||||
en: 76fbbf0fe38aef10
|
||||
zh: 76fbbf0fe38aef10
|
||||
|
||||
@@ -18,7 +18,7 @@ This table connects model-visible tool names to the plugin package and service s
|
||||
| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager`, `ctx.sandboxPolicy` | `tool/call`, `tool/result`, `user/message` | - | - |
|
||||
| `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after an answer or timeout`, `late user/message` | - | ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely. |
|
||||
| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.ptcRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. |
|
||||
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. |
|
||||
| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. With a job registry composed every call registers with the generic `ctx.jobs` runtime as it starts, collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; without one, or with `enableRunInBackground: false`, the tool registers a foreground-only schema without the `run_in_background` parameter. |
|
||||
@@ -515,7 +515,7 @@ Ask the user a concise question when you need confirmation, a choice, or missing
|
||||
|
||||
Source: [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts)
|
||||
|
||||
ask_user_question pauses the tool call until the active UI provider returns a human answer.
|
||||
ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely.
|
||||
|
||||
<a id="deepseek-aidsh-tools"></a>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager`, `ctx.sandboxPolicy` | `tool/call`, `tool/result`, `user/message` | - | - |
|
||||
| `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after a UI/provider answers the question` | - | ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。 |
|
||||
| `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after an answer or timeout`、`late user/message` | - | ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才启用前台超时与 pending 结果,同时问题仍可回答;timed 模式内 `timeout: -1` 让本次调用无限期阻塞。 |
|
||||
| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.ptcRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 |
|
||||
| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 |
|
||||
| `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`、`tool/result` | - | bash 工具是 bash 执行器 seam 面向模型的消费方。组合中有 job 注册表时,每次调用一启动就注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;没有注册表或 `enableRunInBackground: false` 时,工具注册不带 `run_in_background` 参数的纯前台 schema。 |
|
||||
@@ -519,7 +519,7 @@
|
||||
|
||||
来源:[`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts)
|
||||
|
||||
ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。
|
||||
ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才启用前台超时与 pending 结果,同时问题仍可回答;timed 模式内 `timeout: -1` 让本次调用无限期阻塞。
|
||||
|
||||
<a id="deepseek-aidsh-tools"></a>
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: cd8120cba5566e19
|
||||
zh: 65359436088c8bc5
|
||||
/dsh-unified-corner-radius-standard/select-the-radius:
|
||||
en: 51cd57ec0fe8bb03
|
||||
zh: 53784eee8aa8be6e
|
||||
en: 06ddd6bf9ef1e48c
|
||||
zh: 4c42d0c9266a00e8
|
||||
/dsh-unified-corner-radius-standard/reuse-tokens-and-preserve-deliberate-shapes:
|
||||
en: fb6d50f3139ad186
|
||||
zh: 668cb0c02bd22167
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ Prefer an existing shared size variant when a proposed control falls between the
|
||||
|
||||
Concrete defaults: `Button` uses H28/R8 for `sm` and H36/R12 for `md`, including outline variants. Standard H32 inputs and H36 selectors use R12. The Workspace files and New terminal guide entries both use R20, including title-only and described variants. Reserve R28 for their enclosing panel or dialog, not those entries.
|
||||
|
||||
Code blocks, diffs, and file previews share R16: changed-file entry cards, delivered-file cards, generic file attachment cards, hover preview panels, and the file-icon tiles inside entry cards. Their hover fills keep the same contour.
|
||||
Code blocks, diffs, and file previews share R16: changed-file entry cards, delivered-file cards, generic file attachment cards, and hover preview panels. The 40px file-icon tiles inside changed-file and delivery cards use R10 on all four corners as a component-specific exception. Their hover fills keep the same contour.
|
||||
|
||||
<a id="shapes"></a>
|
||||
## Reuse tokens and preserve deliberate shapes
|
||||
|
||||
@@ -40,7 +40,7 @@ H 表示以 CSS 像素计、包含边框的设计外部高度。R 表示浏览
|
||||
|
||||
具体默认值:`Button` 的 `sm` 使用 H28/R8,`md` 使用 H36/R12,包含描边变体。标准 H32 输入框和 H36 选择器使用 R12。“工作区文件”和“新建终端”引导入口统一使用 R20,包含仅标题与带描述的变体。R28 留给包围它们的面板或对话框,不用于这些入口。
|
||||
|
||||
代码块、diff 与文件预览共用 R16:包括改动文件入口卡片、交付文件卡片、通用附件文件卡片、悬停预览浮层,以及入口卡片内部的文件图标底框。各自的 hover 保持同一轮廓。
|
||||
代码块、diff 与文件预览共用 R16:包括改动文件入口卡片、交付文件卡片、通用附件文件卡片和悬停预览浮层。改动文件与交付文件卡片内部的 40px 文件图标底框作为组件特例,四角统一使用 R10。各自的 hover 保持同一轮廓。
|
||||
|
||||
<a id="shapes"></a>
|
||||
## 复用 token,保留有意设计的形状
|
||||
|
||||
Loaded 100 of 677 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user