refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
README.md: 131f76fc8bc7b63449e022ad91fd8453c10d5f01
README.zh.md: 8d0cd4e82b8bf4256db9b0f3cb868630f57ce68e
README.md: ecf463da619662fe158511079e7773ee3c627ac8
README.zh.md: f8922b160cdc6feca94dea998163c25d803a5535

View File

@@ -2,19 +2,19 @@
English | [中文](README.zh.md)
Conversation domain: skeleton (header/tabs/composer/empty state), chat view (grouped step-summary flow, streaming tail isolation, and turn status), composer dock (session stats sticky with the input), input dock (queue rows plus the todo plan strip), details shell, and scope-addressed ConversationService. Tool presentation belongs to [`ui-tool`](../ui-tool/README.md).
Conversation domain: skeleton (header/tabs/composer/empty state), chat view (grouped step-summary flow, streaming tail isolation, and turn status), composer dock (session stats sticky with the input), input dock (queue rows plus the todo plan strip), details shell, and scope-addressed ConversationController. Tool presentation belongs to [`ui-tool`](../ui-tool/README.md).
Compaction renders as one collapsed row at the checkpoint's flow position without replacing the transcript above it. Automatic compaction uses the context-compacted title. Every completed marker with a loaded `compact/summary` event shows the replaced-item and estimated-token counts and discloses the summary on click. Manual `/compact` starts as a running `compact` row; on successful settlement its explicit summary-event reference folds that command into the checkpoint row under the same React key. A completed checkpoint keeps the context-compaction icon at rest and replaces it with the collapsed or expanded disclosure only on hover or keyboard focus. Input rejection, no compactable history, cancellation, and failure retain the generic command row and its handler-authored text. Pairing never depends on adjacency because durable context may be injected while compaction is running. The framed checkpoint payload is model-facing and never renders; when the cited `compact/summary` event is outside the loaded window, the checkpoint remains visible but non-expandable.
Compaction renders as one collapsed row at the checkpoint's flow position without replacing the transcript above it. Automatic compaction uses the context-compacted title. Every completed marker with a loaded `compaction/summary` event shows the replaced-item and estimated-token counts and discloses the summary on click. Manual `/compact` starts as a running `compact` row; on successful settlement its explicit summary-event reference folds that command into the checkpoint row under the same React key. A completed checkpoint keeps the context-compaction icon at rest and replaces it with the collapsed or expanded disclosure only on hover or keyboard focus. Input rejection, no compactable history, cancellation, and failure retain the generic command row and its handler-authored text. Pairing never depends on adjacency because durable context may be injected while compaction is running. The framed checkpoint payload is model-facing and never renders; when the cited `compaction/summary` event is outside the loaded window, the checkpoint remains visible but non-expandable.
The resident conversation shell survives no-session and session transitions. Without a current session it locks message actions and presents the whole dashed composer card as a trigger for the root-scoped `conversation.hero.workspace` Workspace picker; the textarea remains read-only and keyboard-accessible. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. The root always owns the same scrollport and Hero/composer subtree; separate strict-session header and body outlets fill their regions when the first Session arrives, so the Workspace picker, scroll body, composer seat, and textarea retain their React and DOM identity. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it the scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). That scrollport reserves its scrollbar gutter unconditionally, and a view opting into a composer overlay leaves it a scroll container, so the input card keeps one horizontal position whether or not the transcript scrolls and whichever view tab is shown ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host.
Another plugin can make one session's composer inert through `ctx.conversation.blocks`: it sets a block carrying its own localized reason, and the bar renders the same disabled textarea with that reason as the placeholder — the no-workspace posture, reused. The push direction is the constraint, not a preference: the plugins that know a session cannot send (ui-model, when no adapter serves its route) already depend on this package, so this package cannot read them. The model seat is the one control a block leaves live — every block this contract has is cleared by choosing a model, so locking it too would leave the composer asking for the only thing it prevents. A block is an affordance only; the Host refuses a prompt it cannot route regardless of what any client disables. The no-workspace state wins when both hold, because picking a workspace is the earlier prerequisite.
Another plugin can make one session's composer inert through `ctx.conversation.blocks`: it sets a block carrying its own localized reason, and the bar renders the same disabled textarea with that reason as the placeholder — the no-workspace posture, reused. The push direction is the constraint, not a preference: the plugins that know a session cannot send (ui-model-selection, when no adapter serves its route) already depend on this package, so this package cannot read them. The model seat is the one control a block leaves live — every block this contract has is cleared by choosing a model, so locking it too would leave the composer asking for the only thing it prevents. A block is an affordance only; the Host refuses a prompt it cannot route regardless of what any client disables. The no-workspace state wins when both hold, because picking a workspace is the earlier prerequisite.
The view ring is a slot: the strict session-body registration declares the session-scoped `'conversation.view'` list in its `children` table, that body renders the active entry through its renderSlot share (`only: <active id>`), and view tabs project from registration options (`id`/`order`/`label`). The chat view is this package's own entry; plugins such as ui-trajectory contribute tabs through `ctx.slots.register`, and each view owns its chrome.
Chat business rows are independent registry contributions rather than a closed built-in union. A client plugin declaration-merges its typed `ChatNodeDataMap` key, registers a `ConversationNodeDefinition` on `ctx.conversationEvents`, and registers the matching keyed renderer on `conversation.chat.node`; it does not modify Session folds or a central renderer switch. The [Conversation Node cookbook](../../../docs/cookbook/adding-a-conversation-node.md) covers stable event ids, append/prepend replay, Location data, and renderer constraints.
Approvals take over the composer through the chain this package declares: `ApprovalPanel` registers as a selector-routed `'conversation.composer'` entry (the ui-question pattern) and occupies the composer in place of the InputBar while an approval wait is pending (amber strip, justification headline, paired command line from the running call's args, one-shot refuse/allow). The `PendingApproval` domain face in `contract/slots.ts` owns the wire encoding — the `ApprovalResponsePayload` value with the audit correlation — over the runtime's `PendingWait` carrier; the broadcast `approval/resolved` frame settles the wait and restores the composer. The runtime manager projects every approval or question wait through `SessionSummary.pendingInteraction`, including sessions never instantiated; `ui-workspace` owns its sidebar presentation. Pending waits leave the message flow entirely: questions (ui-question) and approvals (ApprovalPanel) both answer through the composer takeover, so no display-only placeholder card remains. The composer's bottom-row Access seat mounts `PermissionSelect`, fed by the host-computed `permissions` projection through the standard-kit `useProjection` (key absence hides the chip); the chip opens a Menu-primitive dropdown whose kebab-case preset names render as title-case labels. Safe preset picks submit `/permission <preset>` immediately through the bar's injected `command` callback, while `danger-full-access` is presented as `Full access` and first opens an in-page Modal risk confirmation. The enabling action stays disabled until the user checks the acknowledgement; cancel, Escape, close, and mask click submit nothing.
Approvals take over the composer through the chain this package declares: `ApprovalPanel` registers as a selector-routed `'conversation.composer'` entry (the ui-user-questions pattern) and occupies the composer in place of the InputBar while an approval wait is pending (amber strip, justification headline, paired command line from the running call's args, one-shot refuse/allow). The `PendingApproval` domain face in `contract/slots.ts` owns the wire encoding — the `ApprovalResponsePayload` value with the audit correlation — over the runtime's `PendingWait` carrier; the broadcast `approval/resolved` frame settles the wait and restores the composer. The runtime manager projects every approval or question wait through `SessionSummary.pendingInteraction`, including sessions never instantiated; `ui-workspace` owns its sidebar presentation. Pending waits leave the message flow entirely: questions (ui-user-questions) and approvals (ApprovalPanel) both answer through the composer takeover, so no display-only placeholder card remains. The composer's bottom-row Access seat mounts `PermissionSelect`, fed by the host-computed `permissions` projection through the standard-kit `useProjection` (key absence hides the chip); the chip opens a Menu-primitive dropdown whose kebab-case preset names render as title-case labels. Safe preset picks submit `/permission <preset>` immediately through the bar's injected `command` callback, while `danger-full-access` is presented as `Full access` and first opens an in-page Modal risk confirmation. The enabling action stays disabled until the user checks the acknowledgement; cancel, Escape, close, and mask click submit nothing.
The session header renders the session-scoped `'conversation.session.header.actions'` list beside the title and the independent `'conversation.session.header.utilities'` list at the right edge. Session context and lineage controls remain in `actions`; optional Session utilities cannot reorder or move them. The composer chain currency includes the current conversation `session`; ui-subagent selects one-shot or parent-unavailable addressed sessions for reason-specific read-only copy, while the ordinary InputBar keeps every addressed child Send-only because the continuation service exposes no public per-Activation cancellation operation and `session.cancel` would bypass its ownership.
@@ -26,7 +26,7 @@ The chat view keeps Tool placement but delegates Tool presentation. Each ordered
The chat flow projects consecutive model-retry nodes across retry turns into one stable, muted status row updated to the latest attempt; every retry event remains in the runtime snapshot and session log. Its frontend countdown anchors the scheduled delay to client receipt, avoiding host/browser clock skew, rounds remaining time up to seconds, and has a one-second floor. The latest unresolved retry uses a left-to-right text shimmer. Subsequent turn facts distinguish an attempt that started from one cancelled during backoff, while the Host running bit only controls the live animation; the row then shows a static completed or cancelled label. Normal policy rows show the finite retry maximum; always policy rows show `∞`. Activating the row reveals the latest exact retry delay and failure message. The client runtime removes each failed step's streaming tail before its retry node arrives, while the status remains visible after a later attempt succeeds. An unretried terminal failure renders as a persistent inline status at its turn boundary, showing the display-safe durable message and optional error code without offering an action the Host cannot fulfill; AUTH copy never echoes provider-supplied credential fragments.
`TodoDock` takes the `'conversation.input.dock'` list slot at `order: 0` — before Goal and Queue — and is the plan strip: it reads the host-computed `todos` projection via `useProjection` (standing plan: latest `todo/write` with no later `turn/start`) and renders `TodoPanel`, which takes the plain list, hides itself while the list is empty, and starts collapsed as a header of title plus its own `·`-joined per-status counts (localized, `1 completed · 2 in progress · 1 pending`, zero-count segments omitted). The dock adapter owns selection so the panel stays a pure function of its props. Anything the input-zone composer chain hides (a `conversation.composer` takeover such as ui-question's) hides the whole dock, this strip included. The `todo_write` Tool row belongs to [`ui-tool`](../ui-tool/README.md).
`TodoDock` takes the `'conversation.input.dock'` list slot at `order: 0` — before Goal and Queue — and is the plan strip: it reads the host-computed `todos` projection via `useProjection` (standing plan: latest `todo/write` with no later `turn/start`) and renders `TodoPanel`, which takes the plain list, hides itself while the list is empty, and starts collapsed as a header of title plus its own `·`-joined per-status counts (localized, `1 completed · 2 in progress · 1 pending`, zero-count segments omitted). The dock adapter owns selection so the panel stays a pure function of its props. Anything the input-zone composer chain hides (a `conversation.composer` takeover such as ui-user-questions's) hides the whole dock, this strip included. The `todo_write` Tool row belongs to [`ui-tool`](../ui-tool/README.md).
`QueueDock` is the terminal input-dock entry at `order: 20`. It hides while empty, renders one pending row directly, and defaults two or more rows to a collapsed `"<n> 条排队消息"` header whose button expands or collapses the complete list. The header exposes `aria-expanded` and `aria-controls`; the expanded list scrolls within a 180px height bound. An active edit or mutation keeps its rows visible, and emptying the queue restores the collapsed default for the next queue. Each visible ordinary-session row remains a single-line preview with its exact-occurrence edit, delete, and strict-steer actions; addressed subagents retain the rows as a read-only projection because their continuation transport does not expose queue mutation. If strict steer loses to a closed window, the original occurrence remains queued for normal delivery; if the driver already claimed it, normal delivery is already underway. Neither converged race displays a failure, while transport and unknown failures do.
@@ -38,7 +38,7 @@ Per-session UI state for selection and the active view lives in the declared cha
Image intake accepts paste and whole-page drop: the bar binds document-level drag listeners (the composer-bar slot is `kind: 'single'`, so at most one bar binds them) and shows the `DropOverlay` atom while a file drag is over the window — text drags pass through untouched, and a locked or busy composer shows the blocked overlay and refuses the drop. Both gestures feed one intake pre-check against the host's `imageLimits` projection (count, per-image bytes, aggregate bytes): an addition that would break a limit is refused as a whole batch with an immediate banner naming the limit, and never enters the rail. Host-side rejections that arrive anyway surface as product copy mapped from the `attachment-error` reason (`image-labels.ts` `attachmentErrorText`); reasons the user cannot act on fold into one send-failed line carrying the reason code, and non-attachment error codes keep their developer-facing message plus code.
The composer bar declares session-scoped single seats for `'conversation.input.plan'` (right of the local access-mode control) and `'conversation.input.model'` (immediately before the pending indicator and send/stop controls), plus list slots for overlay, dock, left, and right input extensions. Feature packages own each control and its state; ui-conversation supplies placement, the `locked` owner prop, and the standard slot shares. The leading plus button is a Command launcher, not an attachment surface: it asks the session's `SlashController` to open only the `/` trigger's `command` source over the current textarea selection, while ui-slash's existing `MenuView` remains the sole floating menu and pick path. No file row, file input, upload protocol, or second menu component is introduced. While the `plan` projection's effective target is plan mode, InputBar swaps its textarea placeholder to the plan-task wording, localized through the `conversation` locale namespace this package registers (the `placeholder.plan` / `hint.plan` keys) and shared verbatim with the claimed `/plan` command hint (a host-folded value read through the standard-kit `useProjection`; owner-supplied placeholders win). A pending composer takeover remains mounted when another conversation view is active so the blocked agent can still receive its answer; without a pending interaction, the active-session composer belongs to Chat. The composer-bar slot itself is `session-maybe`: with no current session the same bar keeps message actions inert (machine faces absent, `disabled` owner prop), while the whole dashed card opens the existing Workspace picker by pointer and the read-only textarea opens it through Enter or Space. Disabled controls release pointer events to the card, and the card contains `pointerdown` so the open picker's outside-close cannot race a reopen. The bar never swaps in a parallel tree, so the textarea DOM survives Workspace selection; strict-session control seats stay empty until a session exists.
The composer bar declares session-scoped single seats for `'conversation.input.plan'` (right of the local access-mode control) and `'conversation.input.model'` (immediately before the pending indicator and send/stop controls), plus list slots for overlay, dock, left, and right input extensions. Feature packages own each control and its state; ui-conversation supplies placement, the `locked` owner prop, and the standard slot shares. The leading plus button is a Command launcher, not an attachment surface: it asks the session's `InputTriggerController` to open only the `/` trigger's `command` source over the current textarea selection, while ui-input-trigger's existing `MenuView` remains the sole floating menu and pick path. No file row, file input, upload protocol, or second menu component is introduced. While the `plan` projection's effective target is plan mode, InputBar swaps its textarea placeholder to the plan-task wording, localized through the `conversation` locale namespace this package registers (the `placeholder.plan` / `hint.plan` keys) and shared verbatim with the claimed `/plan` command hint (a host-folded value read through the standard-kit `useProjection`; owner-supplied placeholders win). A pending composer takeover remains mounted when another conversation view is active so the blocked agent can still receive its answer; without a pending interaction, the active-session composer belongs to Chat. The composer-bar slot itself is `session-maybe`: with no current session the same bar keeps message actions inert (machine faces absent, `disabled` owner prop), while the whole dashed card opens the existing Workspace picker by pointer and the read-only textarea opens it through Enter or Space. Disabled controls release pointer events to the card, and the card contains `pointerdown` so the open picker's outside-close cannot race a reopen. The bar never swaps in a parallel tree, so the textarea DOM survives Workspace selection; strict-session control seats stay empty until a session exists.
The chat stats line takes its token accounting from the generic token-meter `tokenUsage` projection read through the standard-kit `useProjection`: billed input is uncached input plus cache reads and writes; cache hit divides cache reads by that total. The turn and step counts, the LLM and tool wall times, and the latency/throughput group all ride the whole-log `sessionStats` projection (host-folded from step boundaries, first-token chunks, tool pairs, and assembled messages), so paging and compaction cannot change any strip figure; an assembly without that unit falls back to the window fold over visible nodes, whose fields mirror the projection's. The strip averages each recorded step's TTFT and divides sampled output tokens by their summed decode spans into a latency/throughput group localized through the `conversation` locale namespace (`TTFT avg … · … tok/s` in English); a step missing a timing boundary or a usage sample drops out of those figures instead of skewing them, and durable count, token, and context groups remain visible when compaction leaves no assistant node in the loaded window. The turn-count, step-count, duration, cache, and token labels use the same namespace. Each settled turn additionally appends hover-revealed `TTFT {s}s · {tps} tok/s` labels to its assistant footer after the `Ran for` duration — the turn's first-step TTFT and its turn-aggregate decode throughput — gated on the turn's timing being in the loaded window (a contiguous log suffix, so an in-window turn carries every one of its steps) and omitting whichever figure is unrecorded. A deployment without token-meter drops the token groups; when the line overflows, it elides with an ellipsis and a delayed hover tooltip carries the full text only while actually clipped. Context occupancy renders as the composer's trailing ContextMeter: a 14px occupancy ring after the model seat, fed by `contextPressure` and rendered only once both a numerator and a route capacity are known, that click-opens a panel pairing the `percent used` header and `~used / capacity` figures with a color-segmented bar and `~`-prefixed heuristic composition rows (system prompt, tools, messages) from the `contextBreakdown` projection. The ring and header read `projectedTokens` — the provider sample carried forward over the surface's movement since — so a compaction registers immediately instead of after a further turn; the composition rows stay wholly heuristic and therefore still do not sum to the header ([rationale](../../llm/token-meter/README.md)). Occupancy is deliberately an approximation: numerator and capacity are independent last-wins projection fields, not one atomic request observation.

View File

@@ -2,13 +2,13 @@
[English](README.md) | 中文
会话领域:骨架(标题栏/标签页/编辑器/空状态)、聊天视图(分组步骤摘要流、流式尾部隔离与轮次状态)、编辑器 dock与输入区一同 sticky 的会话统计行)、输入区 dock队列行加 todo 计划条)、详情壳层,以及按 scope 寻址的 ConversationService。工具展示属于 [`ui-tool`](../ui-tool/README.md)。
会话领域:骨架(标题栏/标签页/编辑器/空状态)、聊天视图(分组步骤摘要流、流式尾部隔离与轮次状态)、编辑器 dock与输入区一同 sticky 的会话统计行)、输入区 dock队列行加 todo 计划条)、详情壳层,以及按 scope 寻址的 ConversationController。工具展示属于 [`ui-tool`](../ui-tool/README.md)。
压缩compaction在检查点自身的消息流位置渲染为一行折叠标记不替换其上方的 transcript文本记录。自动压缩使用「上下文已压缩」标题。每个已加载对应 `compact/summary` 事件的完成标记都会显示被替换条目数量和估算 token 数量,并可点击展开摘要。手动 `/compact` 开始时显示为运行中的 `compact` 行;成功结算后,其显式摘要事件引用会在保持同一 React key 的前提下把该命令折叠进检查点行。完成的检查点静止时保留上下文压缩context compaction图标仅在悬停或键盘聚焦时将其替换为收起展开指示图标。输入被拒绝、没有可压缩历史、取消和失败时仍使用通用命令行及处理器撰写的文本。配对绝不依赖相邻关系因为压缩运行期间可能注入持久上下文。面向模型的带框检查点载荷绝不渲染被引用的 `compact/summary` 事件位于已加载窗口之外时,检查点仍然可见但不可展开。
压缩compaction在检查点自身的消息流位置渲染为一行折叠标记不替换其上方的 transcript文本记录。自动压缩使用「上下文已压缩」标题。每个已加载对应 `compaction/summary` 事件的完成标记都会显示被替换条目数量和估算 token 数量,并可点击展开摘要。手动 `/compact` 开始时显示为运行中的 `compact` 行;成功结算后,其显式摘要事件引用会在保持同一 React key 的前提下把该命令折叠进检查点行。完成的检查点静止时保留上下文压缩context compaction图标仅在悬停或键盘聚焦时将其替换为收起展开指示图标。输入被拒绝、没有可压缩历史、取消和失败时仍使用通用命令行及处理器撰写的文本。配对绝不依赖相邻关系因为压缩运行期间可能注入持久上下文。面向模型的带框检查点载荷绝不渲染被引用的 `compaction/summary` 事件位于已加载窗口之外时,检查点仍然可见但不可展开。
常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会锁定消息操作,并让整张虚线编辑器卡片成为根作用域 `conversation.hero.workspace` Workspace picker 的入口textarea 保持只读且支持键盘操作。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。根组件始终拥有同一个滚动容器与 Hero编辑器子树首个会话到达时彼此独立的严格会话页头和主体 outlet 只填入各自区域,因此 Workspace picker、滚动主体、编辑器 seat 与 textarea 都保留原有 React 和 DOM identity。空白会话与活跃会话渲染相同的输入区主体InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段会话标题栏作为普通列 chrome仅显示当前会话标题和视图标签fork 谱系仍保留为会话数据,不投影到标题栏。其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock输入区 dock输入栏。该滚动容器无条件预留自己的滚动条槽选用编辑器 overlay 的视图也仍把它保留为滚动容器,因此无论对话记录是否滚动、无论展示哪个视图标签,输入卡片都保持同一个横向位置([决策](../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。
别的插件可以经 `ctx.conversation.blocks` 让某个会话的编辑器变为惰性:它设置一个携带自己本地化理由的 block输入栏就渲染同一个禁用的 textarea并把该理由作为 placeholder——复用无 Workspace 时的那套姿态。推送方向是约束而非偏好知道某会话发不出消息的插件ui-model在没有适配器服务其路由时本就依赖本包因此本包读不到它们。模型 seat 是 block 唯一保留可用的控件——这份约定里的每个 block 都靠选模型来解除把它一起锁上会让编辑器索要它自己拦下的那件事。block 只是提示性设计;无论客户端禁用了什么,宿主都会拒绝一个它无法路由的提示词。两者同时成立时以无 Workspace 姿态为准,因为选 Workspace 是更靠前的前提。
别的插件可以经 `ctx.conversation.blocks` 让某个会话的编辑器变为惰性:它设置一个携带自己本地化理由的 block输入栏就渲染同一个禁用的 textarea并把该理由作为 placeholder——复用无 Workspace 时的那套姿态。推送方向是约束而非偏好知道某会话发不出消息的插件ui-model-selection,在没有适配器服务其路由时)本就依赖本包,因此本包读不到它们。模型 seat 是 block 唯一保留可用的控件——这份约定里的每个 block 都靠选模型来解除把它一起锁上会让编辑器索要它自己拦下的那件事。block 只是提示性设计;无论客户端禁用了什么,宿主都会拒绝一个它无法路由的提示词。两者同时成立时以无 Workspace 姿态为准,因为选 Workspace 是更靠前的前提。
视图环是一个 slot严格会话主体注册在 `children` 表中声明会话作用域的 `'conversation.view'` 列表,并通过自身的 renderSlot share 渲染活跃配置项(`only: <active id>`);视图标签页则从注册选项(`id``order``label`投影而来。聊天视图是该包自身的配置项ui-trajectory 等插件通过 `ctx.slots.register` 贡献标签页,每个视图负责自己的 chrome。
@@ -24,7 +24,7 @@ Think 行默认保持折叠,并在不展开思维链的情况下暴露实时
聊天流会将跨重试轮次连续出现的模型重试节点投影为一个稳定的弱化状态行,并用最新一次尝试更新该行;每个重试事件仍保留在运行时快照与会话日志中。前端倒计时以客户端收到事件的时刻为计划延迟的起点,避免 Host 与浏览器的时钟偏差;剩余时间向上取整到秒,且下限为 1 秒。最近一次尚未完成的重试会显示从左到右的文字渐变动画。后续轮次事实用于区分已开始的尝试与在退避期间取消的尝试Host 的 running 位只控制实时动画随后该行会显示静态的已完成或已取消标签。normal 策略行显示有限重试上限always 策略行显示 `∞`。激活该行会显示最近一次重试的精确延迟和失败消息。客户端运行时会在相应重试节点到达前移除每个失败步骤的流式输出尾部;后续某次尝试成功后,该状态仍保持可见。未进入重试的终态失败会在其轮次边界渲染为持久的内联状态,展示适合显示的持久消息与可选错误码,但不会提供 Host 无法兑现的操作AUTH 文案绝不会回显提供方给出的凭据片段。
审批通过本包声明的链条接管编辑器:`ApprovalPanel` 注册为按选择器路由的 `'conversation.composer'` 配置项ui-question 模式),在审批等待未决期间取代 InputBar 占据编辑器(琥珀色条、理由标题、来自运行中调用参数的配对命令行、一次性的拒绝/允许)。`contract/slots.ts` 中的 `PendingApproval` 领域面在运行时 `PendingWait` 载体之上拥有 wire 编码——带审计关联的 `ApprovalResponsePayload` 值;广播的 `approval/resolved` 帧使等待落定并恢复编辑器。运行时 manager 会将所有审批或问题等待通过 `SessionSummary.pendingInteraction` 投影出来,未实例化的会话也不例外;`ui-workspace` 负责其侧边栏呈现。未决等待完全离开消息流问题ui-question与审批ApprovalPanel都经编辑器接管作答不再保留只读占位卡。编辑器底行的 Access 席位挂载 `PermissionSelect`,由 host 计算的 `permissions` 投影经标准工具包 `useProjection` 供数key 缺席即隐藏 chipchip 打开 Menu 原语下拉,其中 kebab-case 预设名渲染为 Title Case 标签;普通安全预设会立即经输入栏注入的 `command` 回调提交 `/permission <preset>`,而 `danger-full-access` 在界面中显示为 `Full access`,选择后先打开页面内的 Modal 风险确认。用户勾选确认项前启用按钮始终不可用取消、Escape、关闭按钮与点击遮罩都不会提交命令。
审批通过本包声明的链条接管编辑器:`ApprovalPanel` 注册为按选择器路由的 `'conversation.composer'` 配置项ui-user-questions 模式),在审批等待未决期间取代 InputBar 占据编辑器(琥珀色条、理由标题、来自运行中调用参数的配对命令行、一次性的拒绝/允许)。`contract/slots.ts` 中的 `PendingApproval` 领域面在运行时 `PendingWait` 载体之上拥有 wire 编码——带审计关联的 `ApprovalResponsePayload` 值;广播的 `approval/resolved` 帧使等待落定并恢复编辑器。运行时 manager 会将所有审批或问题等待通过 `SessionSummary.pendingInteraction` 投影出来,未实例化的会话也不例外;`ui-workspace` 负责其侧边栏呈现。未决等待完全离开消息流问题ui-user-questions与审批ApprovalPanel都经编辑器接管作答不再保留只读占位卡。编辑器底行的 Access 席位挂载 `PermissionSelect`,由 host 计算的 `permissions` 投影经标准工具包 `useProjection` 供数key 缺席即隐藏 chipchip 打开 Menu 原语下拉,其中 kebab-case 预设名渲染为 Title Case 标签;普通安全预设会立即经输入栏注入的 `command` 回调提交 `/permission <preset>`,而 `danger-full-access` 在界面中显示为 `Full access`,选择后先打开页面内的 Modal 风险确认。用户勾选确认项前启用按钮始终不可用取消、Escape、关闭按钮与点击遮罩都不会提交命令。
`TodoDock``order: 0` 占用 `'conversation.input.dock'` 列表 slot位于 Goal 与 Queue 之前),作为计划条读取 host 计算的 `todos` 投影(当前计划:其后没有更晚 `turn/start` 的最近一次 `todo/write`)并渲染 `TodoPanel`。面板接收纯列表,列表为空时自我隐藏;列表非空时默认折叠,表头显示标题及以 `·` 连接的各状态计数(如 `1 已完成 · 2 进行中 · 1 待处理`省略零计数。dock adapter 拥有 selection因此面板保持为 props 的纯函数。输入区 composer 链隐藏的一切也会隐藏整个 dock。`todo_write` 工具行属于 [`ui-tool`](../ui-tool/README.md)。
@@ -38,7 +38,7 @@ Host 带 placement 的 `session/queue` 快照也会携带待处理 steering。Qu
图片经粘贴与整页拖放进入:输入栏绑定 document 级拖拽监听composer-bar slot 为 `kind: 'single'`,同一时刻至多一个 bar 绑定),文件拖拽悬停窗口时显示 `DropOverlay` 原子组件——纯文本拖拽不受影响,锁定或忙碌的 composer 显示禁用遮罩并拒绝 drop。两种手势共用一条对宿主 `imageLimits` 投影的加入预检(数量、单图字节、总字节):会突破上限的加入整批拒收,立刻弹出点名上限的横幅,完全不进入附件栏。仍然到达的宿主侧拒绝按 `attachment-error` 原因映射为产品文案(`image-labels.ts``attachmentErrorText`);用户无法解决的原因折叠为一条带原因码的发送失败文案,非附件错误码保留开发者可读的原文加错误码。
输入栏为 `'conversation.input.plan'`(位于本地 access 模式控件右侧)和 `'conversation.input.model'`(渲染在 pending 指示器与发送/停止控件之前)声明会话作用域的单实例 seat并为 overlay、dock、left 和 right 输入扩展声明列表 slot。各功能包拥有相应控件及其状态ui-conversation 提供放置位置、`locked` owner prop 和标准 slot share。前置加号按钮是 Command launcher而非附件入口它要求当前会话的 `SlashController` 基于 textarea 当前 selection只打开 `/` trigger 的 `command` source同时 ui-slash 既有的 `MenuView` 仍是唯一的浮层菜单与 pick 路径。不引入 File 行、file input、上传协议或第二套菜单组件。当 `plan` 投影的有效目标为 plan mode 时InputBar 将文本框 placeholder 切换为 plan 任务措辞,经本包注册的 `conversation` locale 命名空间(`placeholder.plan` / `hint.plan` 键)本地化,并与已认领 `/plan` 命令的提示逐字共用同一份文案(经标准套件 `useProjection` 读取的 host 折叠值owner 提供的 placeholder 优先)。另一个会话视图活跃时,待处理的 composer 接管仍保持挂载,使被阻塞的 agent智能体仍能收到回答没有待处理交互时活跃会话的 composer 归 Chat 所有。composer bar slot 本身为 `session-maybe`:没有当前会话时,同一个 bar 会让消息操作保持不可交互machine face 均缺席、`disabled` owner prop整张虚线卡片可经指针打开现有 Workspace picker只读 textarea 也可通过 Enter 或 Space 打开。禁用控件会把指针事件交给卡片,卡片也会拦下 `pointerdown`,避免已打开 picker 的外点关闭与重新打开发生竞态。它不会换入一棵平行树,因此选择 Workspace 时 textarea DOM 不会被销毁;严格会话作用域的控件 seat 在会话存在之前保持为空。
输入栏为 `'conversation.input.plan'`(位于本地 access 模式控件右侧)和 `'conversation.input.model'`(渲染在 pending 指示器与发送/停止控件之前)声明会话作用域的单实例 seat并为 overlay、dock、left 和 right 输入扩展声明列表 slot。各功能包拥有相应控件及其状态ui-conversation 提供放置位置、`locked` owner prop 和标准 slot share。前置加号按钮是 Command launcher而非附件入口它要求当前会话的 `InputTriggerController` 基于 textarea 当前 selection只打开 `/` trigger 的 `command` source同时 ui-input-trigger 既有的 `MenuView` 仍是唯一的浮层菜单与 pick 路径。不引入 File 行、file input、上传协议或第二套菜单组件。当 `plan` 投影的有效目标为 plan mode 时InputBar 将文本框 placeholder 切换为 plan 任务措辞,经本包注册的 `conversation` locale 命名空间(`placeholder.plan` / `hint.plan` 键)本地化,并与已认领 `/plan` 命令的提示逐字共用同一份文案(经标准套件 `useProjection` 读取的 host 折叠值owner 提供的 placeholder 优先)。另一个会话视图活跃时,待处理的 composer 接管仍保持挂载,使被阻塞的 agent智能体仍能收到回答没有待处理交互时活跃会话的 composer 归 Chat 所有。composer bar slot 本身为 `session-maybe`:没有当前会话时,同一个 bar 会让消息操作保持不可交互machine face 均缺席、`disabled` owner prop整张虚线卡片可经指针打开现有 Workspace picker只读 textarea 也可通过 Enter 或 Space 打开。禁用控件会把指针事件交给卡片,卡片也会拦下 `pointerdown`,避免已打开 picker 的外点关闭与重新打开发生竞态。它不会换入一棵平行树,因此选择 Workspace 时 textarea DOM 不会被销毁;严格会话作用域的控件 seat 在会话存在之前保持为空。
聊天统计行的 token 账目来自经标准套件 `useProjection` 读取的通用 token-meter 投影 `tokenUsage`计费输入为未缓存输入、缓存读取与缓存写入之和缓存命中率以缓存读取除以该总量。轮次与步骤计数、LLM大语言模型与工具墙钟时间、以及延迟吞吐分组都来自全日志的 `sessionStats` 投影Host 端从步边界、首 token chunk、工具配对与已组装消息折算因此分页与压缩都无法改变统计条的任何数字未组合该单元的装配回退为对可见节点做窗口折算其字段与投影一一对应。统计条把每个有完整记录的步骤的 TTFT首 token 延迟)取平均,并用采样到的输出 token 数除以其解码时长之和,得到经 `conversation` locale 命名空间本地化的延迟/吞吐分组(中文为 `首 token 平均 … · … tok/s`);缺少某个 timing 边界或 usage 采样的步骤会直接退出这些数字而不是让它们失真压缩compaction使已加载窗口不再包含 assistant 节点时持久计数、token 与上下文分组仍保持可见。轮次计数、步骤计数、耗时、缓存与 token 各项的标签也使用同一命名空间。每个已结算轮次还会在其 assistant footer 的 `用时` 之后追加 hover 才显示的 `首 token {s}秒 · {tps} tok/s` 标签——即该轮次首个步骤的 TTFT 与轮次聚合的解码吞吐——仅当该轮次的 timing 位于已加载窗口内才显示(窗口是日志的连续后缀,因此窗口内的轮次必然带着它的全部步骤),未记录的数字会各自省略。未组合 token-meter 的部署会整组省略 token 分组;统计行过长时以省略号截断,仅在内容真的被裁切时由延迟 hover tooltip 承载完整文本。上下文占用率渲染为 composer 尾部的 ContextMeter模型座位之后的一枚 14px 占用圆环,由 `contextPressure` 供数,仅当分子与路由容量都已知时才渲染;点击弹出的面板把「已用百分比」标题与 `~已用 / 容量` 数字,与来自 `contextBreakdown` 投影、带 `~` 前缀的启发式组成明细行(系统提示词、工具、对话消息)及分色分段进度条并列。圆环与标题读取 `projectedTokens`——把提供方样本沿此后表层的增减推进到当下——因此压缩会立刻反映出来,而不必再等一整轮;组成明细行仍是纯启发式,因此加起来依然不等于标题数字([原理](../../llm/token-meter/README.md))。占用率是刻意为之的近似值:分子与容量是两个相互独立的「后写覆盖」投影字段,并非同一次请求的原子观测。

View File

@@ -64,10 +64,10 @@
"@deepseek-ai/dsh-client-ui-attachment": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-compact": "workspace:^",
"@deepseek-ai/dsh-compaction": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm-retry": "workspace:^",
"@deepseek-ai/dsh-session-stats": "workspace:^",
@@ -89,14 +89,14 @@
"@deepseek-ai/dsh-client-ui-attachment": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-compact": "workspace:^",
"@deepseek-ai/dsh-compaction": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm-retry": "workspace:^",
"@deepseek-ai/dsh-permission": "workspace:^",
"@deepseek-ai/dsh-permission-presets": "workspace:^",
"@deepseek-ai/dsh-plan-mode": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-stats": "workspace:^",

View File

@@ -18,7 +18,7 @@ import type {
} from './contract/slots.ts'
import type { InputNotice } from './input/contract.ts'
import { createChatStore } from './stores.ts'
import { ConversationService, UnsupportedImageMediaTypeError } from './service.ts'
import { ConversationController, UnsupportedImageMediaTypeError } from './service.ts'
import type { IConversation } from './service.ts'
import { ComposerBlockRegistry } from './input/blocks.ts'
import type { ComposerBlock } from './input/blocks.ts'
@@ -98,8 +98,8 @@ function scopedConversation(sessions: ISessions, id: SessionId): IConversation {
}
/** Resolve package-internal attachment operations from the public service registration. */
function concreteConversation(ctx: Context): ConversationService {
const conversation = ctx.get('conversation') as ConversationService | undefined
function concreteConversation(ctx: Context): ConversationController {
const conversation = ctx.get('conversation') as ConversationController | undefined
if (conversation === undefined) throw new Error('ui-conversation: conversation service unavailable')
return conversation
}
@@ -165,12 +165,12 @@ export function apply(ctx: Context): void {
version: () => slots.getVersion('conversation.view'),
}
// The per-session input machine registry (InputService face; published as
// The per-session input machine registry (SessionInputResolver face; published as
// ctx.conversation.input by the service below sharing this one instance).
const inputHub = new InputHub(ctx, t)
// The composer-block registry: a plugin that knows a session cannot send —
// ui-model, when no adapter serves the session's route — raises a block
// ui-model-selection, when no adapter serves the session's route — raises a block
// here, and the bar reads its own session's store. It cannot flow the other
// way: this package must not import the plugins that would know.
const composerBlocks = new ComposerBlockRegistry()
@@ -302,7 +302,7 @@ export function apply(ctx: Context): void {
}
const conversation = concreteConversation(ctx)
const shell = inputHub.shell(sessionId)
const slash = inputHub.slash(sessionId)
const inputTriggers = inputHub.inputTriggers(sessionId)
return {
keyboard: shell,
addImages: (files) => {
@@ -328,12 +328,12 @@ export function apply(ctx: Context): void {
draftImages: ids => conversation.draftImages(ids),
resolveSubmitMode: (running, gesture, steeringAvailable) =>
submissionPolicy.resolve(running, gesture, steeringAvailable),
toggleCommandMenu: slash === undefined
toggleCommandMenu: inputTriggers === undefined
? undefined
: (selection) => {
shell.dismissPopup()
const snapshot = shell.snapshot
slash.toggleSource('command', {
inputTriggers.toggleSource('command', {
trigger: '/',
query: '',
position: snapshot.draft.slice(0, selection.start).trim() === '' ? 'leading' : 'inline',
@@ -354,14 +354,14 @@ export function apply(ctx: Context): void {
hooks: {
notices: shell.notices,
lexicon: shell.lexicon,
menuLauncher: slash?.launcher ?? ABSENT_MENU_LAUNCHER,
menuLauncher: inputTriggers?.launcher ?? ABSENT_MENU_LAUNCHER,
},
}
},
}, InputBar)
// The approval takeover: a selector-routed entry of the chain this package
// just declared (the ui-question registration pattern; the entry lives here
// just declared (the ui-user-questions registration pattern; the entry lives here
// because approval answering is core conversation UX, not an optional tool).
// Zero business face — data and verbs both ride the matched carrier.
// priority 1: question takeovers (default 0) win when both kinds are
@@ -432,7 +432,7 @@ export function apply(ctx: Context): void {
// registers itself as `conversation` and lives on its own child fiber.
// Presentation registrants depend directly on their slot declarations;
// this service remains only where conversation actions are required.
ctx.plugin(ConversationService, { input: inputHub, blocks: composerBlocks })
ctx.plugin(ConversationController, { input: inputHub, blocks: composerBlocks })
// The plan strip rides the input dock above the queue rows (same posture).
ctx.plugin(todoDockEntry)

View File

@@ -395,7 +395,7 @@ export function ChatView({
t={t}
/>
))}
{/* No pending placeholders: questions (ui-question) and approvals
{/* No pending placeholders: questions (ui-user-questions) and approvals
(ApprovalPanel) both take over the composer, so a flow card would
double-render the same wait. */}
{/* Turn-level loading signal: rides the whole running turn (first-token

View File

@@ -3,7 +3,7 @@
// marker reports where the model stopped seeing that history — it never
// replaces it. The framed checkpoint payload is written for the model and is
// not rendered; the disclosure shows the summary from the checkpoint's own
// cited `compact/summary` event, and a window cut that left that event outside makes the row
// cited `compaction/summary` event, and a window cut that left that event outside makes the row
// non-expandable rather than empty.
import { memo, useState } from 'react'

View File

@@ -118,10 +118,10 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
* the next one rather than applied to a current one.
*/
'conversation.hero.agentPreset': { kind: 'single'; scope: 'root'; owner: HeroAgentPresetOwnerProps }
// 'conversation.input.overlay' merges in ui-slash (the dependency
// direction is the hard constraint — ui-slash cannot import
// 'conversation.input.overlay' merges in ui-input-trigger (the dependency
// direction is the hard constraint — ui-input-trigger cannot import
// this package, while this package's input contract already imports
// ui-slash, so the type arrives transitively). The runtime declaration
// ui-input-trigger, so the type arrives transitively). The runtime declaration
// (children table in apply.ts) stays here with the other input slots.
/**
* Stacked strip above the input (queue rows / GoalBar / attachments;
@@ -446,7 +446,7 @@ export interface ComposerBarInjected {
gesture: ComposerSubmitGesture,
steeringAvailable: boolean,
) => InputSubmitMode
/** Toggle the shared slash menu with only its command source; absent without ui-slash or a session. */
/** Toggle the shared slash menu with only its command source; absent without ui-input-trigger or a session. */
toggleCommandMenu: ((selection: EditSelection) => void) | undefined
/** Cancel the in-flight turn; absent with the session. */
stop: (() => void) | undefined
@@ -539,7 +539,7 @@ export type ConversationSessionHeaderSlotProps =
export type ApprovalWait = PendingWait<'approval'>
/**
* Approval domain face over the carrier (the ui-question PendingQuestion
* Approval domain face over the carrier (the ui-user-questions PendingQuestion
* pattern): render identity and question material forwarded transparently;
* answer owns the wire encoding — the ApprovalResponsePayload value shape
* with the audit correlation the host reconciles — and turns a rejected

View File

@@ -4,8 +4,8 @@ import type {
ConversationNodeDefinition,
} from '@deepseek-ai/dsh-client-runtime/client'
import { isReplacementSurfaceEvent } from '@deepseek-ai/dsh-client-runtime/client'
import type { CompactCheckpointSource } from '@deepseek-ai/dsh-compact/checkpoint'
import type {} from '@deepseek-ai/dsh-compact/types'
import type { CompactionCheckpointSource } from '@deepseek-ai/dsh-compaction/checkpoint'
import type {} from '@deepseek-ai/dsh-compaction/types'
import type {} from '@deepseek-ai/dsh-commands/types'
import type { ManualCompactionChatData } from '../contract/chat-nodes.ts'
import { chatNode } from './common.ts'
@@ -21,7 +21,7 @@ declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
type CommandId = CommandNode['commandId']
const COMPACT_PLUGIN: CompactCheckpointSource['plugin'] = 'compact'
const COMPACT_PLUGIN: CompactionCheckpointSource['plugin'] = 'compact'
interface CommandState {
readonly command: CommandNode
@@ -96,7 +96,7 @@ function compactSource(event: Parameters<ConversationNodeDefinition['match']>[0]
/**
* Build the visible summary marker from optional lifecycle evidence.
* @param match - compact/summary Match, when loaded.
* @param match - compaction/summary Match, when loaded.
* @param checkpoint - replacement checkpoint Match.
* @returns final compaction summary Node data.
*/
@@ -104,7 +104,7 @@ function compactSummary(match: ConversationMatch | undefined, checkpoint: Conver
let summary: string | null = null
let shadowedItemCount: number | null = null
let shadowedTokenCount: number | null = null
if (match?.event.type === 'compact/summary') {
if (match?.event.type === 'compaction/summary') {
const data = match.event.data
if (Array.isArray(data.summary)) {
const text = data.summary
@@ -135,7 +135,7 @@ function compactSummary(match: ConversationMatch | undefined, checkpoint: Conver
function fallbackState(context: ConversationNodeContext<CommandState>): CommandState | undefined {
const done = context.matches.find(match => match.event.type === 'command/done')
const checkpoint = context.matches.find(match => compactSource(match.event) !== undefined)
const summary = context.matches.find(match => match.event.type === 'compact/summary')
const summary = context.matches.find(match => match.event.type === 'compaction/summary')
if (checkpoint === undefined) return done === undefined ? undefined : { command: commandFromDone(done) }
const source = compactSource(checkpoint.event)
if (source?.sourceCommandId === undefined) return done === undefined ? undefined : { command: commandFromDone(done) }
@@ -167,7 +167,7 @@ export function updateCompactionState<State extends CompactionEvidence>(
state: State,
match: ConversationMatch,
): State {
if (match.event.type === 'compact/summary') return { ...state, summary: match }
if (match.event.type === 'compaction/summary') return { ...state, summary: match }
if (compactSource(match.event) !== undefined) return { ...state, checkpoint: match }
return state
}
@@ -187,9 +187,9 @@ export const commandDefinition: ConversationNodeDefinition<CommandState> = {
if (checkpoint?.sourceCommandId !== undefined) {
return { id: String(checkpoint.sourceCommandId), role: 'update' }
}
if (event.type === 'compact/start'
|| event.type === 'compact/summary'
|| event.type === 'compact/end') {
if (event.type === 'compaction/start'
|| event.type === 'compaction/summary'
|| event.type === 'compaction/end') {
if (event.data.sourceCommandId !== undefined) {
return { id: String(event.data.sourceCommandId), role: 'update' }
}

View File

@@ -2,7 +2,7 @@ import type { Context } from '@deepseek-ai/cordis'
import type {
CompactionSummaryNode, ConversationMatch, ConversationNodeContext, ConversationNodeDefinition,
} from '@deepseek-ai/dsh-client-runtime/client'
import type {} from '@deepseek-ai/dsh-compact/types'
import type {} from '@deepseek-ai/dsh-compaction/types'
import { chatNode } from './common.ts'
import { compactSource, compactSummary, updateCompactionState } from './command.ts'
@@ -19,7 +19,7 @@ interface CompactionState {
}
function fallbackState(context: ConversationNodeContext<CompactionState>): CompactionState {
const summary = context.matches.find(match => match.event.type === 'compact/summary')
const summary = context.matches.find(match => match.event.type === 'compaction/summary')
const checkpoint = context.matches.find(match => compactSource(match.event) !== undefined)
return {
...summary === undefined ? {} : { summary },
@@ -36,13 +36,13 @@ export const compactionDefinition: ConversationNodeDefinition<CompactionState> =
if (checkpoint !== undefined && checkpoint.sourceCommandId === undefined) {
return { id: checkpoint.compactionId, role: 'update' }
}
if (event.type === 'compact/start'
|| event.type === 'compact/summary'
|| event.type === 'compact/end') {
if (event.type === 'compaction/start'
|| event.type === 'compaction/summary'
|| event.type === 'compaction/end') {
if (event.data.sourceCommandId !== undefined) return null
const compactionId: unknown = event.data.compactionId
if (typeof compactionId !== 'string' || compactionId === '') return null
return { id: compactionId, role: event.type === 'compact/start' ? 'start' : 'update' }
return { id: compactionId, role: event.type === 'compaction/start' ? 'start' : 'update' }
}
return null
},

View File

@@ -15,7 +15,7 @@ export type {} from './conversation-nodes/turn-max-tokens.ts'
export type {} from './conversation-nodes/turn-tail.ts'
export { apply, inject } from './apply.ts'
export { ConversationService } from './service.ts'
export { ConversationController } from './service.ts'
export type { IConversation } from './service.ts'
export type { DraftAttachmentId } from './input/contract.ts'

View File

@@ -2,7 +2,7 @@
* Composer blocks: the one way another plugin stops a session's input.
*
* The composer cannot read the plugins that would know — the dependency runs
* ui-model → ui-conversation, never back — so a blocker pushes here and the
* ui-model-selection → ui-conversation, never back — so a blocker pushes here and the
* bar reads its own session's store. A block carries the localized reason it
* exists, because the plugin that raised it owns that copy; the composer only
* knows how to render an inert textarea with a placeholder, exactly as it

View File

@@ -10,7 +10,7 @@ import type { Branded } from '@deepseek-ai/dsh-brand'
import type {
ArbitrateKey, ArbitrateOutcome, CommandClaim, ConsumeTokenRequest, PickOutcome,
ReferenceInsert, SubmitOutcome, TokenSpan,
} from '@deepseek-ai/dsh-client-ui-slash/client'
} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { QueueRow } from '../contract/queue.ts'
import type { InputSubmitMode } from '../contract/composer-submission.ts'
@@ -47,7 +47,7 @@ export interface SessionInput extends InputTarget {
/**
* Surface a notice outside the machine's own effect stream: detached
* command results and business notifications render through here.
* Session-routed — resolving the facade via InputService.for(actx) lands
* Session-routed — resolving the facade via SessionInputResolver.for(actx) lands
* the notice on that session's composer, so a result arriving after a
* session switch still reaches its own session.
* @param level - severity tier.
@@ -59,7 +59,7 @@ export interface SessionInput extends InputTarget {
}
/** Session-addressed access to the per-session input facade. */
export interface InputService {
export interface SessionInputResolver {
/** Resolve the facade for one session-scope ctx. */
for(actx: ClientContext): SessionInput
}
@@ -208,7 +208,7 @@ export interface InputMachineOptions {
/** Published input state (the currency; per-session). */
export interface InputState {
readonly draft: string
/** Ordered runtime-only image ids; bytes and URLs stay in ConversationService. */
/** Ordered runtime-only image ids; bytes and URLs stay in ConversationController. */
readonly imageIds: readonly DraftAttachmentId[]
/** Monotonic draft revision (span CAS compares against this). */
readonly draftRev: number

View File

@@ -2,7 +2,7 @@
* SessionInput shell over the pure input machine: the sole machine caller
* and effect executor. Owns the InputState store (machine state + the queue
* overlay), the notice channel, and the submit transaction plumbing
* (adjudicate via the session's SlashController; claim.submit; default
* (adjudicate via the session's InputTriggerController; claim.submit; default
* sink). Package-private; the hub alone constructs it and wires the scoped
* event listeners onto it.
*/
@@ -10,8 +10,8 @@ import type { ClientContext, ObservableSnapshot, SnapshotStore } from '@deepseek
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type {
ArbitrateKey, ArbitrateOutcome, CommandClaim, ConsumeTokenRequest, PickOutcome,
ReferenceInsert, SlashController, TokenSpan,
} from '@deepseek-ai/dsh-client-ui-slash/client'
ReferenceInsert, InputTriggerController, TokenSpan,
} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type {
DraftAttachmentId, EditRange, EditSelection, InputActions, InputEffect, InputNotice, InputState,
PasteComponent, QueuedMessage, SessionInput, SubmitAttempt,
@@ -34,7 +34,7 @@ export interface SessionInputDeps {
/** Session-scope ctx handed to claim.submit transactions. */
actx: ClientContext
/** Enter adjudication face resolver; absent/undefined answer = every '/' line falls to the default sink. */
slash?: (() => SlashController | undefined) | undefined
inputTriggers?: (() => InputTriggerController | undefined) | undefined
/** PopupSelect shell face resolver (dismissal on submit lock / escape). */
popup?: (() => PopupDismissFace | undefined) | undefined
/** Queue read face; overlaid onto InputState.queue (absent = empty). */
@@ -204,7 +204,7 @@ export class SessionInputShell implements SessionInput {
const phase = this.snapshot.phase
if (phase === 'adjudicating' || phase === 'submitting') {
this.deps.popup?.()?.dismiss()
this.deps.slash?.()?.track(this.snapshot.draft, 0, { tier: 'frozen' }, this.snapshot.draftRev)
this.deps.inputTriggers?.()?.track(this.snapshot.draft, 0, { tier: 'frozen' }, this.snapshot.draftRev)
}
}
@@ -215,7 +215,7 @@ export class SessionInputShell implements SessionInput {
* @param caret - caret position in draft coordinates.
*/
track(draft: string, caret: number): void {
this.deps.slash?.()?.track(draft, caret, { tier: guardOf(this.snapshot.phase) }, this.snapshot.draftRev)
this.deps.inputTriggers?.()?.track(draft, caret, { tier: guardOf(this.snapshot.phase) }, this.snapshot.draftRev)
}
/**
@@ -225,7 +225,7 @@ export class SessionInputShell implements SessionInput {
* @returns the menu's verdict; 'pass' when no pipeline is mounted.
*/
arbitrate(key: ArbitrateKey, composing: boolean): ArbitrateOutcome {
return this.deps.slash?.()?.arbitrate(key, composing) ?? 'pass'
return this.deps.inputTriggers?.()?.arbitrate(key, composing) ?? 'pass'
}
/**
@@ -243,15 +243,15 @@ export class SessionInputShell implements SessionInput {
* @returns true = a claim/insert was applied — the caller preventDefaults.
*/
space(): boolean {
const slash = this.deps.slash?.()
if (slash === undefined) return false
const consumed = slash.onSpace()
const inputTriggers = this.deps.inputTriggers?.()
if (inputTriggers === undefined) return false
const consumed = inputTriggers.onSpace()
// Machine-driven draft replacement never passes through onChange, so
// re-track: the caret lands after the token, where detection sees
// whitespace and closes the menu.
if (consumed) {
const next = this.snapshot
slash.track(next.draft, next.draft.length, { tier: guardOf(next.phase) }, next.draftRev)
inputTriggers.track(next.draft, next.draft.length, { tier: guardOf(next.phase) }, next.draftRev)
}
return consumed
}
@@ -270,8 +270,8 @@ export class SessionInputShell implements SessionInput {
* subscribers never fire.
*/
readonly lexicon: ObservableSnapshot<ReadonlyMap<'/' | '@', readonly string[]>> = {
getSnapshot: () => this.deps.slash?.()?.lexicon.getSnapshot() ?? EMPTY_LEXICON,
subscribe: fn => this.deps.slash?.()?.lexicon.subscribe(fn) ?? (() => {}),
getSnapshot: () => this.deps.inputTriggers?.()?.lexicon.getSnapshot() ?? EMPTY_LEXICON,
subscribe: fn => this.deps.inputTriggers?.()?.lexicon.subscribe(fn) ?? (() => {}),
}
/**
@@ -420,11 +420,11 @@ export class SessionInputShell implements SessionInput {
this.deps.defaultSink(draft.trim(), imageIds, mode)
return
}
const slash = this.deps.slash?.()
const inputTriggers = this.deps.inputTriggers?.()
const controller = new AbortController()
void Promise.all(occurrences.map(async (o) => {
if (slash === undefined) throw new Error(`no serializer for reference source "${o.source}"`)
return { offset: o.offset, text: await slash.serializeReference(o.source, o.ref, controller.signal) }
if (inputTriggers === undefined) throw new Error(`no serializer for reference source "${o.source}"`)
return { offset: o.offset, text: await inputTriggers.serializeReference(o.source, o.ref, controller.signal) }
})).then(
(parts) => {
if (this.disposed) return
@@ -450,13 +450,13 @@ export class SessionInputShell implements SessionInput {
/** Enter adjudication: poll the session controller; failure = notice + draft retained (never a silent downgrade). */
private adjudicate(attempt: SubmitAttempt, draft: string): void {
const slash = this.deps.slash?.()
if (slash === undefined) {
const inputTriggers = this.deps.inputTriggers?.()
if (inputTriggers === undefined) {
// No pipeline mounted: the '/' line is an ordinary message.
this.run(this.core.dispatch({ type: 'adjudicated', attempt, outcome: undefined }))
return
}
slash.adjudicate(draft.trim(), attempt.signal).then(
inputTriggers.adjudicate(draft.trim(), attempt.signal).then(
(outcome: PickOutcome) => {
if (this.dead(attempt)) return
this.run(this.core.dispatch({ type: 'adjudicated', attempt, outcome }))

View File

@@ -1,18 +1,18 @@
/**
* InputHub: the InputService implementation (`ctx.conversation.input`) — one
* InputHub: the SessionInputResolver implementation (`ctx.conversation.input`) — one
* SessionInputShell per session, created inside the sessions provide
* materialization (the 'input' standard-kit entry IS the
* creation trigger) and torn down by the scope disposer (instance-and-scope
* share one lifecycle). The hub registers the three scoped input-mutation
* listeners on each session's actx (the sole consumer side of the ui-slash
* listeners on each session's actx (the sole consumer side of the ui-input-trigger
* bail events) and owns the default-sink choreography: every session is a
* real host entity, so the sink is one unconditional prompt path.
*/
import type { ClientContext, ISessions, SessionBinding, SessionFace, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { SlashController } from '@deepseek-ai/dsh-client-ui-slash/client'
import type { InputTriggerController } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client'
import { queueReadFaceOf } from '../queue/store.ts'
import type { ComposerKeyboard, DraftAttachmentId, InputService, SessionInput } from './contract.ts'
import type { ComposerKeyboard, DraftAttachmentId, SessionInputResolver, SessionInput } from './contract.ts'
import type { InputSubmitMode } from '../contract/composer-submission.ts'
import type { PopupDismissFace } from './facade.ts'
import { SessionInputShell } from './facade.ts'
@@ -33,8 +33,8 @@ interface ConversationAttachmentFace {
releaseDraftImage(id: DraftAttachmentId): void
}
/** Session-addressed input facade registry (InputService face + composer-layer extras). */
export class InputHub implements InputService {
/** Session-addressed input facade registry (SessionInputResolver face + composer-layer extras). */
export class InputHub implements SessionInputResolver {
private readonly shells = new Map<SessionId, SessionInputShell>()
/**
@@ -47,7 +47,7 @@ export class InputHub implements InputService {
) {}
/**
* Resolve the facade for one session-scope ctx (InputService face).
* Resolve the facade for one session-scope ctx (SessionInputResolver face).
* @param actx - session-scope context.
* @returns the resident per-session facade.
*/
@@ -72,7 +72,7 @@ export class InputHub implements InputService {
const { sessionId: id, session, ctx: actx } = binding
const shell = new SessionInputShell({
actx,
slash: () => this.controller(actx),
inputTriggers: () => this.controller(actx),
popup: () => this.popup(actx),
queue: queueReadFaceOf(session),
defaultSink: (text, imageIds, mode) => { this.sink(session, text, imageIds, mode) },
@@ -133,9 +133,9 @@ export class InputHub implements InputService {
* Resolve the optional slash controller for composer chrome that launches
* the shared candidate menu without typing a trigger.
* @param id - session id.
* @returns the resident controller, or undefined when ui-slash is absent.
* @returns the resident controller, or undefined when ui-input-trigger is absent.
*/
slash(id: SessionId): SlashController | undefined {
inputTriggers(id: SessionId): InputTriggerController | undefined {
const actx = this.sessions().scope(id)
return actx === undefined ? undefined : this.controller(actx)
}
@@ -191,13 +191,13 @@ export class InputHub implements InputService {
}
}
private controller(actx: ClientContext): SlashController | undefined {
const slash = this.rootCtx.get('slash')
return slash?.sessionOf(actx)
private controller(actx: ClientContext): InputTriggerController | undefined {
const inputTriggers = this.rootCtx.get('inputTriggers')
return inputTriggers?.sessionOf(actx)
}
private popup(actx: ClientContext): PopupDismissFace | undefined {
const command = this.rootCtx.get('command') as CommandFace | undefined
const command = this.rootCtx.get('commandUi') as CommandFace | undefined
return command?.popupFor(actx)
}

View File

@@ -13,7 +13,7 @@
* as a draftRev advance (begin-command / insert-ref / consume-token /
* paste-upgrade all answer their bail events this way).
*/
import type { CommandClaim, ReferenceInsert, TokenSpan } from '@deepseek-ai/dsh-client-ui-slash/client'
import type { CommandClaim, ReferenceInsert, TokenSpan } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { InputSubmitMode } from '../contract/composer-submission.ts'
import type {
ConsumeTokenGuard, EditRange, EditSelection, InputEffect, InputEvent, InputMachineOptions,

View File

@@ -17,7 +17,7 @@ import type { ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attach
import type { ComposerAttachment } from './contract/slots.ts'
import type { QueueAction, QueueItemId } from './contract/queue.ts'
import type { ComposerBlocks } from './input/blocks.ts'
import type { DraftAttachmentId, InputService } from './input/contract.ts'
import type { DraftAttachmentId, SessionInputResolver } from './input/contract.ts'
import type { InputSubmitMode } from './contract/composer-submission.ts'
/**
@@ -26,8 +26,8 @@ import type { InputSubmitMode } from './contract/composer-submission.ts'
* test fake must supply.
*/
export interface IConversation {
/** The per-session input machine registry (InputService face). */
readonly input: InputService
/** The per-session input machine registry (SessionInputResolver face). */
readonly input: SessionInputResolver
/**
* The per-session composer-block registry: how a plugin the composer
* cannot import makes a session's input inert with its own reason.
@@ -88,9 +88,9 @@ export class UnsupportedImageMediaTypeError extends Error {
}
/** Scope-addressed conversation service (root singleton, provided as `conversation`). */
export class ConversationService extends Service implements IConversation {
/** The per-session input machine registry (InputService face). */
readonly input: InputService
export class ConversationController extends Service implements IConversation {
/** The per-session input machine registry (SessionInputResolver face). */
readonly input: SessionInputResolver
/** The per-session composer-block registry. */
readonly blocks: ComposerBlocks
private readonly draftAttachments = new Map<DraftAttachmentId, ComposerAttachment>()
@@ -102,11 +102,11 @@ export class ConversationService extends Service implements IConversation {
/**
* @param ctx - owning root context (the plugin apply context; the service
* registers itself and follows that fiber's lifetime).
* @param config - carries the InputService and composer-block registry
* @param config - carries the SessionInputResolver and composer-block registry
* constructed by the plugin apply (the same instances the slot inject
* factories close over).
*/
constructor(ctx: Context, config: { input: InputService; blocks: ComposerBlocks }) {
constructor(ctx: Context, config: { input: SessionInputResolver; blocks: ComposerBlocks }) {
super(ctx, 'conversation')
this.input = config.input
this.blocks = config.blocks

View File

@@ -24,7 +24,7 @@ export function ConversationRoot({
const cwd = useSessions(s => sessionId === undefined ? undefined : s.byId[sessionId]?.cwd)
const summaryBlank = useSessions(s => sessionId === undefined ? undefined : s.byId[sessionId]?.blank)
const workspaces = useWorkspaces(s => s)
// A plugin this package cannot import (ui-model) says this session cannot
// A plugin this package cannot import (ui-model-selection) says this session cannot
// send; its reason is already localized by whoever raised it.
const composerBlock = useComposerBlock(block => block)

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import clsx from 'clsx'
import type { PermissionSelect as PermissionSelectValue } from '@deepseek-ai/dsh-permission/client'
import type { PermissionSelect as PermissionSelectValue } from '@deepseek-ai/dsh-permission-presets/client'
import { IconChevronDownOutline14, Menu, RiskConfirmation } from '@deepseek-ai/dsh-client-ui-primitives'
import type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ComposerBarProps } from '../contract/slots.ts'

View File

@@ -17,7 +17,7 @@
import { describe, expect, it, vi } from 'vitest'
import { SlotTestRuntime, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
import type { SessionBehaviorOverrides } from '@deepseek-ai/dsh-client-test-runtime'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import type { ISession, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {
@@ -59,7 +59,7 @@ async function bench() {
})
const layoutFake = { openDetails: vi.fn(), closeDetails: vi.fn() }
runtime.provide('layout', layoutFake)
const locale = new LocaleService(runtime.ctx)
const locale = new LocaleRuntime(runtime.ctx)
runtime.provide('locale', locale)
runtime.slots.installLocale(locale)

View File

@@ -3,7 +3,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, waitFor, within } from '@testing-library/react'
import { useState } from 'react'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import type { ISession, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
import { SlotTestRuntime, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
@@ -55,7 +55,7 @@ async function bench(opts?: { blank?: boolean }) {
runtime.provide('remote', { $on: () => () => {} })
runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never)
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
const locale = new LocaleService(runtime.ctx)
const locale = new LocaleRuntime(runtime.ctx)
runtime.provide('locale', locale)
runtime.slots.installLocale(locale)
await runtime.sessions.add({
@@ -83,7 +83,7 @@ describe('resident composer', () => {
runtime.provide('remote', { $on: () => () => {} })
runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never)
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
const locale = new LocaleService(runtime.ctx)
const locale = new LocaleRuntime(runtime.ctx)
runtime.provide('locale', locale)
runtime.slots.installLocale(locale)
await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
@@ -113,7 +113,7 @@ describe('resident composer', () => {
runtime.provide('remote', { $on: () => () => {} })
runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never)
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
const locale = new LocaleService(runtime.ctx)
const locale = new LocaleRuntime(runtime.ctx)
runtime.provide('locale', locale)
runtime.slots.installLocale(locale)
await runtime.workspaces.update((draft) => {
@@ -182,7 +182,7 @@ describe('prompt rejection through the assembled composer', () => {
runtime.provide('remote', { $on: () => () => {} })
runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never)
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
const locale = new LocaleService(runtime.ctx)
const locale = new LocaleRuntime(runtime.ctx)
runtime.provide('locale', locale)
runtime.slots.installLocale(locale)
const prompt = vi.fn<ISession['prompt']>(async () => ({

View File

@@ -8,7 +8,7 @@
import { describe, expect, it, vi } from 'vitest'
import { SlotTestRuntime, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client'
@@ -29,7 +29,7 @@ async function bench() {
await runtime.sessions.add(
{ id: CHILD, summary: { title: 'C', displayTitle: 'C', parentId: ROOT } }, { current: false })
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
const locale = new LocaleService(runtime.ctx)
const locale = new LocaleRuntime(runtime.ctx)
runtime.provide('locale', locale)
runtime.slots.installLocale(locale)

View File

@@ -288,7 +288,7 @@ describe('MessageItem arms', () => {
seq: 3,
content: [{ type: 'text', text: '<system-reminder>\nInstructions from: AGENTS.md\n</system-reminder>' }],
source: {
kind: 'workspace-instructions',
kind: 'agent-instructions',
form: 'instructions',
baseline: true,
changes: [
@@ -318,7 +318,7 @@ describe('MessageItem arms', () => {
seq: 3,
content: [{ type: 'text', text: 'delta' }],
source: {
kind: 'workspace-instructions',
kind: 'agent-instructions',
form: 'instructions',
changes: [
{ action: 'set', scope: 'a', path: 'new/AGENTS.md' },
@@ -433,13 +433,13 @@ describe('MessageItem arms', () => {
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'instruction prose' }],
source: { kind: 'workspace-instructions', form: 'instructions', changes: [{ action: 'set' }] },
provenance: { role: 'inject', label: 'workspace-instructions' },
source: { kind: 'agent-instructions', form: 'instructions', changes: [{ action: 'set' }] },
provenance: { role: 'inject', label: 'agent-instructions' },
form: 'instructions',
} as never}
/>,
)
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*workspace-instructions$/ }))
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*agent-instructions$/ }))
expect(view.container.querySelector('[data-context-files]')).toBeNull()
expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('instruction prose')
expect(view.container.querySelector('[data-context-fields]')).not.toBeNull()
@@ -558,13 +558,13 @@ describe('MessageItem arms', () => {
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'instruction prose' }],
source: { kind: 'workspace-instructions', form: 'instructions', changes: [{ action: 'merge', path: 'A.md' }] },
provenance: { role: 'inject', label: 'workspace-instructions' },
source: { kind: 'agent-instructions', form: 'instructions', changes: [{ action: 'merge', path: 'A.md' }] },
provenance: { role: 'inject', label: 'agent-instructions' },
form: 'instructions',
} as never}
/>,
)
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*workspace-instructions$/ }))
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*agent-instructions$/ }))
expect(view.container.querySelector('[data-context-files]')).toBeNull()
expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('instruction prose')
})
@@ -613,9 +613,9 @@ describe('MessageItem arms', () => {
<MessageItem t={t} node={{
kind: 'context',
seq: 3,
content: [{ type: 'text', text: 'background task bash-1 finished.' }],
source: { kind: 'plugin', plugin: 'tool-tasks', form: 'notice', summary: 'bash pnpm test [status: completed]' },
provenance: { role: 'inject', label: 'tool-tasks' },
content: [{ type: 'text', text: 'background job bash-1 finished.' }],
source: { kind: 'plugin', plugin: 'tool-jobs', form: 'notice', summary: 'bash pnpm test [status: completed]' },
provenance: { role: 'inject', label: 'tool-jobs' },
form: 'notice',
} as never}
/>,
@@ -629,14 +629,14 @@ describe('MessageItem arms', () => {
const view = render(
<MessageItem t={t} node={{
kind: 'context', seq: 3, content: [{ type: 'text', text: 'notice prose' }],
source: { kind: 'plugin', plugin: 'tool-tasks', form: 'notice' },
provenance: { role: 'inject', label: 'tool-tasks' },
source: { kind: 'plugin', plugin: 'tool-jobs', form: 'notice' },
provenance: { role: 'inject', label: 'tool-jobs' },
form: 'notice',
} as never}
/>,
)
expect(view.container.querySelector('[data-context-summary]')).toBeNull()
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*tool-tasks$/ }))
fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*tool-jobs$/ }))
expect(view.container.querySelector('[data-context-fields]')).not.toBeNull()
})

View File

@@ -137,7 +137,7 @@ const compaction = (over: Partial<CompactionSummaryNode> = {}): CompactionSummar
/** Empty sessions-list hook for the global standard-kit seat. */
function emptySessions() {
const store = createSnapshotStore<SessionListState>(
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined })
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined })
return bindSnapshotSelector(store)
}

View File

@@ -588,12 +588,12 @@ describe('built-in conversation node Definitions', () => {
name: 'compact',
source: { kind: 'user' },
}),
at(11, 'compact/start', {
at(11, 'compaction/start', {
compactionId: 'manual-1',
sourceCommandId: 'command-1',
turn: null,
}),
at(12, 'compact/summary', {
at(12, 'compaction/summary', {
compactionId: 'manual-1',
sourceCommandId: 'command-1',
summary: [{ type: 'text', text: 'manual summary' }],
@@ -609,7 +609,7 @@ describe('built-in conversation node Definitions', () => {
sourceCommandId: 'command-1',
},
}, { surfaceOp: { op: 'replace', start: 1, end: 2 } }),
at(14, 'compact/end', {
at(14, 'compaction/end', {
compactionId: 'manual-1',
sourceCommandId: 'command-1',
turn: null,
@@ -619,8 +619,8 @@ describe('built-in conversation node Definitions', () => {
kind: 'success',
sourceEventSeq: 12,
}),
at(20, 'compact/start', { compactionId: 'automatic-1', turn: null }),
at(21, 'compact/summary', {
at(20, 'compaction/start', { compactionId: 'automatic-1', turn: null }),
at(21, 'compaction/summary', {
compactionId: 'automatic-1',
summary: [{ type: 'text', text: 'automatic summary' }],
shadowedSeqs: [3, 4],
@@ -630,7 +630,7 @@ describe('built-in conversation node Definitions', () => {
...textMessage('automatic-checkpoint', 'checkpoint'),
source: { kind: 'plugin', plugin: 'compact', compactionId: 'automatic-1' },
}, { surfaceOp: { op: 'replace', start: 3, end: 4 } }),
at(23, 'compact/end', { compactionId: 'automatic-1', turn: null }),
at(23, 'compaction/end', { compactionId: 'automatic-1', turn: null }),
])
const manual = node(snapshot(compactions), 'manual-compaction')
@@ -654,8 +654,8 @@ describe('built-in conversation node Definitions', () => {
expect(before?.data).toMatchObject({ summary: null, summaryEventSeq: null })
value.prepend([
at(9, 'compact/start', { compactionId: 'compact-1', turn: null }),
at(10, 'compact/summary', {
at(9, 'compaction/start', { compactionId: 'compact-1', turn: null }),
at(10, 'compaction/summary', {
compactionId: 'compact-1',
summary: [
{ type: 'text', text: 'older ' },
@@ -680,7 +680,7 @@ describe('built-in conversation node Definitions', () => {
it('renders a historical compaction when its start remains outside the loaded window', () => {
const value = assembler([
at(10, 'compact/summary', {
at(10, 'compaction/summary', {
compactionId: 'compact-windowed',
summary: [{ type: 'text', text: 'loaded summary' }],
shadowedSeqs: [1, 2, 3],
@@ -702,10 +702,10 @@ describe('built-in conversation node Definitions', () => {
it('ignores legacy compaction transactions without correlation ids', () => {
const value = assembler([
at(10, 'compact/start', { turn: null }),
at(11, 'compact/end', { turn: null, error: 'This operation was aborted' }),
at(20, 'compact/start', { turn: null }),
at(21, 'compact/summary', {
at(10, 'compaction/start', { turn: null }),
at(11, 'compaction/end', { turn: null, error: 'This operation was aborted' }),
at(20, 'compaction/start', { turn: null }),
at(21, 'compaction/summary', {
summary: [{ type: 'text', text: 'legacy summary' }],
shadowedSeqs: [1, 2, 3],
shadowedTokenCount: 42,
@@ -714,7 +714,7 @@ describe('built-in conversation node Definitions', () => {
...textMessage('legacy-checkpoint', 'checkpoint'),
source: { kind: 'plugin', plugin: 'compact' },
}, { surfaceOp: { op: 'replace', start: 1, end: 3 } }),
at(23, 'compact/end', { turn: null }),
at(23, 'compaction/end', { turn: null }),
], true)
expect(node(snapshot(value), 'compaction')).toBeUndefined()
@@ -897,7 +897,7 @@ describe('built-in conversation node Definitions', () => {
step: 1,
message: toolResult('root', 'root result'),
}, { surfaceOp: 'append' }),
at(20, 'compact/summary', {
at(20, 'compaction/summary', {
compactionId: 'manual-1',
sourceCommandId: 'command-1',
summary: [{ type: 'text', text: 'manual summary' }],

View File

@@ -16,7 +16,7 @@ afterEach(() => {
function emptySessions() {
return bindSnapshotSelector(createSnapshotStore<SessionListState>({
ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined,
ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined,
}))
}

View File

@@ -111,7 +111,7 @@ describe('render branch tails', () => {
const chat = createChatStore().create()
chat.actions.select({ turnSeq: 1, callId: 'ghost' } satisfies SelectionTarget)
const emptyList = createSnapshotStore<SessionListState>(
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined })
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined })
const emptyWorkspaces = createSnapshotStore<WorkspaceListState>({
items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null,
baselinesReady: true, recentWorkspaceId: undefined,
@@ -167,7 +167,7 @@ describe('render branch tails', () => {
const chat = createChatStore().create()
chat.actions.select({ turnSeq: 9, callId: 'p1:code:1:code:1', toolName: 'read' } satisfies SelectionTarget)
const emptyList = createSnapshotStore<SessionListState>(
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined })
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined })
const emptyWorkspaces = createSnapshotStore<WorkspaceListState>({
items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null,
baselinesReady: true, recentWorkspaceId: undefined,

View File

@@ -1,11 +1,11 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import { Settings, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { SettingsProvider, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import {
CONVERSATION_SETTINGS_NAMESPACE, DEFAULT_BUSY_ENTER_BEHAVIOR, apply,
} from '@deepseek-ai/dsh-client-ui-conversation'
class MemorySettings extends Settings {
class MemorySettings extends SettingsProvider {
readonly writable = true
protected load(): Promise<Record<string, unknown>> { return Promise.resolve({}) }
protected persist(_ns: SettingsNamespace, _section: Record<string, unknown>): Promise<void> {

View File

@@ -123,9 +123,9 @@ function bench(over?: BenchOptions) {
// never reached — these benches drive plain-draft flows only).
...(lex !== undefined
? {
slash: (() => ({
inputTriggers: (() => ({
lexicon: { getSnapshot: () => lex, subscribe: () => () => {} },
})) as unknown as NonNullable<ShellDeps['slash']>,
})) as unknown as NonNullable<ShellDeps['inputTriggers']>,
}
: {}),
})
@@ -147,7 +147,7 @@ function bench(over?: BenchOptions) {
useSession: bindSnapshotSelector(session),
useSessions: bindSnapshotSelector(createSnapshotStore({
ids: [], byId: {}, current: undefined, phase: 'ready',
subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined,
subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined,
})),
useWorkspaces: bindSnapshotSelector(createSnapshotStore({
items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null,

View File

@@ -8,7 +8,7 @@
* Pure event sequences — no React, no DOM, no ambient clock.
*/
import { describe, expect, it } from 'vitest'
import type { CommandClaim, ReferenceInsert, TokenSpan } from '@deepseek-ai/dsh-client-ui-slash/client'
import type { CommandClaim, ReferenceInsert, TokenSpan } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { InputEffect, SubmitAttempt } from '../src/client/input/contract.ts'
import { InputMachine, PLACEHOLDER, projectClipboard } from '../src/client/input/machine.ts'
import { deriveDecorations, scanTextRefs } from '../src/client/input/decorations.ts'

View File

@@ -12,7 +12,7 @@ import {
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
} from '@deepseek-ai/dsh-client-runtime/client'
import type { ClientContext, ConversationSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { SubmitOutcome } from '@deepseek-ai/dsh-client-ui-slash/client'
import type { SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
import { SessionInputShell } from '../src/client/input/facade.ts'
@@ -40,7 +40,7 @@ function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled
useSession: bindSnapshotSelector(session),
useSessions: bindSnapshotSelector(createSnapshotStore({
ids: [], byId: {}, current: undefined, phase: 'ready',
subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined,
subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined,
})),
useWorkspaces: bindSnapshotSelector(createSnapshotStore({
items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null,

View File

@@ -1,21 +1,21 @@
// @vitest-environment jsdom
/**
* Scenario-chain integration (scenarios A/C/D/H/I): the real per-session
* SlashController pipeline over a real session scope (SessionsService over
* InputTriggerController pipeline over a real session scope (SessionRuntime over
* a listed host session) + a command source implementing the decision
* table's relevant cells + the real SessionInput machine (scoped-event
* listeners wired the way the hub does) + the real InputBar. ui-command
* listeners wired the way the hub does) + the real InputBar. ui-commands
* itself is not a dependency of this package; the source below is the
* decision-table contract at the `SlashSource` boundary.
* decision-table contract at the `InputTriggerSource` boundary.
*/
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import {
EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS, SessionsService,
EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS, SessionRuntime,
} from '@deepseek-ai/dsh-client-runtime/client'
import { SlashService } from '@deepseek-ai/dsh-client-ui-slash/client'
import type { ClientSessionContext, CommandClaim, PickOutcome, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-slash/client'
import { InputTriggerService } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { ClientSessionContext, CommandClaim, PickOutcome, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import { FakeApiClient, fakeRemote, ok } from '../../runtime/tests/fake-api.client.ts'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
@@ -89,25 +89,25 @@ const COMMANDS: FakeCommand[] = [
{ name: 'compact', description: '压缩上下文' },
]
/** Real scope bench: SessionsService over one listed session + SlashController + shell listeners (the hub wiring shape). */
async function scopedBench(register?: (slash: SlashService) => void) {
/** Real scope bench: SessionRuntime over one listed session + InputTriggerController + shell listeners (the hub wiring shape). */
async function scopedBench(register?: (inputTriggers: InputTriggerService) => void) {
const ctx = new Context()
const api = new FakeApiClient()
api.onWorkspaceList = () => Promise.resolve(ok({ items: [] }))
const sessionId = 'scenario-s1' as Parameters<SessionsService['open']>[0]
const sessionId = 'scenario-s1' as Parameters<SessionRuntime['open']>[0]
api.onList = () => Promise.resolve(ok({
items: [{ sessionId, updatedAt: 1, running: false, blank: false, cwd: '/w/a' }],
}) as never)
const sessions = new SessionsService(ctx, api, fakeRemote()) // provides 'sessions' itself
const sessions = new SessionRuntime(ctx, api, fakeRemote()) // provides 'sessions' itself
await sessions.refresh()
await Promise.resolve() // manager notifier flush
await ctx.plugin(SlashService).await()
const slash = ctx.get('slash') as SlashService
register?.(slash)
await ctx.plugin(InputTriggerService).await()
const inputTriggers = ctx.get('inputTriggers') as InputTriggerService
register?.(inputTriggers)
const actx = sessions.scope(sessionId)!
const controller = slash.sessionOf(actx)
const controller = inputTriggers.sessionOf(actx)
const sink = vi.fn()
const shell = new SessionInputShell({ actx, slash: () => controller, defaultSink: sink })
const shell = new SessionInputShell({ actx, inputTriggers: () => controller, defaultSink: sink })
// The hub's listener wiring, verbatim.
actx.on('slash/input-begin-command', req => shell.beginCommand(req.claim, req.span) ? true : undefined)
actx.on('slash/input-insert-reference', req => shell.insertReference(req.reference, req.span) ? true : undefined)
@@ -126,7 +126,7 @@ async function scopedBench(register?: (slash: SlashService) => void) {
useSession: bindSnapshotSelector(sessionStore),
useSessions: bindSnapshotSelector(createSnapshotStore({
ids: [], byId: {}, current: undefined, phase: 'ready',
subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined,
subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined,
})),
useWorkspaces: bindSnapshotSelector(createSnapshotStore({
items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null,
@@ -164,14 +164,14 @@ async function scopedBench(register?: (slash: SlashService) => void) {
const type = (text: string): void => {
fireEvent.change(textarea, { target: { value: text } })
}
return { ctx, slash, controller, shell, wiring, view, textarea, type, sink }
return { ctx, inputTriggers, controller, shell, wiring, view, textarea, type, sink }
}
async function bench(executeImpl?: (line: string) => Promise<SubmitOutcome>) {
const execute = vi.fn(executeImpl ?? ((line: string) =>
Promise.resolve({ kind: 'success' as const, text: `已执行 ${line}` })))
const { source, executed } = commandSource(COMMANDS, execute)
const base = await scopedBench((slash) => { slash.registerSource(source) })
const base = await scopedBench((inputTriggers) => { inputTriggers.registerSource(source) })
return { ...base, execute, executed }
}
@@ -266,8 +266,8 @@ describe('scenario: reference decoration lights up when the lexicon settles', ()
it('a typed /name token gains the text-ref mark without further input once the roll goes hot', async () => {
let roll: readonly string[] | undefined
let notify: (() => void) | undefined
const b = await scopedBench((slash) => {
slash.registerSource({
const b = await scopedBench((inputTriggers) => {
inputTriggers.registerSource({
trigger: '/', name: 'skill',
candidates: () => Promise.resolve([]),
onPick: () => undefined,
@@ -302,8 +302,8 @@ describe('scenario I: unknown /xyz + enter', () => {
})
it('adjudication failure (source warmup throw) notices and keeps the draft', async () => {
const b = await scopedBench((slash) => {
slash.registerSource({
const b = await scopedBench((inputTriggers) => {
inputTriggers.registerSource({
trigger: '/', name: 'command',
candidates: () => Promise.resolve([]),
onPick: () => undefined,

View File

@@ -1,6 +1,6 @@
// @vitest-environment jsdom
/**
* Exercises selection persistence through the real SlotsService store axis;
* Exercises selection persistence through the real SlotRegistry store axis;
* component stubs cannot prove per-session identity or disposal.
*/
import { beforeEach, describe, expect, it } from 'vitest'
@@ -96,7 +96,7 @@ describe('selection survives on the store seat', () => {
expect(localStorage.getItem('dsh.conversation.chat.s1')).not.toBeNull()
// TestSessions.remove drives the same public slot lifecycle contract the
// production SessionsService calls when the scope dies (pruneStoreScope).
// production SessionRuntime calls when the scope dies (pruneStoreScope).
await b.runtime.sessions.remove('s1')
// Persisted residue is gone with the session...

View File

@@ -1,5 +1,5 @@
// @vitest-environment jsdom
// ConversationService scope addressing over the runtime's real scope tag:
// ConversationController scope addressing over the runtime's real scope tag:
// TestSessions mints tagged scopes through the production createScope, so the
// service's scopeOf/binding path runs against production resolution (no local
// tag probe).
@@ -10,7 +10,7 @@ import { makeTranslate, SlotTestRuntime } from '@deepseek-ai/dsh-client-test-run
import type { QueuedMessage, SessionFace } from '@deepseek-ai/dsh-client-runtime/client'
import { ComposerBlockRegistry } from '../src/client/input/blocks.ts'
import { InputHub } from '../src/client/input/hub.ts'
import { ConversationService, UnsupportedImageMediaTypeError } from '../src/client/service.ts'
import { ConversationController, UnsupportedImageMediaTypeError } from '../src/client/service.ts'
import { zh } from '../src/client/locales.ts'
async function bench(readAttachment?: SessionFace['readAttachment']) {
@@ -26,18 +26,18 @@ async function bench(readAttachment?: SessionFace['readAttachment']) {
// config.input is required (the apply shares its hub with the inject
// factories); the bench passes its own instance explicitly.
const hub = new InputHub(runtime.ctx, makeTranslate(zh, {}))
const fiber = runtime.ctx.plugin(ConversationService, {
const fiber = runtime.ctx.plugin(ConversationController, {
input: hub,
blocks: new ComposerBlockRegistry(),
})
await fiber.await()
const root = runtime.ctx.get('conversation') as ConversationService
const scoped = runtime.sessions.scope('s1')!.get('conversation') as ConversationService
const root = runtime.ctx.get('conversation') as ConversationController
const scoped = runtime.sessions.scope('s1')!.get('conversation') as ConversationController
const shell = hub.shellFor(runtime.sessions.binding('s1')!)
return { runtime, fiber, root, scoped, hub, shell, prompt, updateQueue, cancel, loadOlder }
}
describe('ConversationService', () => {
describe('ConversationController', () => {
it('routes operations through the public Session binding', async () => {
const b = await bench()
await b.scoped.send('hello')
@@ -129,19 +129,19 @@ describe('ConversationService', () => {
await b.runtime.dispose()
})
it('fails loudly from the root scope, on an unbound session, or without SessionsService', async () => {
it('fails loudly from the root scope, on an unbound session, or without SessionRuntime', async () => {
const b = await bench()
await expect(b.root.send('x')).rejects.toThrow(/requires a session scope/)
await b.runtime.sessions.remove('s1')
await expect(b.scoped.send('x')).rejects.toThrow(/resolved no binding/)
await b.runtime.dispose()
// No SessionsService at all: a bare context (the runtime always provides one).
// No SessionRuntime at all: a bare context (the runtime always provides one).
const bare = new Context()
await bare.plugin(ConversationService, {
await bare.plugin(ConversationController, {
input: new InputHub(bare, makeTranslate(zh, {})),
blocks: new ComposerBlockRegistry(),
}).await()
const orphan = bare.get('conversation') as ConversationService
const orphan = bare.get('conversation') as ConversationController
await expect(orphan.send('x')).rejects.toThrow(/sessions service unavailable/)
})
})

View File

@@ -110,7 +110,7 @@ function mount(
ids: listed ? [root, SID] : [root],
byId: { [root]: rootRow, ...listed && { [SID]: childRow } },
current: SID,
phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined,
phase: 'ready', subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined,
})
const workspaces = createSnapshotStore<WorkspaceListState>(workspaceState(workspaceRows))
const session = createSnapshotStore<ConversationSnapshot>(snapshot)

View File

@@ -4,12 +4,12 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import type { ReactNode } from 'react'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import type { ChatViewSlotProps, ConvViewProps } from '../src/client/contract/slots.ts'
describe('view-ring type negatives (compile-time; body never runs)', () => {
it('holds the negative samples as expect-error sites', () => {
const negatives = (slots: SlotsService) => {
const negatives = (slots: SlotRegistry) => {
// 1. List-kind registration requires the id shape field.
// @ts-expect-error missing `id` on a list-slot registration
slots.register({ name: 'conversation.view', order: 1 }, (_p: ConvViewProps) => null)
@@ -58,7 +58,7 @@ describe('view-ring type negatives (compile-time; body never runs)', () => {
describe('view-ring runtime dual (real ledger)', () => {
function bench() {
const ctx = new Context()
const slots = new SlotsService(ctx)
const slots = new SlotRegistry(ctx)
// The conversation entry's role: declare the ring (declaring is claiming).
slots.register({
name: 'root',

View File

@@ -63,7 +63,7 @@
"path": "../../todo/tool-todo"
},
{
"path": "../ui-slash"
"path": "../ui-input-trigger"
},
{
"path": "../ui-layout"
@@ -75,10 +75,10 @@
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../interaction/permission"
"path": "../../interaction/permission-presets"
},
{
"path": "../ui-settings"