mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
docs(client): document web architecture
This commit is contained in:
@@ -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 docs/architecture.md
|
||||
architecture.md: 06d5163566816eb732b51ce784cd2ab92218d4fd
|
||||
architecture.zh.md: 55162553b62300e5e1bb34bab468cb3956877e56
|
||||
architecture.md: e37f2321377242803fb89f0a2258682e6c52801c
|
||||
architecture.zh.md: 69abbcdf52654ecbce6549d25ee089b937228123
|
||||
|
||||
@@ -140,4 +140,4 @@ New behavior attaches to a documented extension point. Changing the loop itself
|
||||
| Fork a live session | `ctx.sessions.fork(source, boundary?, childSessionId?)` |
|
||||
| Scope a registration to one agent | use that agent's `agent.ctx` |
|
||||
|
||||
The [extension cookbook](cookbook/extension-cookbook.md) maps features to capabilities and indexes the step-by-step guides for [packages](cookbook/adding-a-package.md), [tools](cookbook/adding-a-tool.md), [LLM adapters](cookbook/adding-an-llm-adapter.md), [Chat nodes](cookbook/adding-a-conversation-node.md), and [settings cards](cookbook/adding-a-settings-card.md).
|
||||
The [extension cookbook](cookbook/extension-cookbook.md) maps features to capabilities and indexes the step-by-step guides for [packages](cookbook/adding-a-package.md), [tools](cookbook/adding-a-tool.md), [LLM adapters](cookbook/adding-an-llm-adapter.md), and [settings cards](cookbook/adding-a-settings-card.md). The [Conversation subsystem](subsystems/conversation.md) owns Chat-node assembly.
|
||||
|
||||
@@ -144,4 +144,4 @@ seam 正是替换一个提供方就能改变整个产品的原因。文件系统
|
||||
| fork 活跃会话 | `ctx.sessions.fork(source, boundary?, childSessionId?)` |
|
||||
| 将注册项限定到单个 agent | 使用该 agent 的 `agent.ctx` |
|
||||
|
||||
[扩展实操手册](cookbook/extension-cookbook.zh.md)将功能映射到能力,并索引[包](cookbook/adding-a-package.zh.md)、[工具](cookbook/adding-a-tool.zh.md)、[LLM(大语言模型)适配器](cookbook/adding-an-llm-adapter.zh.md)、[Chat 节点](cookbook/adding-a-conversation-node.zh.md)和[设置卡片](cookbook/adding-a-settings-card.zh.md)的分步指南。
|
||||
[扩展实操手册](cookbook/extension-cookbook.zh.md)将功能映射到能力,并索引[包](cookbook/adding-a-package.zh.md)、[工具](cookbook/adding-a-tool.zh.md)、[LLM(大语言模型)适配器](cookbook/adding-an-llm-adapter.zh.md)和[设置卡片](cookbook/adding-a-settings-card.zh.md)的分步指南。[Conversation 子系统](subsystems/conversation.zh.md)负责 Chat node 组装。
|
||||
|
||||
@@ -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 docs/cookbook/extension-cookbook.md
|
||||
extension-cookbook.md: 1081166674cb946675d8864091c50def018f9f97
|
||||
extension-cookbook.zh.md: defdda53e8f8ea849d2f236566bef48751f4ad96
|
||||
extension-cookbook.md: 2fe03506d5b89eff544bbb6dea10a8b6429dfe3e
|
||||
extension-cookbook.zh.md: 506793219988418618e2ae499369d43dabfd20b6
|
||||
|
||||
@@ -34,7 +34,7 @@ This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an
|
||||
|
||||
## A UI plugin
|
||||
|
||||
A UI plugin renders from the `session/event` feed (the assistant token stream as `assistant/chunk`, plus turn/step boundaries and tool activity), and drives input back in via `agent.followup()` / `agent.steer()`. A browser plugin contributing a business row to the built-in Web Client instead registers a `ConversationNodeDefinition` and keyed Chat renderer; follow the [Conversation Node guide](adding-a-conversation-node.md).
|
||||
A UI plugin renders from the `session/event` feed (the assistant token stream as `assistant/chunk`, plus turn/step boundaries and tool activity), and drives input back in via `agent.followup()` / `agent.steer()`. A browser plugin contributing a business row to the built-in Web Client instead registers a `ConversationNodeDefinition` and keyed Chat renderer; follow the [Conversation subsystem reference](../subsystems/conversation.md).
|
||||
|
||||
```ts
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
|
||||
@@ -36,7 +36,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
## UI 插件
|
||||
|
||||
UI 插件从 `session/event` 事件流渲染(助手 token 流以 `assistant/chunk` 形式到达,加上轮次/步骤边界与工具活动),并通过 `agent.followup()` / `agent.steer()` 将输入驱动回去。如果浏览器插件要向内建 Web Client 贡献业务行,则应注册 `ConversationNodeDefinition` 与 keyed Chat renderer;具体步骤见 [Conversation Node 指南](adding-a-conversation-node.zh.md)。
|
||||
UI 插件从 `session/event` 事件流渲染(助手 token 流以 `assistant/chunk` 形式到达,加上轮次/步骤边界与工具活动),并通过 `agent.followup()` / `agent.steer()` 将输入驱动回去。如果浏览器插件要向内建 Web Client 贡献业务行,则应注册 `ConversationNodeDefinition` 与 keyed Chat renderer;具体约定见 [Conversation 子系统参考](../subsystems/conversation.zh.md)。
|
||||
|
||||
```ts
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
|
||||
@@ -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 docs/subsystems/README.md
|
||||
README.md: fabd6c1075280c955b1cf7e3afaea0df6fa98992
|
||||
README.zh.md: 686ed0a5dcd469dfef60eea7b2e679fd4220e666
|
||||
README.md: 2b277650c4b9e320183f75845d68f922b31a522e
|
||||
README.zh.md: 2279857e0d58cb36e8027c196075622344d705ae
|
||||
|
||||
@@ -51,7 +51,10 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures
|
||||
| [webhook.md](webhook.md) | authenticated provider deliveries, arbitrary programmatic rules, and fire-and-forget Workspace Session creation |
|
||||
| [storage.md](storage.md) | the storage subsystem: the backend contract (`StorageBackend`), `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` |
|
||||
| [workspace.md](workspace.md) | the workspace registry: `Workspace`/`WorkspaceId`, registration and resolution, the session `cwd` relationship |
|
||||
| [web-client.md](web-client.md) | the browser architecture: boot, Remote communication, paired Client models, UI adapters, Conversation assembly, Slots, and reconnect semantics |
|
||||
| [client-modules.md](client-modules.md) | the web plugin table: `dsh.client` declarations, `WebBootGraph` wire composition, the bundle route and index tap |
|
||||
| [slots.md](slots.md) | typed Web UI composition: declaration ownership, cardinality and scope, framework and feature injection, props derivation, and the shipped hierarchy |
|
||||
| [conversation.md](conversation.md) | target-neutral Session-event assembly: Context identity, Location data, replay paths, view builders, and target-owned render nodes |
|
||||
| [session-projection.md](session-projection.md) | the projection seam: `SessionProjectionMap`, the pure `ProjectionDefinition` unit, `ProjectionSnapshot`'s consistent cut, the change feed |
|
||||
| [session-telemetry.md](session-telemetry.md) | the outbound session-reporting capability seam: `SessionTelemetryRecord`/`SessionTelemetrySeverity`, the `SessionTelemetrySink` contract, and the `session-telemetry/record` redact waterfall |
|
||||
|
||||
|
||||
@@ -51,7 +51,10 @@
|
||||
| [webhook.md](webhook.zh.md) | 通过身份验证的提供方交付、任意程序化规则,以及 fire-and-forget 的 Workspace Session 创建 |
|
||||
| [storage.md](storage.zh.md) | 存储子系统:后端约定(`StorageBackend`)、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed` |
|
||||
| [workspace.md](workspace.zh.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 |
|
||||
| [web-client.md](web-client.zh.md) | 浏览器架构:启动、Remote 通信、配对的 Client model、UI adapter、Conversation 组装、Slots 与重连语义 |
|
||||
| [client-modules.md](client-modules.zh.md) | Web 插件表:`dsh.client` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
|
||||
| [slots.md](slots.zh.md) | 类型化 Web UI 组合:声明所有权、cardinality 与 scope、框架与功能注入、props 推导及当前层级 |
|
||||
| [conversation.md](conversation.zh.md) | target-neutral Session event 组装:Context identity、Location data、replay 路径、view builder 与 target 自有 render node |
|
||||
| [session-projection.md](session-projection.zh.md) | 投影 seam:`SessionProjectionMap`、纯函数 `ProjectionDefinition` 单元、`ProjectionSnapshot` 的一致切面、变更馈送 |
|
||||
| [session-telemetry.md](session-telemetry.zh.md) | 对外会话上报能力 seam:`SessionTelemetryRecord`/`SessionTelemetrySeverity`、`SessionTelemetrySink` 约定和 `session-telemetry/record` 脱敏 waterfall |
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 docs/cookbook/adding-a-conversation-node.md
|
||||
adding-a-conversation-node.md: daa86f90473cb7023a21e1cfa25339fcd79aa558
|
||||
adding-a-conversation-node.zh.md: 8c6360a99acb49aebadf9a28f440853b18514b9a
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/conversation.md
|
||||
conversation.md: d26abf73292faacdf3a4186819c4738d1de0270e
|
||||
conversation.zh.md: 7fff9e0433b0022c75a35d3885398f241818a21d
|
||||
@@ -1,12 +1,26 @@
|
||||
# Add a Web Client conversation node
|
||||
# Conversation assembly
|
||||
|
||||
English | [中文](adding-a-conversation-node.zh.md)
|
||||
English | [中文](conversation.zh.md)
|
||||
|
||||
This tutorial adds one business-owned row to the Web Client Chat view. The finished plugin correlates a durable Session event family into one Context, incrementally builds business State, publishes typed Step data, and renders a keyed Chat Node without scanning the Session window or other rendered nodes. It assumes the Host already records the events and the client plugin is composed into the Web bundle; external Host-side UIs and additional view targets such as Trajectory are outside this tutorial.
|
||||
Conversation is the target-neutral assembly layer between a Client Session event window and browser views. [`ui-conversation`](../../packages/client/ui-conversation/README.md) owns the event and view registries, one identity-stable binding per `SessionBinding`, Turn/Step locations, incremental Context assembly, target sources, the shared shell, and input orchestration. Target packages such as [`ui-chat`](../../packages/client/ui-chat/README.md) and [`ui-trajectory`](../../packages/client/ui-trajectory/README.md) own their Definitions, final snapshots, and rendering.
|
||||
|
||||
The [Conversation Node assembly decision](../../.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md) owns the rationale and complete engine model. This guide covers the implementation path.
|
||||
This page defines the data model and the extension path for a business-owned Conversation node. The [Web Client architecture](web-client.md) places the subsystem between Client models and Slots; the [Conversation Node assembly decision](../../.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md) owns its rationale.
|
||||
|
||||
## 1. Design a replayable event family
|
||||
## Data model and ownership
|
||||
|
||||
The Session Controller owns the contiguous loaded event window. `ui-conversation` observes that existing source and converts each entry to `{ event, view? }`; it never opens a second history stream. One `ConversationNodeAssembler` per Session applies every registered Definition and publishes an independent source for each registered view target.
|
||||
|
||||
| Concept | Owner and purpose |
|
||||
|---|---|
|
||||
| Event Definition | A business package matches one event at a time, correlates it by stable `(kind, id)`, folds deterministic State, and optionally materializes one target node. |
|
||||
| Context | The engine-owned ordered Matches and current State for one `(kind, id)`. Update-only evidence may remain pending until pagination supplies its unique start. |
|
||||
| Location | The engine-owned Session, Turn, or Step coordinates derived from durable boundary events. Definitions may publish typed data onto one Turn or Step. |
|
||||
| View Definition | A target package creates one incremental builder per Session and owns the final snapshot type for that target. |
|
||||
| View | A Slot entry such as Chat or Trajectory reads only its target snapshot and renders target-owned nodes. |
|
||||
|
||||
Chat and Trajectory may recognize the same durable event family, but each keeps its own Definition State and final node payload. Shared target-neutral machinery is limited to identity routing, ordered replay, Location data, predecessor dependencies, and publication cadence.
|
||||
|
||||
## Replayable event families
|
||||
|
||||
Choose one stable business id before writing the Definition. Every event that contributes to the same Node must carry that id or derive it independently from its own payload; the client must never assign an update to “the latest unfinished” Context.
|
||||
|
||||
@@ -22,7 +36,7 @@ Use the producer-owned branded id type across the process boundary. Put the `Ses
|
||||
|
||||
Incremental events are supported. Prefer whole-value checkpoints when the producer can emit them cheaply, because they remain useful when the start is outside the loaded window. Each delta must carry the stable id and produce deterministic State when replayed in ascending log `seq`; it must not depend on live-only memory. If the current history window contains only updates, the assembler keeps a pending Context and builds no State until an older page supplies the start. If the product must render before the start is loaded, a terminal or checkpoint event must carry enough whole fallback state for the Definition to build that result directly; do not recover it by scanning unrelated events.
|
||||
|
||||
## 2. Implement the Definition and typed Chat payload
|
||||
## Definition and typed Chat payload
|
||||
|
||||
The example keeps the producer declarations and client contribution in one block so the complete relationship is visible. In a package family, keep the branded id and `SessionEventMap` declaration with the event producer, and keep the Definition, Chat data merge, and renderer in the client plugin.
|
||||
|
||||
@@ -89,7 +103,7 @@ interface ReviewChatData {
|
||||
readonly summary?: string
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
|
||||
declare module '@deepseek-ai/dsh-client-ui-chat/client' {
|
||||
interface ChatNodeDataMap {
|
||||
'review-job': ReviewChatData
|
||||
}
|
||||
@@ -200,13 +214,13 @@ export function apply(ctx: ClientContext): void {
|
||||
|
||||
`target` and `buildViewNode(context)` declare one target-owned rendering contribution and must appear together. Preserve `context.key` as the React-facing identity, choose `anchorSeq` from durable ordering evidence, and return only renderer-ready data. Once a target Node has been published, keep returning the same key; use `visibility: 'hidden'` when it must temporarily leave the visible flow rather than withdrawing it with `null`.
|
||||
|
||||
## 3. Query an earlier business Context only at start
|
||||
## Predecessor reads
|
||||
|
||||
Some Definitions need the latest earlier State of another business kind. `start` receives a `ConversationContextReader`; call `reader.previous<State>(kind)` there instead of accepting a Context collection or scanning events. The reader returns the nearest started Context before the current start `seq` as read-only data.
|
||||
|
||||
The assembler records that dependency. If an older prepend later supplies a nearer predecessor, closes a previously unknown window gap, or revises the predecessor State, it reruns the dependent Context from `start` and replays its updates in ascending `seq`. The queried Definition remains responsible for writing useful State; the reader exposes no business-specific query methods and grants no mutation authority over another Context.
|
||||
|
||||
## 4. Understand the three ingestion paths
|
||||
## Window update paths
|
||||
|
||||
History may be requested from the tail backward one page at a time, but every accepted page is normalized into ascending `seq` before State replay.
|
||||
|
||||
@@ -220,7 +234,7 @@ With `D` registered Definitions, one incoming event performs `D` current-event m
|
||||
|
||||
`publication` controls when changed State is materialized. Use `immediate` for structural or terminal changes, `animation-frame` for high-frequency visible deltas, and `none` when the State change feeds only a later publication. The engine still applies every update in log order; cadence only coalesces view publication.
|
||||
|
||||
## 5. Verify replay, pagination, and rendering
|
||||
## Verification obligations
|
||||
|
||||
Add focused tests that establish these outcomes:
|
||||
|
||||
+24
-10
@@ -1,12 +1,26 @@
|
||||
# 添加 Web Client Conversation Node
|
||||
# Conversation 组装
|
||||
|
||||
[English](adding-a-conversation-node.md) | 中文
|
||||
[English](conversation.md) | 中文
|
||||
|
||||
本教程为 Web Client Chat 视图添加一行由业务自行拥有的内容。完成后的插件会把一个持久 Session 事件族关联成一个 Context,增量构造业务 State,发布类型化 Step 数据,再渲染 keyed Chat Node;整个过程不扫描 Session 窗口或其他已渲染节点。本教程假设 Host 已经记录这些事件,且该 Client 插件已组装进 Web bundle;Host 侧外部 UI 和 Trajectory 等额外视图目标不在本文范围内。
|
||||
Conversation 是 Client Session event window 与浏览器 view 之间的 target-neutral assembly 层。[`ui-conversation`](../../packages/client/ui-conversation/README.zh.md)拥有 event 与 view registry、每个 `SessionBinding` 对应的 identity-stable binding、Turn/Step Location、增量 Context assembly、target source、共享 shell 与输入编排。[`ui-chat`](../../packages/client/ui-chat/README.zh.md)和 [`ui-trajectory`](../../packages/client/ui-trajectory/README.zh.md)等 target 包拥有各自的 Definition、最终 snapshot 与渲染。
|
||||
|
||||
[Conversation Node 组装决策](../../.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md)记录完整的引擎模型和设计理由;本文只说明实现路径。
|
||||
本文定义数据模型与业务自有 Conversation node 的扩展路径。[Web Client 架构](web-client.zh.md)说明该子系统在 Client model 与 Slots 之间的位置;[Conversation Node 组装决策](../../.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md)记录其设计理由。
|
||||
|
||||
## 1. 设计可回放的事件族
|
||||
## 数据模型与所有权
|
||||
|
||||
Session Controller 拥有连续的已加载 event window。`ui-conversation` 观察这一个现有 source,并把每个 entry 转换为 `{ event, view? }`;它绝不另开一条 history stream。每个 Session 对应一个 `ConversationNodeAssembler`,它应用所有已注册 Definition,并为每个已注册 view target 发布独立 source。
|
||||
|
||||
| 概念 | Owner 与用途 |
|
||||
|---|---|
|
||||
| Event Definition | 业务包一次匹配一条 event,以稳定 `(kind, id)` 关联事件、折叠确定性 State,并可选择 materialize 一个 target node。 |
|
||||
| Context | Engine 为一个 `(kind, id)` 拥有的有序 Match 与当前 State。只有 update 的证据可以保持 pending,直到分页补齐其唯一 start。 |
|
||||
| Location | Engine 根据持久 boundary event 推导的 Session、Turn 或 Step 坐标。Definition 可以向一个 Turn 或 Step 发布类型化数据。 |
|
||||
| View Definition | Target 包为每个 Session 创建一个增量 builder,并拥有该 target 的最终 snapshot 类型。 |
|
||||
| View | Chat 或 Trajectory 等 Slot entry 只读取自身 target snapshot,并渲染 target 自有 node。 |
|
||||
|
||||
Chat 与 Trajectory 可以识别同一个持久 event family,但各自保留自己的 Definition State 与最终 node payload。共享的 target-neutral 机制只包括 identity routing、有序 replay、Location data、predecessor dependency 与 publication cadence。
|
||||
|
||||
## 可回放 event family
|
||||
|
||||
编写 Definition 前先选定稳定的业务 id。构成同一个 Node 的每条事件都必须携带该 id,或只凭自身 payload 独立推导出该 id;Client 绝不能把 update 猜测为属于“最近一个未完成”的 Context。
|
||||
|
||||
@@ -22,7 +36,7 @@
|
||||
|
||||
系统支持增量事件。如果生产方能以较低成本发出 whole-value checkpoint,应优先采用,因为 start 位于已加载窗口之外时它仍可直接使用。每条 delta 都必须携带稳定 id,并且按照日志 `seq` 升序回放时能够确定性地产生 State;它不能依赖只存在于实时内存中的状态。如果当前历史窗口只有 update,Assembler 会保留一个 pending Context,并在更早分页补齐 start 前不构造 State。如果产品必须在 start 尚未加载时渲染,terminal 或 checkpoint 事件就必须携带足够的完整 fallback 状态,让 Definition 能直接构造结果;不要通过扫描无关事件恢复它。
|
||||
|
||||
## 2. 实现 Definition 与类型化 Chat payload
|
||||
## Definition 与类型化 Chat payload
|
||||
|
||||
为了完整展示关联关系,下面把生产方声明和 Client 贡献写在同一个代码块里。实际的包族中,branded id 与 `SessionEventMap` 声明留在事件生产方,Definition、Chat data 合并与 renderer 留在 Client 插件。
|
||||
|
||||
@@ -89,7 +103,7 @@ interface ReviewChatData {
|
||||
readonly summary?: string
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
|
||||
declare module '@deepseek-ai/dsh-client-ui-chat/client' {
|
||||
interface ChatNodeDataMap {
|
||||
'review-job': ReviewChatData
|
||||
}
|
||||
@@ -200,13 +214,13 @@ export function apply(ctx: ClientContext): void {
|
||||
|
||||
`target` 与 `buildViewNode(context)` 必须同时声明一项由 target 拥有的渲染贡献。把 `context.key` 保留为 React 侧身份,根据持久排序证据选择 `anchorSeq`,并且只返回 renderer 可以直接使用的数据。某个 target Node 一旦发布,就要继续返回同一个 key;需要暂时离开可见流时使用 `visibility: 'hidden'`,不要改为返回 `null` 撤回它。
|
||||
|
||||
## 3. 只在 start 时查询更早的业务 Context
|
||||
## Predecessor read
|
||||
|
||||
有些 Definition 需要另一个业务 kind 在当前位置之前的最新 State。`start` 会收到 `ConversationContextReader`;应在这里调用 `reader.previous<State>(kind)`,不要接收 Context 集合或扫描事件。Reader 返回当前 start `seq` 之前最近一个已启动 Context 的只读数据。
|
||||
|
||||
Assembler 会记录这项依赖。如果后续 older prepend 带来了更近的前序 Context、补齐了原先未知的窗口缺口,或者前序 State 被修订,引擎会从 `start` 重新运行依赖方 Context,并按 `seq` 升序回放其 update。被查询的 Definition 仍负责把有用信息写入自身 State;Reader 不提供业务专用查询方法,也不授予修改其他 Context 的权限。
|
||||
|
||||
## 4. 理解三条摄入路径
|
||||
## Window 更新路径
|
||||
|
||||
历史可能从尾部开始一页一页向前请求,但每个已接收分页都会先按 `seq` 升序归一化,再进入 State 回放。
|
||||
|
||||
@@ -220,7 +234,7 @@ Assembler 会记录这项依赖。如果后续 older prepend 带来了更近的
|
||||
|
||||
`publication` 控制发生 State 变更后何时物化。结构或 terminal 变化使用 `immediate`,高频可见 delta 使用 `animation-frame`,只为后续发布积累 State 时使用 `none`。引擎仍会按日志顺序应用每条 update;该选项只合并视图发布频率。
|
||||
|
||||
## 5. 验证回放、分页与渲染
|
||||
## 验证要求
|
||||
|
||||
添加聚焦测试,证明以下结果:
|
||||
|
||||
@@ -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 docs/subsystems/session.md
|
||||
session.md: 7d80adfc25e3ebb9f482a3e1c84c16163dba318e
|
||||
session.zh.md: 6337a54bd221a3908793c2231bcde4af8a879bb9
|
||||
session.md: edb8f4ebb427bfce6e4def023e65f4697608ceb2
|
||||
session.zh.md: 7b3c7a8e50688ba19694d5f45e43d224c2245ef1
|
||||
|
||||
@@ -573,7 +573,7 @@ Consumers that order Sessions by human activity exclude this boundary: picking a
|
||||
|
||||
A plugin may declaration-merge extra `SessionEventMap` types. These are **log-only**: NOT `SurfaceEventType`s (they carry no `surfaceOp` and contribute nothing to derived history). Their owner decides whether they belong to an open execution turn or may stand between turns, and enforces any relation in its own invariant companion. The generated [persistence log event catalog](../persistence-catalog.md) enumerates every core and plugin-contributed event with its payload, surface badge, and declaration site; the compaction seam's `compaction/*` semantics are discussed on [compaction.md](compaction.md).
|
||||
|
||||
When several events in one plugin-owned family assemble into one Web Client Conversation Node, every start, update, result, resource, or interruption event in that family carries or independently derives the same stable business id. This requirement applies to correlated Node families, not to every Session event; it lets the client group each event without guessing from adjacency or scanning history. See the [Conversation Node cookbook](../cookbook/adding-a-conversation-node.md).
|
||||
When several events in one plugin-owned family assemble into one Web Client Conversation Node, every start, update, result, resource, or interruption event in that family carries or independently derives the same stable business id. This requirement applies to correlated Node families, not to every Session event; it lets the client group each event without guessing from adjacency or scanning history. See the [Conversation subsystem](conversation.md).
|
||||
|
||||
The hook bridges' `hook/invoked` / `hook/result` pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record because it runs before turn 1; its context remains pending in the inbox until a waking delivery opens a turn (see [the hook-bridges Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)).
|
||||
|
||||
|
||||
@@ -577,7 +577,7 @@ interface TurnEndReasonMap {
|
||||
|
||||
插件可以通过 declaration merging 添加额外的 `SessionEventMap` 类型。这些是**仅日志**事件:不是 `SurfaceEventType`(不携带 `surfaceOp`,不参与派生历史)。事件所有方决定它们属于一个开放的执行轮次,还是可以独立位于轮次之间,并在自己的不变量配套插件中强制所需关系。生成的[持久化日志事件目录](../persistence-catalog.zh.md)会列出每个核心或插件贡献的事件,以及其 payload、surface 标记和声明位置;压缩 seam 的 `compaction/*` 语义在 [compaction.md](compaction.zh.md) 中讨论。
|
||||
|
||||
如果同一个插件事件族中的多条事件要组装成一个 Web Client Conversation Node,该事件族中的每条 start、update、result、resource 或 interruption 事件都必须携带或独立推导出同一个稳定业务 id。此要求只约束需要关联的 Node 事件族,并不要求每条 Session 事件都有业务 id;Client 因此无须根据相邻关系猜测归属,也无须扫描历史。参见 [Conversation Node 实操手册](../cookbook/adding-a-conversation-node.zh.md)。
|
||||
如果同一个插件事件族中的多条事件要组装成一个 Web Client Conversation Node,该事件族中的每条 start、update、result、resource 或 interruption 事件都必须携带或独立推导出同一个稳定业务 id。此要求只约束需要关联的 Node 事件族,并不要求每条 Session 事件都有业务 id;Client 因此无须根据相邻关系猜测归属,也无须扫描历史。参见 [Conversation 子系统](conversation.zh.md)。
|
||||
|
||||
钩子桥接层的 `hook/invoked` / `hook/result` 对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。`UserPromptSubmit`、`PreToolUse`、`PostToolUse` 与 `Stop` 在 loop 已打开的轮次内触发,因此其 `hook/*` 记录天然位于轮次之内。`SessionStart` 不生成 `hook/*` 记录,因为它在轮次 1 之前运行;其上下文会在 inbox 中保持待处理,直到唤醒交付打开一个轮次(见[钩子桥接 Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md))。
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 docs/subsystems/slots.md
|
||||
slots.md: d201223c9e630f16f310d8ff90318ab8c43211e5
|
||||
slots.zh.md: 23277e94e2a9e85be7f1745a172dfd9cb8a5be37
|
||||
@@ -0,0 +1,171 @@
|
||||
# Web Client Slots
|
||||
|
||||
English | [中文](slots.zh.md)
|
||||
|
||||
Slots are the Web Client's typed React composition system. [`dsh-client-ui-slots`](../../packages/client/ui-slots/README.md) defines the React-free registry and type algebra; [`dsh-client-ui-renderer`](../../packages/client/ui-renderer/README.md) binds observable sources to hooks, renders the tree, and owns React contexts internally. A feature plugin contributes UI through `ctx.slots.register()` and never imports another feature plugin's component.
|
||||
|
||||
This page documents slot ownership, component inputs, extension APIs, and the shipped hierarchy. The surrounding boot, Remote, Client model, and Conversation paths are in [Web Client architecture](web-client.md).
|
||||
|
||||
## Declaration and lifecycle
|
||||
|
||||
`SlotMap` is the compile-time registry. A package declaration-merges the key, cardinality, scope, owner props, keyed props, and optional slot-level inject face. The runtime declaration is the matching `children` entry on the component that owns the render location.
|
||||
|
||||
Declaring a child has three effects: it makes the child key live, authorizes that parent entry's `renderSlot` or `renderSlotChain` call, and records the runtime dispatch specification. One live entry owns each declaration. Registering into an undeclared slot or declaring a child already owned elsewhere fails during plugin activation.
|
||||
|
||||
`root` is the only built-in declaration and the only key rendered through the Cordis service itself. `ui-renderer` calls `ctx.slots.renderSlot('root', {})`; every descendant is rendered through the `renderSlot` or `renderSlotChain` prop of the entry that declared it.
|
||||
|
||||
Registrations and declarations follow Cordis effect lifetimes. Disposing an entry removes its contribution and recursively collapses the child slots it declared. A feature that contributes into another package's slot therefore uses `ctx.slots.inject(key, callback)`: the callback runs for each declaration lifetime, its effects are removed when the owner collapses, and it runs again if the owner is mounted again.
|
||||
|
||||
```tsx ignore-check
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
|
||||
type HeaderActionProps = PropsRuntime<'conversation.session.header.actions'>
|
||||
|
||||
function HeaderAction({ useSession }: HeaderActionProps) {
|
||||
const running = useSession(snapshot => snapshot.running)
|
||||
return <button disabled={running}>Review</button>
|
||||
}
|
||||
|
||||
export const inject = ['slots']
|
||||
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.slots.inject('conversation.session.header.actions', () =>
|
||||
ctx.slots.register({
|
||||
name: 'conversation.session.header.actions',
|
||||
id: 'review',
|
||||
order: 100,
|
||||
}, HeaderAction))
|
||||
}
|
||||
```
|
||||
|
||||
## Cardinality and scope
|
||||
|
||||
The slot declaration fixes two independent axes.
|
||||
|
||||
| Axis | Value | Meaning |
|
||||
|---|---|---|
|
||||
| cardinality | `single` | One cell. The active priority winner renders. Use a child slot instead of treating this as an additive list. |
|
||||
| cardinality | `list` | Cells are addressed by required `id` and ordered by `order`, then registration order. |
|
||||
| cardinality | `keyed` | The owner dispatches an `entryKey`; the matching cell renders with any key-specific props. |
|
||||
| cardinality | `chain` | Each entry supplies a pure `select(owner)` function. The first non-null result in priority order renders and receives that result as `matched`; otherwise the owner fallback renders. |
|
||||
| scope | `root` | One root-scoped component and store instance. |
|
||||
| scope | `session-maybe` | Follows current selection but stays renderable without a Session; Session values are optional. |
|
||||
| scope | `session` | Requires a resolved Session binding and receives definite Session values. |
|
||||
|
||||
`priority` is a shadowing rank for `single`, `list`, and `keyed` cells and an election order for `chain`. Lower values run or render first. Ordinary additive contributions should choose a fresh list `id` or keyed `key`; intentionally reusing a shipped cell replaces its presentation.
|
||||
|
||||
## Component inputs
|
||||
|
||||
A registered component receives inputs assembled at its binding site. Components derive these types rather than copying their members.
|
||||
|
||||
| Input | Declared by | Component type |
|
||||
|---|---|---|
|
||||
| owner values and standard scope values | the `SlotMap` row and installed scope adapters | `PropsRuntime<K>` |
|
||||
| authorized child renderers | the registration's `children` keys | `PropsRenderSlots<S>` |
|
||||
| selector hook and mutation callbacks for shared view state | the registration's `store` | `PropsStore<H>` |
|
||||
| private data, callbacks, and observable hooks | the registration's `inject` factory | `InjectFace<I>` |
|
||||
| localized `t` function | the registration's `locale` namespace | `PropsLocale<N>` |
|
||||
| selected chain value | the registration's `select` result | `matched` through `ComposedProps` |
|
||||
|
||||
`SessionProvider` is also present in `PropsRenderSlots` when an entry declares a strict Session child. It binds that subtree to the current Session identity and remounts the body when the identity changes.
|
||||
|
||||
Components never receive `ctx`. Parent-owned point-in-time values enter through the owner argument to `renderSlot`; shared view state uses a declared store; services and model objects stay in the `apply` closure and are projected into callbacks or observable sources.
|
||||
|
||||
## Framework-provided hooks
|
||||
|
||||
The shipped adapters add these standard props. They are available according to the target slot's scope, independent of which package registered the component.
|
||||
|
||||
| Availability | Props | Owner |
|
||||
|---|---|---|
|
||||
| every scope | `useSessions`, `useSessionPendingInteraction` | `ui-session` |
|
||||
| every scope | `useWorkspaces` | `ui-workspace` |
|
||||
| `session` | `sessionId`, `useSession`, `useProjection` | `ui-session` |
|
||||
| `session-maybe` | optional `sessionId`, `useSession`, `useProjection` results | `ui-session` |
|
||||
| `session` | `useConversation`, `useInput`, `inputActions` | `ui-conversation` |
|
||||
| `session-maybe` | optional `useConversation`, `useInput`, `inputActions` results | `ui-conversation` |
|
||||
| `session` | `useChat` | `ui-chat` |
|
||||
| `session` | `useTrajectory` | `ui-trajectory` |
|
||||
|
||||
The renderer also creates `useStore` from a declared store and `t` from a declared locale namespace. These are registration-derived props rather than global standard props.
|
||||
|
||||
Framework and domain-adapter owners may extend the standard set through `ctx.slots.provideRoot()` or `ctx.uiSession.provide()` together with the corresponding `GlobalStandardProps`, `SessionStandardProps`, or `SessionMaybeStandardProps` declaration merge. A feature component should not create a React hook prop itself or add a global standard prop for entry-private data.
|
||||
|
||||
## Developer-provided injection
|
||||
|
||||
The `inject` option on a registration is the ordinary feature-owned injection point. Its factory runs in the plugin's `apply` world, may close over injected Cordis services, and returns only the data and callbacks that the component needs. For a `session` slot it receives `sessionId`; for `session-maybe` it receives `sessionId | undefined`; when a store is declared it also receives the store's bound actions.
|
||||
|
||||
A reserved `hooks` object in that return value accepts bare `getSnapshot`/`subscribe` sources. The renderer converts `hooks: { status }` into a `useStatus(selector)` component prop and caches the binding by source identity. Components do not receive the source itself and do not call `useSyncExternalStore` directly.
|
||||
|
||||
The owner of a slot may put an `inject` face in the child declaration when every occupant needs the same capability. Plain members reach all occupants unchanged. Function-valued members inside its `hooks` object are hook factories; they receive the slot's standard props and optional per-render `hookContext`, then return the constrained hook exposed to the occupant. `conversation.chat.node` uses this mechanism to provide `useTurnData(key)` for the node currently being rendered.
|
||||
|
||||
Use owner props for values already known at one render occurrence, registration `inject` for one entry's callbacks and private observables, slot-level `inject` for a capability controlled by the slot owner, and a declared store for mutable view state shared across entries or preserved across remounts. React nodes compose through child slots, not through injected values.
|
||||
|
||||
## Current hierarchy
|
||||
|
||||
The hierarchy below is the shipped declaration tree. A child exists only while the named parent entry is mounted; optional feature entries can therefore make a subtree appear or disappear as one lifecycle unit.
|
||||
|
||||
```text
|
||||
root
|
||||
├─ sidebar
|
||||
│ ├─ sidebar.brand.mark
|
||||
│ ├─ sidebar.brand.name
|
||||
│ ├─ sidebar.footer.action
|
||||
│ ├─ sidebar.workspaces
|
||||
│ │ └─ sidebar.workspaces.directoryFlow
|
||||
│ └─ sidebar.settings
|
||||
│ ├─ settings.trigger
|
||||
│ ├─ settings.header
|
||||
│ ├─ settings.action
|
||||
│ ├─ settings.close
|
||||
│ ├─ settings.onboarding
|
||||
│ └─ settings.section
|
||||
│ ├─ settings.general.item
|
||||
│ └─ settings.plugins.tab
|
||||
│ └─ settings.plugin.item
|
||||
├─ conversation
|
||||
│ ├─ conversation.session
|
||||
│ │ └─ conversation.view
|
||||
│ │ ├─ conversation.chat.node
|
||||
│ │ │ ├─ conversation.chat.assistant-actions
|
||||
│ │ │ ├─ conversation.chat.commandview
|
||||
│ │ │ ├─ conversation.chat.turnTail
|
||||
│ │ │ └─ tool.call.toolview
|
||||
│ │ │ └─ tool.view.cordis
|
||||
│ │ └─ conversation.message.images
|
||||
│ ├─ conversation.session.header
|
||||
│ │ ├─ conversation.session.header.lineage
|
||||
│ │ ├─ conversation.session.header.actions
|
||||
│ │ └─ conversation.session.header.utilities
|
||||
│ ├─ conversation.composer
|
||||
│ │ └─ conversation.approval.detail
|
||||
│ ├─ conversation.composer.bar
|
||||
│ │ ├─ conversation.input.attachments
|
||||
│ │ ├─ conversation.input.plan
|
||||
│ │ └─ conversation.input.model
|
||||
│ ├─ conversation.input.overlay
|
||||
│ ├─ conversation.input.dock
|
||||
│ ├─ conversation.composer.dock
|
||||
│ ├─ conversation.input.left
|
||||
│ ├─ conversation.input.right
|
||||
│ ├─ conversation.hero.brand.mark
|
||||
│ ├─ conversation.hero.workspace
|
||||
│ │ └─ conversation.hero.workspace.directoryFlow
|
||||
│ └─ conversation.hero.agentPreset
|
||||
├─ details
|
||||
│ └─ conversation.details.tool
|
||||
└─ shell.overlay
|
||||
```
|
||||
|
||||
The generated Client inspect catalog is the exhaustive contract for each key: cardinality, scope, owner props, standard props, current occupants, declaration owner, and replacement risk. A running dynamic package can query the live tree and an exact key with `cordis_inspect what:"client"`; the source catalog is generated from `SlotMap` declarations and `slots.register()` call sites by `pnpm run gen-client-catalog`.
|
||||
|
||||
## Extension rules
|
||||
|
||||
- Import another feature package only for declarations with `import type`; never import or re-export its runtime values.
|
||||
- Declare a new child slot only in the component that owns and renders that location. Other packages wait with `ctx.slots.inject()` and contribute through `ctx.slots.register()`.
|
||||
- Keep business and transport state in their owning Cordis services or Client models. Slot stores hold shared viewing and interaction state only.
|
||||
- Keep observable source and snapshot identities stable between changes. Republish through the same source whenever its value changes.
|
||||
- Pass JSON-compatible data and callbacks between UI domains. The `hooks` compartment is the sole exception for bare observables; React content travels through slots.
|
||||
- Treat `single` and an occupied keyed cell as replacement points. Use list ids or an unoccupied key for additive extensions.
|
||||
@@ -0,0 +1,171 @@
|
||||
# Web Client Slots
|
||||
|
||||
[English](slots.md) | 中文
|
||||
|
||||
Slots 是 Web Client 的类型化 React 组合系统。[`dsh-client-ui-slots`](../../packages/client/ui-slots/README.zh.md)定义不依赖 React 的注册表与类型代数;[`dsh-client-ui-renderer`](../../packages/client/ui-renderer/README.zh.md)把可观测源绑定成钩子、渲染整棵树,并在内部拥有 React context。功能插件通过 `ctx.slots.register()` 贡献 UI,绝不导入其他功能插件的组件。
|
||||
|
||||
本文记录 slot 的所有权、组件输入、扩展 API 与当前层级。外围的启动、Remote、Client model 与 Conversation 数据通路见 [Web Client 架构](web-client.zh.md)。
|
||||
|
||||
## 声明与生命周期
|
||||
|
||||
`SlotMap` 是编译期注册表。包通过声明合并写入 key、cardinality(基数)、scope、owner props、keyed props 与可选的 slot 级 inject face。运行时声明则是拥有该渲染位置的组件在 `children` 中给出的对应条目。
|
||||
|
||||
声明一个 child 会同时产生三种效果:令该 child key 生效、授权 parent entry 调用 `renderSlot` 或 `renderSlotChain`,以及记录运行时 dispatch 规格。每个声明只能有一个存活 owner。向未声明 slot 注册,或重复声明其他 entry 已拥有的 child,都会在插件激活时失败。
|
||||
|
||||
`root` 是唯一内建声明,也是唯一由 Cordis service 自身渲染的 key。`ui-renderer` 调用 `ctx.slots.renderSlot('root', {})`;其余每个后代都通过声明它的 entry 所收到的 `renderSlot` 或 `renderSlotChain` prop 渲染。
|
||||
|
||||
注册和声明遵循 Cordis effect 生命周期。销毁一个 entry 会移除其贡献,并递归折叠它声明的 child slots。因此,向其他包的 slot 贡献功能时使用 `ctx.slots.inject(key, callback)`:callback 会在每段声明生命周期内运行,owner 折叠时其 effect 随之移除,owner 再次挂载时则重新运行。
|
||||
|
||||
```tsx ignore-check
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
|
||||
type HeaderActionProps = PropsRuntime<'conversation.session.header.actions'>
|
||||
|
||||
function HeaderAction({ useSession }: HeaderActionProps) {
|
||||
const running = useSession(snapshot => snapshot.running)
|
||||
return <button disabled={running}>Review</button>
|
||||
}
|
||||
|
||||
export const inject = ['slots']
|
||||
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.slots.inject('conversation.session.header.actions', () =>
|
||||
ctx.slots.register({
|
||||
name: 'conversation.session.header.actions',
|
||||
id: 'review',
|
||||
order: 100,
|
||||
}, HeaderAction))
|
||||
}
|
||||
```
|
||||
|
||||
## Cardinality 与 scope
|
||||
|
||||
Slot 声明固定两个相互独立的维度。
|
||||
|
||||
| 维度 | 值 | 含义 |
|
||||
|---|---|---|
|
||||
| cardinality | `single` | 单个 cell,渲染当前 priority 胜者;需要并列内容时应声明 child slot,而不是把它当作列表。 |
|
||||
| cardinality | `list` | cell 由必填 `id` 定址,先按 `order`、再按注册顺序排列。 |
|
||||
| cardinality | `keyed` | owner 传入 `entryKey`;匹配 cell 以该 key 对应的 props 渲染。 |
|
||||
| cardinality | `chain` | 每个 entry 提供纯 `select(owner)` 函数;按 priority 顺序遇到的第一个非 null 结果获选,并以 `matched` 传给组件;全部拒绝时渲染 owner fallback。 |
|
||||
| scope | `root` | 一个 root 作用域组件和 store 实例。 |
|
||||
| scope | `session-maybe` | 跟随当前选择,但没有 Session 时仍可渲染;Session 值是可选的。 |
|
||||
| scope | `session` | 要求可解析的 Session binding,并收到确定存在的 Session 值。 |
|
||||
|
||||
对于 `single`、`list` 和 `keyed` cell,`priority` 是遮蔽优先级;对于 `chain`,它是选举顺序。数值越小越先运行或渲染。普通增量贡献应选用新的 list `id` 或 keyed `key`;复用已有 cell 表示有意替换其展示。
|
||||
|
||||
## 组件输入
|
||||
|
||||
注册组件会在 binding 位置收到组装后的输入。组件应从这些类型推导 props,不要重新抄写成员。
|
||||
|
||||
| 输入 | 声明者 | 组件类型 |
|
||||
|---|---|---|
|
||||
| owner 值与标准 scope 值 | `SlotMap` 条目与已安装的 scope adapter | `PropsRuntime<K>` |
|
||||
| 获授权的 child renderer | 注册项的 `children` keys | `PropsRenderSlots<S>` |
|
||||
| 共享视图状态的 selector hook 与 mutation callback | 注册项的 `store` | `PropsStore<H>` |
|
||||
| 私有数据、callback 与 observable hook | 注册项的 `inject` factory | `InjectFace<I>` |
|
||||
| 本地化 `t` 函数 | 注册项的 `locale` namespace | `PropsLocale<N>` |
|
||||
| chain 选中的值 | 注册项的 `select` 结果 | 通过 `ComposedProps` 提供的 `matched` |
|
||||
|
||||
当 entry 声明 strict Session child 时,`PropsRenderSlots` 还会提供 `SessionProvider`。它把子树绑定到当前 Session identity,并在 identity 改变时重新挂载 body。
|
||||
|
||||
组件绝不会收到 `ctx`。父组件在某次渲染时已经知道的值通过 `renderSlot` 的 owner 参数进入;共享视图状态使用声明的 store;service 与 model object 留在 `apply` closure 中,只向组件投影 callback 或 observable source。
|
||||
|
||||
## 框架提供的 hooks
|
||||
|
||||
当前组合中的 adapter 会添加以下标准 props。它们按目标 slot 的 scope 提供,与注册组件来自哪个包无关。
|
||||
|
||||
| 可用范围 | Props | Owner |
|
||||
|---|---|---|
|
||||
| 所有 scope | `useSessions`、`useSessionPendingInteraction` | `ui-session` |
|
||||
| 所有 scope | `useWorkspaces` | `ui-workspace` |
|
||||
| `session` | `sessionId`、`useSession`、`useProjection` | `ui-session` |
|
||||
| `session-maybe` | 结果可选的 `sessionId`、`useSession`、`useProjection` | `ui-session` |
|
||||
| `session` | `useConversation`、`useInput`、`inputActions` | `ui-conversation` |
|
||||
| `session-maybe` | 结果可选的 `useConversation`、`useInput`、`inputActions` | `ui-conversation` |
|
||||
| `session` | `useChat` | `ui-chat` |
|
||||
| `session` | `useTrajectory` | `ui-trajectory` |
|
||||
|
||||
Renderer 还会根据声明的 store 创建 `useStore`,并根据声明的 locale namespace 创建 `t`。这些是由注册项推导的 props,不属于全局标准 props。
|
||||
|
||||
框架与领域 adapter owner 可以通过 `ctx.slots.provideRoot()` 或 `ctx.uiSession.provide()` 扩展标准集合,同时提供对应的 `GlobalStandardProps`、`SessionStandardProps` 或 `SessionMaybeStandardProps` 声明合并。普通功能组件不应自行创建 React hook prop,也不应为 entry 私有数据添加全局标准 prop。
|
||||
|
||||
## 开发者提供的 injection
|
||||
|
||||
注册项的 `inject` 选项是通常使用的功能私有注入点。它的 factory 在插件的 `apply` 世界中运行,可以闭包捕获已经注入的 Cordis service,并且只返回组件所需的数据与 callback。对于 `session` slot,它会收到 `sessionId`;对于 `session-maybe`,它收到 `sessionId | undefined`;声明 store 后,它还会收到该 store 绑定后的 actions。
|
||||
|
||||
返回值中保留的 `hooks` 对象接收裸 `getSnapshot`/`subscribe` source。Renderer 把 `hooks: { status }` 转换为组件 prop `useStatus(selector)`,并按 source identity 缓存绑定。组件不会收到 source 本身,也不直接调用 `useSyncExternalStore`。
|
||||
|
||||
当每个 occupant 都需要同一种能力时,slot owner 可以在 child 声明里放置 `inject` face。普通成员会原样交给所有 occupant;其 `hooks` 对象中的函数成员是 hook factory,它会收到 slot 的标准 props 与可选的逐次渲染 `hookContext`,再返回提供给 occupant 的受限 hook。`conversation.chat.node` 正是通过这种机制,为当前渲染的 node 提供 `useTurnData(key)`。
|
||||
|
||||
一次渲染时 owner 已知的值走 owner props;单个 entry 的 callback 与私有 observable 走注册项 `inject`;由 slot owner 控制、所有 occupant 共享的能力走 slot 级 `inject`;需要跨 entry 共享或跨重新挂载保留的可变视图状态走声明的 store。React node 通过 child slot 组合,不通过注入值传递。
|
||||
|
||||
## 当前层级
|
||||
|
||||
下图是当前发布组合的声明树。只有具名 parent entry 已挂载时,其 child 才存在;因此可选功能 entry 可以作为一个生命周期单元让整棵子树出现或消失。
|
||||
|
||||
```text
|
||||
root
|
||||
├─ sidebar
|
||||
│ ├─ sidebar.brand.mark
|
||||
│ ├─ sidebar.brand.name
|
||||
│ ├─ sidebar.footer.action
|
||||
│ ├─ sidebar.workspaces
|
||||
│ │ └─ sidebar.workspaces.directoryFlow
|
||||
│ └─ sidebar.settings
|
||||
│ ├─ settings.trigger
|
||||
│ ├─ settings.header
|
||||
│ ├─ settings.action
|
||||
│ ├─ settings.close
|
||||
│ ├─ settings.onboarding
|
||||
│ └─ settings.section
|
||||
│ ├─ settings.general.item
|
||||
│ └─ settings.plugins.tab
|
||||
│ └─ settings.plugin.item
|
||||
├─ conversation
|
||||
│ ├─ conversation.session
|
||||
│ │ └─ conversation.view
|
||||
│ │ ├─ conversation.chat.node
|
||||
│ │ │ ├─ conversation.chat.assistant-actions
|
||||
│ │ │ ├─ conversation.chat.commandview
|
||||
│ │ │ ├─ conversation.chat.turnTail
|
||||
│ │ │ └─ tool.call.toolview
|
||||
│ │ │ └─ tool.view.cordis
|
||||
│ │ └─ conversation.message.images
|
||||
│ ├─ conversation.session.header
|
||||
│ │ ├─ conversation.session.header.lineage
|
||||
│ │ ├─ conversation.session.header.actions
|
||||
│ │ └─ conversation.session.header.utilities
|
||||
│ ├─ conversation.composer
|
||||
│ │ └─ conversation.approval.detail
|
||||
│ ├─ conversation.composer.bar
|
||||
│ │ ├─ conversation.input.attachments
|
||||
│ │ ├─ conversation.input.plan
|
||||
│ │ └─ conversation.input.model
|
||||
│ ├─ conversation.input.overlay
|
||||
│ ├─ conversation.input.dock
|
||||
│ ├─ conversation.composer.dock
|
||||
│ ├─ conversation.input.left
|
||||
│ ├─ conversation.input.right
|
||||
│ ├─ conversation.hero.brand.mark
|
||||
│ ├─ conversation.hero.workspace
|
||||
│ │ └─ conversation.hero.workspace.directoryFlow
|
||||
│ └─ conversation.hero.agentPreset
|
||||
├─ details
|
||||
│ └─ conversation.details.tool
|
||||
└─ shell.overlay
|
||||
```
|
||||
|
||||
生成的 Client inspect catalog 是每个 key 的完整参考,包含 cardinality、scope、owner props、标准 props、当前 occupant、声明 owner 与替换风险。运行中的动态包可以用 `cordis_inspect what:"client"` 查询实时树与某个精确 key;源码 catalog 由 `pnpm run gen-client-catalog` 根据 `SlotMap` 声明和 `slots.register()` 调用点生成。
|
||||
|
||||
## 扩展规则
|
||||
|
||||
- 另一个功能包只能通过 `import type` 引入声明;绝不导入或转发它的运行时值。
|
||||
- 只在拥有并渲染某个位置的组件中声明新的 child slot。其他包通过 `ctx.slots.inject()` 等待,再通过 `ctx.slots.register()` 贡献内容。
|
||||
- 业务与传输状态留在所属 Cordis service 或 Client model 中。Slot store 只承载共享的视图与交互状态。
|
||||
- 可观测 source 及其 snapshot identity 在值变化前保持稳定;值变化时通过同一个 source 发布。
|
||||
- UI domain 之间只传 JSON 兼容数据和 callback。`hooks` compartment 是裸 observable 的唯一例外;React 内容通过 slot 传递。
|
||||
- 将 `single` 和已有 occupant 的 keyed cell 视为替换点。增量扩展使用 list id 或尚未占用的 key。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 docs/subsystems/web-client.md
|
||||
web-client.md: 40902c273e2daafb5ea8acf2aefb0ce2a418b3f4
|
||||
web-client.zh.md: 79452e73c7ed6ed89df834995291c8f0e44d8258
|
||||
@@ -0,0 +1,95 @@
|
||||
# Web Client architecture
|
||||
|
||||
English | [中文](web-client.zh.md)
|
||||
|
||||
The Web Client is a browser-side Cordis application assembled from independently loaded plugins. Its architecture has four reusable foundations: [Client Modules](client-modules.md) loads the plugin graph, the [API Gateway](../api-gateway.md) provides typed Host communication, [Slots](slots.md) composes React UI, and [Conversation](conversation.md) turns a Session event window into target-owned views. This page connects those systems and defines where Client models and feature packages belong.
|
||||
|
||||
## Layers and ownership
|
||||
|
||||
| Layer | Main owners | Responsibility |
|
||||
|---|---|---|
|
||||
| Host application | business services and `packages/api/*-controller` Host entries | Own authoritative state, persistence, mutation ordering, access policy, and stream production. |
|
||||
| Transport and API assembly | `client/connection`, `api/gateway`, `api/remotes` | Establish a Client generation, expose generated `ctx.remote` methods and streams, forward selected Cordis events, and carry cancellation and results. |
|
||||
| Client models | `api/session-controller/client`, `api/workspace-controller/client` | Maintain React-free mirrors of Host state, resolve stream/unary races, own object identities and subscriptions, and expose narrow command services. |
|
||||
| UI adapters | `client/ui-session`, `client/ui-workspace` | Convert model observables into root or Session-scoped standard Slot sources without taking ownership of business state. |
|
||||
| Conversation data | `client/ui-conversation`, target packages such as `ui-chat` and `ui-trajectory` | Assemble durable Session events into independent target snapshots and own the shared conversation shell and input flow. |
|
||||
| Composition and rendering | `client/ui-slots`, `client/ui-renderer`, `client/ui-layout`, feature UI packages | Declare extension locations, derive component props, bind observables to React hooks, and mount the final tree. |
|
||||
|
||||
The dependency direction is Host state → Remote transport → Client model → UI adapter → Conversation or presentation → Slots → React. User actions travel back through callbacks that close over an injected Client service or generated Remote namespace. A presentation component never receives Cordis `ctx`, a transport object, or another feature plugin's implementation.
|
||||
|
||||
## Browser boot
|
||||
|
||||
The Host writes the composed `WebBootGraph` to `window.__DSH_BOOT__` and installs the browser module-loader facade before parser-preloaded scripts execute. The module system is a lazy CommonJS table: loading a bundle registers its factory, while materializing an entry runs the factory with synchronous `require` over platform modules and declared dynamic dependencies.
|
||||
|
||||
The Web boot kernel creates the module system, prefetches `immediately` entries, mounts the vendored Cordis Loader, and creates every graph entry. Cordis service injection determines activation; module graph order determines only whether synchronous imports can be materialized. After the complete roster reaches a settled state, `ui-renderer` hydrates the framework-free boot DOM and calls the sole context-level `renderSlot('root')` operation. [Client Modules](client-modules.md) owns the graph, bundle route, cache revision, and loader details.
|
||||
|
||||
## Remote communication
|
||||
|
||||
Host business services annotate callable methods with Typert Remote decorators. Host generation emits strict descriptors, runtime codecs, declaration merges, and source maps. The Client-side `api-remotes` assembly selects those generated contributions and mounts concrete methods under `ctx.remote.<namespace>` and Session-scoped `agentCtx.remote.<namespace>`. Feature packages depend on the generated service face, not the Gateway implementation or a Host package's runtime entry.
|
||||
|
||||
The Connection owns request correlation, the `/api` carrier, trust checks, Host description, and connection generations. API Gateway owns Remote dispatch, cancellation, logical streams, and selected Host event forwarding. API Proxy handles only `/api` endpoints that no strict Remote descriptor claims; new controller operations belong on generated Remote methods or explicit Remote streams. The [API Gateway reference](../api-gateway.md) defines generation and invocation, while the [Connection README](../../packages/client/connection/README.md) defines the physical carrier and trust policy.
|
||||
|
||||
The internal `$events` logical stream is the Connection generation source. A generation becomes connected only after the event source emits `ready` and `host.describe` succeeds. Host listeners are therefore attached before any controller begins a baseline read. `ctx.remote.$on()` delivers allowlisted ordinary events to the root Client Context and scoped waterfall events to the resolved Session Context; a waterfall listener returns a result, calls `next()`, or rejects.
|
||||
|
||||
## Client models
|
||||
|
||||
Each API controller package owns a paired Host and Client face. The Host side owns authoritative mutation and stream production. The Client side owns an identity-stable, React-free model over the same generated wire types and exposes observable snapshots plus commands. UI packages consume these Client services and do not reproduce transport state in component stores.
|
||||
|
||||
### Sessions
|
||||
|
||||
[`api/session-controller`](../../packages/api/session-controller/README.md) exposes Host commands for list, search, creation, selection data, prompt, queue, cancellation, pagination, and follow/control streams. Its Client side is organized as `ClientSessions → SessionManager → Session`:
|
||||
|
||||
- `ClientSessions` provides `ctx.sessions`, owns Session scopes and stable `SessionBinding` objects, and projects the selected list state.
|
||||
- `SessionManager` owns the list baseline, live list/control updates, lazy Session instances, queues, projection stores, subagent catalogs, and conflict ordering between pulls and later updates.
|
||||
- Each `Session` owns one contiguous event window, paging, follow, prompt/control state, and the observable snapshot consumed by adapters.
|
||||
|
||||
The durable event path opens `follow()` before reading the first page. A page establishes a contiguous window; live events append by sequence; older pages prepend without replacing unrelated objects. A gap or a new physical generation reads a fresh tail through the opening cursor before publishing a replacement. The transient control stream starts every generation with a complete baseline and then applies queue, job, and projection updates.
|
||||
|
||||
### Workspaces
|
||||
|
||||
[`api/workspace-controller`](../../packages/api/workspace-controller/README.md) keeps Workspace mutation policy and the authoritative follow feed on the Host. `ClientWorkspaceModel` owns the browser rows, order, archived Session ids, command echoes, and stream/unary race resolution. Every stream generation starts with a complete baseline followed by `upsert`, `remove`, `order`, and `archived` increments; reconnect replaces the model from the new baseline. `WorkspaceController` exposes that model as `ctx.workspaces`, while `ui-workspace` contributes `useWorkspaces` and navigation callbacks to the UI.
|
||||
|
||||
This pairing is not a second source of business truth. Host controllers decide durable state and mutation outcomes; Client models maintain the latest usable local projection, preserve object identity where useful to rendering, and encode how delayed responses and replacement baselines merge.
|
||||
|
||||
## Conversation and presentation
|
||||
|
||||
`ui-session` installs the `session` scope adapter and publishes `useSessions`, `useSession`, `sessionId`, and `useProjection`. Domain adapters add further standard sources without putting React hooks on the model objects.
|
||||
|
||||
`ui-conversation` binds once to each `SessionBinding.eventSource`. Its event registry correlates raw durable events into stable business Contexts, and its view registry materializes target snapshots. `ui-chat` and `ui-trajectory` register separate Definitions and builders: they may interpret the same event family, but they do not import or share each other's final display model. The shell selects a registered view and passes its snapshot through standard hooks and Slots. [Conversation](conversation.md) defines Context identity, replay, Location data, target builders, and keyed renderers.
|
||||
|
||||
`ui-slots` provides the typed registry and lifecycle ledger; `ui-renderer` is the only package that binds bare observables through `useSyncExternalStore`, owns React contexts, and renders the root tree. Feature components receive framework hooks, owner props, store actions, and explicit injection through their derived props. [Web Client Slots](slots.md) lists those inputs, extension APIs, and the current Slot hierarchy.
|
||||
|
||||
## Data paths
|
||||
|
||||
| Path | Sequence |
|
||||
|---|---|
|
||||
| durable Session display | Host Session log → Remote `follow` plus `page` → Client `Session` event window → Conversation Contexts → target snapshot (`chat`, `trajectory`, or another registered target) → Slot view → React |
|
||||
| transient Session control | Host control baseline → Remote snapshot stream → `SessionManager` queue/job/projection stores → Session and list snapshots → standard hooks → components |
|
||||
| Workspace state | Host Workspace baseline and increments → `ClientWorkspaceModel` → `ctx.workspaces.list` → `useWorkspaces` → sidebar, hero, and navigation entries |
|
||||
| scoped interaction | Host Cordis waterfall → API Remotes `$events` → `ctx.remote.$on()` on the Session Context → owning UI package → result or `next()` |
|
||||
| user command | component callback → registration inject face or Slot owner → `ctx.sessions`, `ctx.workspaces`, or generated scoped Remote → Host Controller → authoritative update → stream or event projection back to the Client |
|
||||
|
||||
## Reconnection
|
||||
|
||||
Physical and logical recovery are separate. Gateway mux restores the physical WebSocket; each `RemoteStream` reopens its own logical source when the Connection publishes a usable generation. A carrier failure is retryable, while a business error, malformed opening item, or protocol violation is terminal for the owning logical stream.
|
||||
|
||||
Recovery follows the data's semantics:
|
||||
|
||||
- A durable Session journal resumes from the last accepted sequence and repairs the loaded window against a tail page before accepting later events.
|
||||
- Session control and Workspace streams retain the last published value while disconnected, then atomically replace it from a fresh opening baseline.
|
||||
- Ordinary forwarded notifications are not replayed. Stateful domains need a baseline, cursor, or explicit query; scoped waterfalls retain their own request lifetime.
|
||||
|
||||
There is no monolithic Client `Runtime`, `HostFrame`, `events.mux`, `events.host`, or universal `resync()` API. The Connection exposes generation state, Gateway owns logical stream supervision, and each Client model defines replacement or resume semantics appropriate to its data.
|
||||
|
||||
## Package boundaries
|
||||
|
||||
Feature plugin packages may share declarations through `import type`; they do not runtime-import or re-export another feature plugin's values. Cross-package behavior uses injected Cordis services, and cross-package UI uses Slots. Target-specific Conversation Definitions, projection helpers, and final view data stay with their target package even when Chat and Trajectory intentionally implement parallel logic.
|
||||
|
||||
Shared runtime values need a narrow static owner with no feature lifecycle, such as `client/store`, `ui-primitives`, or a browser-safe utility package. Transport and generated API assembly may import runtime contributions because assembling one protocol is their explicit responsibility. A feature package does not add `dsh.client.external` merely to bypass this rule.
|
||||
|
||||
Use the four detailed references according to the extension being added:
|
||||
|
||||
- [Client Modules](client-modules.md) for package discovery, loading, shared module identities, and boot order.
|
||||
- [API Gateway](../api-gateway.md) for Host methods, generated Remote contributions, streams, and forwarded events.
|
||||
- [Web Client Slots](slots.md) for components, hooks, stores, injection, and placement.
|
||||
- [Conversation](conversation.md) for durable event correlation, target snapshots, and Chat or Trajectory view contributions.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Web Client 架构
|
||||
|
||||
[English](web-client.md) | 中文
|
||||
|
||||
Web Client 是由独立加载插件组装而成的浏览器侧 Cordis 应用。它有四个可复用底座:[Client Modules](client-modules.zh.md) 加载插件图,[API Gateway](../api-gateway.zh.md) 提供类型化 Host 通信,[Slots](slots.zh.md) 组合 React UI,[Conversation](conversation.zh.md) 把 Session 事件窗口变成各 target 自有的视图。本文串联这些系统,并规定 Client model 与功能包各自所在的位置。
|
||||
|
||||
## 分层与所有权
|
||||
|
||||
| 层 | 主要 owner | 职责 |
|
||||
|---|---|---|
|
||||
| Host 应用 | 业务 service 与 `packages/api/*-controller` Host entry | 拥有权威状态、持久化、mutation 顺序、访问策略与 stream 生产。 |
|
||||
| 传输与 API assembly | `client/connection`、`api/gateway`、`api/remotes` | 建立 Client generation,公开生成的 `ctx.remote` method 与 stream,转发选定的 Cordis event,并承载取消和结果。 |
|
||||
| Client model | `api/session-controller/client`、`api/workspace-controller/client` | 维护不依赖 React 的 Host 状态镜像,处理 stream/unary 竞态,拥有对象 identity 与订阅,并公开收窄的 command service。 |
|
||||
| UI adapter | `client/ui-session`、`client/ui-workspace` | 把 model observable 转换为 root 或 Session scope 的标准 Slot source,不接管业务状态所有权。 |
|
||||
| Conversation 数据 | `client/ui-conversation`、`ui-chat` 与 `ui-trajectory` 等 target package | 把持久 Session event 组装成相互独立的 target snapshot,并拥有共享的 Conversation shell 与输入流程。 |
|
||||
| 组合与渲染 | `client/ui-slots`、`client/ui-renderer`、`client/ui-layout`、各 UI 功能包 | 声明扩展位置、推导组件 props、把 observable 绑定成 React hook,并挂载最终组件树。 |
|
||||
|
||||
依赖方向是 Host 状态 → Remote 传输 → Client model → UI adapter → Conversation 或 presentation → Slots → React。用户操作通过 callback 反向进入注入的 Client service 或生成的 Remote namespace。Presentation component 绝不接收 Cordis `ctx`、transport object 或其他功能插件的实现。
|
||||
|
||||
## 浏览器启动
|
||||
|
||||
Host 把组合后的 `WebBootGraph` 写入 `window.__DSH_BOOT__`,并在 parser-preloaded script 执行前安装浏览器 module-loader facade。模块系统是一张 lazy CommonJS 表:加载 bundle 只注册 factory;materialize entry 时才以同步 `require` 运行 factory,并解析 platform module 和已声明的动态依赖。
|
||||
|
||||
Web boot kernel 创建模块系统、预取 `immediately` entry、挂载 vendored Cordis Loader,再创建图中的每个 entry。Cordis service injection 决定激活顺序;module graph 顺序只决定同步 import 能否被 materialize。完整 roster 到达 settled 状态后,`ui-renderer` hydrate 不依赖框架的 boot DOM,并调用唯一一次 context 级 `renderSlot('root')`。[Client Modules](client-modules.zh.md)负责 graph、bundle route、cache revision 与 loader 细节。
|
||||
|
||||
## Remote 通信
|
||||
|
||||
Host 业务 service 使用 Typert Remote decorator 标记可调用 method。Host generation 产出严格 descriptor、runtime codec、declaration merge 与 source map。Client 侧 `api-remotes` assembly 选择这些生成贡献,并把具体 method 挂到 `ctx.remote.<namespace>` 与 Session scope 的 `agentCtx.remote.<namespace>`。功能包依赖生成的 service face,而不依赖 Gateway 实现或 Host 包的运行时 entry。
|
||||
|
||||
Connection 拥有 request correlation、`/api` carrier、trust check、Host description 与 connection generation。API Gateway 拥有 Remote dispatch、取消、logical stream 与选定 Host event 的转发。API Proxy 只处理没有被严格 Remote descriptor 认领的 `/api` endpoint;新的 controller 操作应进入生成的 Remote method 或显式 Remote stream。[API Gateway 参考](../api-gateway.zh.md)定义生成与调用,[Connection README](../../packages/client/connection/README.zh.md)定义物理 carrier 与信任策略。
|
||||
|
||||
内部 `$events` logical stream 是 Connection generation source。只有 event source 发出 `ready` 且 `host.describe` 成功后,一代 connection 才会进入 connected。Host listener 因而先于任何 controller baseline read 挂载。`ctx.remote.$on()` 把 allowlist 内的普通 event 交付给 root Client Context,并把 scoped waterfall event 交付给已解析的 Session Context;waterfall listener 可以返回结果、调用 `next()` 或拒绝。
|
||||
|
||||
## Client models
|
||||
|
||||
每个 API controller 包都拥有配对的 Host face 与 Client face。Host 侧拥有权威 mutation 与 stream 生产;Client 侧基于相同的生成 wire type 维护 identity 稳定、与 React 无关的 model,并公开 observable snapshot 与 command。UI 包消费这些 Client service,不在 component store 中复制 transport state。
|
||||
|
||||
### Sessions
|
||||
|
||||
[`api/session-controller`](../../packages/api/session-controller/README.zh.md)公开 Session list、search、creation、selection data、prompt、queue、cancellation、pagination 及 follow/control stream 等 Host command。其 Client 侧按 `ClientSessions → SessionManager → Session` 组织:
|
||||
|
||||
- `ClientSessions` 提供 `ctx.sessions`,拥有 Session scope 与稳定的 `SessionBinding` object,并投影选中的 list state。
|
||||
- `SessionManager` 拥有 list baseline、实时 list/control update、惰性 Session instance、queue、projection store、subagent catalog,以及 pull 与后到 update 之间的冲突顺序。
|
||||
- 每个 `Session` 拥有一段连续 event window、pagination、follow、prompt/control state 与供 adapter 消费的 observable snapshot。
|
||||
|
||||
持久 event 路径会先打开 `follow()`,再读取第一页。page 建立连续窗口;实时 event 按 seq append;旧 page prepend 时不替换无关对象。遇到 gap 或新的物理 generation 时,模型先通过 opening cursor 读取新 tail,再发布 replacement。瞬态 control stream 每代以完整 baseline 开始,随后应用 queue、job 与 projection update。
|
||||
|
||||
### Workspaces
|
||||
|
||||
[`api/workspace-controller`](../../packages/api/workspace-controller/README.zh.md)把 Workspace mutation policy 与权威 follow feed 留在 Host。`ClientWorkspaceModel` 拥有浏览器侧 row、order、archived Session id、command echo,以及 stream/unary 竞态合并。每代 stream 先给出完整 baseline,再给出 `upsert`、`remove`、`order` 和 `archived` increment;重连时以新 baseline 替换 model。`WorkspaceController` 把该 model 作为 `ctx.workspaces` 公开,而 `ui-workspace` 向 UI 提供 `useWorkspaces` 与 navigation callback。
|
||||
|
||||
这种配对不会产生第二份业务真相。Host controller 决定持久状态与 mutation outcome;Client model 维护最新可用的本地 projection,在有利于渲染时保持 object identity,并明确 delayed response 与 replacement baseline 的合并规则。
|
||||
|
||||
## Conversation 与 presentation
|
||||
|
||||
`ui-session` 安装 `session` scope adapter,并提供 `useSessions`、`useSession`、`sessionId` 和 `useProjection`。领域 adapter 可以继续添加标准 source,但不会把 React hook 放进 model object。
|
||||
|
||||
`ui-conversation` 对每个 `SessionBinding.eventSource` 只绑定一次。它的 event registry 把原始持久 event 关联成稳定的业务 Context,view registry 则 materialize target snapshot。`ui-chat` 与 `ui-trajectory` 分别注册自己的 Definition 和 builder:它们可以解释同一 event family,但不会导入或共享彼此的最终 display model。Shell 选择一个已注册 view,再通过标准 hook 与 Slot 交付其 snapshot。[Conversation](conversation.zh.md)定义 Context identity、replay、Location data、target builder 与 keyed renderer。
|
||||
|
||||
`ui-slots` 提供类型化 registry 与 lifecycle ledger;`ui-renderer` 是唯一通过 `useSyncExternalStore` 绑定裸 observable、拥有 React context 并渲染 root tree 的包。功能 component 通过推导出的 props 接收 framework hook、owner prop、store action 与显式 injection。[Web Client Slots](slots.zh.md)列出这些输入、扩展 API 与当前 Slot 层级。
|
||||
|
||||
## 数据通路
|
||||
|
||||
| 路径 | 顺序 |
|
||||
|---|---|
|
||||
| 持久 Session 展示 | Host Session log → Remote `follow` 加 `page` → Client `Session` event window → Conversation Context → target snapshot(`chat`、`trajectory` 或其他已注册 target)→ Slot view → React |
|
||||
| 瞬态 Session control | Host control baseline → Remote snapshot stream → `SessionManager` queue/job/projection store → Session 与 list snapshot → 标准 hook → component |
|
||||
| Workspace 状态 | Host Workspace baseline 与 increment → `ClientWorkspaceModel` → `ctx.workspaces.list` → `useWorkspaces` → sidebar、hero 与 navigation entry |
|
||||
| scoped interaction | Host Cordis waterfall → API Remotes `$events` → Session Context 上的 `ctx.remote.$on()` → 所属 UI 包 → result 或 `next()` |
|
||||
| 用户 command | component callback → 注册项 inject face 或 Slot owner → `ctx.sessions`、`ctx.workspaces` 或生成的 scoped Remote → Host Controller → 权威 update → stream 或 event projection 回到 Client |
|
||||
|
||||
## 重连
|
||||
|
||||
物理恢复与逻辑恢复彼此独立。Gateway mux 恢复物理 WebSocket;Connection 发布可用 generation 后,每个 `RemoteStream` 分别重开自己的 logical source。Carrier failure 可以重试;business error、非法 opening item 或 protocol violation 会令所属 logical stream 终止。
|
||||
|
||||
恢复方式由数据语义决定:
|
||||
|
||||
- 持久 Session journal 从最后接受的 seq 继续,并在接受后续 event 前依据 tail page 修复已加载窗口。
|
||||
- Session control 与 Workspace stream 在断开期间保留最后一次发布的值,再用新的 opening baseline 原子替换。
|
||||
- 普通 forwarded notification 不会 replay。需要可靠恢复的 stateful domain 必须提供 baseline、cursor 或显式 query;scoped waterfall 保留自身的 request lifetime。
|
||||
|
||||
架构中没有统一的 Client `Runtime`、`HostFrame`、`events.mux`、`events.host` 或通用 `resync()` API。Connection 公开 generation state,Gateway 管理 logical stream,Client model 则按自身数据定义 replacement 或 resume 语义。
|
||||
|
||||
## 包边界
|
||||
|
||||
功能插件包可以通过 `import type` 共享声明;不得运行时导入或转发另一个功能插件的值。跨包行为使用注入的 Cordis service,跨包 UI 使用 Slots。特定 target 的 Conversation Definition、projection helper 与最终 view data 留在所属 target 包中,即使 Chat 和 Trajectory 有意实现平行逻辑。
|
||||
|
||||
共享运行时值需要一个职责收窄、没有功能生命周期的静态 owner,例如 `client/store`、`ui-primitives` 或浏览器安全的 util 包。Transport 与生成 API assembly 可以导入运行时 contribution,因为组装同一个 protocol 正是它们的显式职责。功能包不能只为绕过此规则而添加 `dsh.client.external`。
|
||||
|
||||
根据所添加的扩展查阅四篇详细参考:
|
||||
|
||||
- [Client Modules](client-modules.zh.md):package discovery、loading、共享 module identity 与 boot order。
|
||||
- [API Gateway](../api-gateway.zh.md):Host method、生成的 Remote contribution、stream 与 forwarded event。
|
||||
- [Web Client Slots](slots.zh.md):component、hook、store、injection 与 placement。
|
||||
- [Conversation](conversation.zh.md):持久 event correlation、target snapshot,以及 Chat 或 Trajectory view contribution。
|
||||
@@ -1,12 +1,12 @@
|
||||
# AGENTS.md — Web client stack
|
||||
|
||||
Rules for `packages/client/*` (the browser side of the dsh web GUI) plus its build entry `apps/web`. They supplement the repo-wide [conventions](../../AGENTS.md#conventions) and the [package rules](../README.md). Before touching slots, component props, stores, or plugin structure, read the [slot system standard](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) (the definitive composition model) and the [web client architecture note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) (loading chain, object layer, services).
|
||||
Rules for `packages/client/*` (the browser side of the dsh web GUI) plus its build entry `apps/web`. They supplement the repo-wide [conventions](../../AGENTS.md#conventions) and the [package rules](../README.md). Read the current [Web Client architecture](../../docs/subsystems/web-client.md), [Slots reference](../../docs/subsystems/slots.md), and [Conversation reference](../../docs/subsystems/conversation.md) before changing the corresponding layer.
|
||||
|
||||
Packages here are named with the directory prefix: `@deepseek-ai/dsh-client-<name>`.
|
||||
|
||||
## Slot and props discipline
|
||||
|
||||
The [slot system standard](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) owns the full design; these are the rules you must not violate when writing or reviewing client code:
|
||||
The [Slots reference](../../docs/subsystems/slots.md) owns the current design; these are the rules you must not violate when writing or reviewing client code:
|
||||
|
||||
1. **One API**: a plugin composes UI only through `ctx.slots.register({ name, children?, store?, inject? }, Component)`. There is no separate slot-definition call, no whitelist face object, no face-minting helper. The shell alone renders `'root'`.
|
||||
2. **children = declaration + authorization**: the slots your component renders are exactly the keys of your register call's `children` object (spec values: `kind`/`scope`). Rendering a slot you didn't declare, or declaring one someone else declared, fails at load — do not work around it; the conflict is the design speaking. Slot names mirror the composition path: `<domain>.<entry>.<hole>` (e.g. `'tool.call.toolview'`).
|
||||
@@ -33,7 +33,7 @@ The `/client` entrypoint of a UI plugin package is its public browser API, not a
|
||||
|
||||
1. **A UI plugin exports no values beyond what cordis loading needs** — `apply` / `inject` (and `Config` where present), plus store factories consumed type-only by components (`ReturnType<typeof createXXXStore>`). Shared types (owner data, injected values, composed prop aliases) may also be exported. Implementation components, pure helpers, constants, and store handles stay internal. Adding any new value export requires user sign-off, not a matching consumer.
|
||||
2. **Same-package tests import internals directly** — relative `../src/client/xxx.ts` from package tests, or the `./src/*` subpath where a spec lives outside the package. Never widen the public API to make a test compile.
|
||||
3. **Cross-package imports of another plugin's symbols are in principle forbidden.** The sanctioned routes are the slot system (register/renderSlot) and ctx services. If neither fits, stop and escalate — do not add an export to unblock yourself.
|
||||
3. **A feature plugin MUST NOT runtime-import or re-export another feature plugin's values, and MUST NOT declare `dsh.client.external` to obtain them.** Shared declarations use `import type`; behavior crosses packages through injected Cordis services, and UI crosses packages through slots. If neither fits, stop and escalate — do not add an export to unblock yourself. Shared runtime code belongs only in a narrow static owner such as `client/store`, `ui-primitives`, or a browser-safe utility package; transport and generated API assemblies keep their explicit infrastructure edges.
|
||||
|
||||
## ctx discipline (components never see ctx)
|
||||
|
||||
@@ -41,7 +41,7 @@ The `/client` entrypoint of a UI plugin package is its public browser API, not a
|
||||
|
||||
## Layering red lines
|
||||
|
||||
The stack has one-way knowledge, settled in the [web client architecture note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md):
|
||||
The stack has one-way knowledge, documented in the [Web Client architecture](../../docs/subsystems/web-client.md):
|
||||
|
||||
1. **Data object layer** (React-free): `client/connection` owns transport generations, `api/session-controller/client` owns `ClientSessions` → `SessionManager` → `Session`, `api/workspace-controller/client` owns Workspace state, and `client/store` owns the snapshot-store engine (`defineStore`, `createSnapshotStore`, `shallowEqual`). Store products are bare observable sources with no hook members.
|
||||
2. **Render machinery** (`ui-renderer`, dynamic plugin): all ctx-to-React integration — slot renderer/outlets, `SessionProvider`, and the uSES adapter. Every hook is composed here at the binding site from bare sources; production business code carries no ui-renderer value dependency.
|
||||
@@ -75,7 +75,7 @@ Client business code may statically read `process.env.DSH_CLIENT_*`; every refer
|
||||
A dynamic browser half either carries a module privately or requests the shared module-table identity. The client baseline is centralized in [`web/src/platform.ts`](web/src/platform.ts): `PLATFORM_MODULES` names shell-seeded React, Cordis, and static Client libraries; `PRELOADED_CLIENT_EXTERNALS` is reserved for dynamic rows whose factories must arrive before shell boot and is empty when no such row exists.
|
||||
|
||||
1. **Baseline externals are implicit for every dynamic bundle.** Do not repeat React, Cordis, `client/store`, `ui-primitives`, or `ui-slots` in package manifests.
|
||||
2. **`dsh.client.external` adds a package-specific request.** Use it only for a non-baseline value import whose dynamic row must be materialized through the module table. Declare the exact import specifier; only a trailing `/client` aliases the package row.
|
||||
2. **`dsh.client.external` is not a feature-plugin dependency mechanism.** Only infrastructure, transport, or generated assembly may add a package-specific non-baseline value request whose dynamic row must be materialized through the module table. Declare the exact import specifier; only a trailing `/client` aliases the package row.
|
||||
3. **Silence means a private copy.** Ordinary third-party implementation libraries may be bundled independently. A value reached only through `import type` is erased and creates no request.
|
||||
4. **A request has two possible suppliers.** A dynamic package supplies its own row; `PLATFORM_MODULES` supplies an exact static-table key. There is no `dsh.client.provide` alias protocol.
|
||||
5. **Validate both sides.** The dynamic build preset externalizes the baseline and rejects undeclared workspace value imports; [`verify-client-packages`](../../scripts/verify-client-packages.ts) rejects malformed or redundant requests, missing suppliers, and synchronous request cycles.
|
||||
@@ -98,7 +98,7 @@ The seam is `loader.internal = modules`: cordis reaches plugin code through `Ent
|
||||
|
||||
## Conversation Node discipline
|
||||
|
||||
- A Chat business feature registers one `ConversationNodeDefinition` and its keyed `conversation.chat.node` renderer; do not add its event switch or fold to `Session`, `SessionManager`, or a central built-in dispatcher. Follow the [Conversation Node cookbook](../../docs/cookbook/adding-a-conversation-node.md).
|
||||
- A Chat business feature registers one `ConversationNodeDefinition` and its keyed `conversation.chat.node` renderer; do not add its event switch or fold to `Session`, `SessionManager`, or a central built-in dispatcher. Follow the [Conversation reference](../../docs/subsystems/conversation.md).
|
||||
- `match(event)` reads only the current event. Every event in a multi-event Context carries or independently derives the same stable business id; `update` folds one Match into State and remains deterministically replayable by log `seq`.
|
||||
- The append hot path and renderers never scan the full event window, Contexts, or Chat Nodes. Accumulate in State, publish same-Turn/Step facts through `buildLocationData()`, and consume final Node data or constrained Location hooks.
|
||||
|
||||
@@ -144,7 +144,7 @@ Bringing up a new `packages/client/<name>` plugin package (ui-workspace is a com
|
||||
|
||||
## New component checklist
|
||||
|
||||
1. Compose through register: add the slot to `SlotMap`, declare it in its parent entry's `children`, and register your component — see the [slot system standard](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md). No other composition route exists.
|
||||
1. Compose through register: add the slot to `SlotMap`, declare it in its parent entry's `children`, and register your component — see the [Slots reference](../../docs/subsystems/slots.md). No other composition route exists.
|
||||
2. Type the props as the four shares (`PropsRuntime` & `PropsRenderSlots` & `PropsStore` & inject face) — derive, don't hand-write. Shared/surviving state goes in a `createXXXStore()` factory declared at register; component-private state stays local.
|
||||
3. Component tests feed props directly (`createXXXStore().create()` for the store data; plain stubs for framework hooks) and assert behavior without render machinery.
|
||||
4. Tokens only in CSS; product copy follows the localization rule above; English comments.
|
||||
|
||||
@@ -377,7 +377,7 @@ describe('docsPages locale routes', () => {
|
||||
const translated = rootPages.filter(page => page.contentLocale === 'zh-CN')
|
||||
const fallbacks = rootPages.filter(page => page.contentLocale === 'en-US')
|
||||
|
||||
expect(translated).toHaveLength(43)
|
||||
expect(translated).toHaveLength(46)
|
||||
expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true)
|
||||
expect(fallbacks).toEqual([])
|
||||
})
|
||||
|
||||
+5
-9
@@ -304,8 +304,11 @@ const subsystemGroups = [
|
||||
]],
|
||||
['平台与接入', 'Platform and access', [
|
||||
['web-server.md', 'HTTP 服务器', 'HTTP server'],
|
||||
['typert.md', 'Typert', 'Typert'],
|
||||
['web-client.md', 'Web Client 架构', 'Web Client architecture'],
|
||||
['client-modules.md', '客户端模块', 'Client modules'],
|
||||
['slots.md', '客户端 Slots', 'Client slots'],
|
||||
['conversation.md', 'Conversation 组装', 'Conversation assembly'],
|
||||
['typert.md', 'Typert', 'Typert'],
|
||||
['storage.md', '存储', 'Storage'],
|
||||
['workspace.md', '工作区', 'Workspaces'],
|
||||
['settings.md', '用户设置', 'User settings'],
|
||||
@@ -344,6 +347,7 @@ const reference = [
|
||||
['docs/capability-seams.md', 'reference/capability-seams.md', '能力服务', 'Capability services', 2],
|
||||
['docs/agent-lifecycle.md', 'reference/agent-lifecycle.md', 'Agent 生命周期', 'Agent lifecycle', 3],
|
||||
['docs/tool-execution-pipeline.md', 'reference/tool-execution-pipeline.md', 'Tool 执行', 'Tool execution', 4],
|
||||
['docs/api-gateway.md', 'reference/api-gateway.md', 'API Gateway', 'API Gateway', 5],
|
||||
] as const).map(([source, route, rootLabel, enLabel, order]): PairedPage => ({
|
||||
source,
|
||||
route,
|
||||
@@ -404,14 +408,6 @@ const reference = [
|
||||
section: { root: '开发手册', en: 'Cookbook' },
|
||||
order,
|
||||
}))),
|
||||
...pairedPages([{
|
||||
source: 'docs/cookbook/adding-a-conversation-node.md',
|
||||
route: 'reference/cookbook/adding-a-conversation-node.md',
|
||||
label: { root: '新增 Conversation Node', en: 'Adding a Conversation Node' },
|
||||
sidebar: { root: 'zh-reference', en: 'en-reference' },
|
||||
section: { root: '开发手册', en: 'Cookbook' },
|
||||
order: 5,
|
||||
}]),
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user