mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
refactor(client): merge React bindings into UI renderer
This commit is contained in:
@@ -26,7 +26,7 @@ Directories layer as follows:
|
||||
- `packages/host/*`: packages provide host-side capability only (representing the Node.js engineering core built on the existing harness plugin system), and additionally
|
||||
- the unified backend protocol (fetch, HTTP, streaming interfaces…) — definitions and support, see the "Message protocol" sections below
|
||||
- `packages/client/*`: packages provide client-side capability only; every package stays single-sided. Three kinds live here (the axes are owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md)):
|
||||
- **Pure libraries** (`ui-slots`, `web-react`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the first three are seeded into the module table.
|
||||
- **Pure libraries** (`ui-slots`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the two client libraries are seeded into the module table.
|
||||
- **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dsh.client` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
|
||||
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dsh.client` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
|
||||
- `apps/` holds the externally exported applications, assembled from Client / Host mixtures.
|
||||
@@ -39,7 +39,7 @@ apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
|
||||
│ consume
|
||||
▼
|
||||
packages/host/* packages/client/*
|
||||
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
|
||||
apiproxy front layer: protocol pure libs: ui-slots / ui-primitives
|
||||
runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
|
||||
webserver Web HTTP carriage client half = src/client/)
|
||||
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
|
||||
@@ -65,8 +65,8 @@ On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Nod
|
||||
| Front layer | `dsh-host-apiproxy` | TS/zod definitions (api/) + the fetch abstraction (fetch/: handler + client base class) | Keep it simple — every consumer needs it; importable from Node and browser alike; protocol content in the "Message protocol" sections below; clients must not bypass api through ctx |
|
||||
| Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dsh.client packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
|
||||
| Carrier layer | `dsh-host-webserver` | Web HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it |
|
||||
| Client libraries | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | Slot registry core / ctx↔React glue / pure React atoms | Zero cordis runtime dependency in components; seeded into the loader module table by the shell |
|
||||
| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | Browser-side cordis plugin tree (wire consumer, core services, theme, i18n, layout, sidebar, conversation, trajectory) — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); the consumption face goes exclusively through ApiProxy |
|
||||
| Client libraries | `dsh-client-ui-slots` / `dsh-client-ui-primitives` | Slot contracts / pure React atoms | Seeded into the loader module table by the shell |
|
||||
| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-ui-renderer` / feature UI packages | Browser-side Cordis plugin tree: wire consumer, core services, theme, React rendering, and feature composition — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); cross-plugin value cooperation uses services and slots |
|
||||
| Application | `@deepseek-ai/dsh` (apps/cli) + `dsh-web-frontend` (apps/web, the vite application) | Coarse bin dispatch + one assembly module per application (web.ts / headless.ts); the vite app is a thin main over the `dsh-client-web` shell surface | Applications use dynamic imports so they never load each other; workspace knowledge like dist location stays in the app |
|
||||
|
||||
#### Naming rule
|
||||
|
||||
+4
-4
@@ -24,7 +24,7 @@ Status: implemented
|
||||
- `packages/host/*`:包只提供 Host 侧能力(代表了以现在 Harness 实体插件系统为主体的 Node.js 代码核心工程),除此之外,还包含
|
||||
- 统一后端协议(fetch、HTTP、流式接口等)定义和支持,见本篇「消息协议」起各节
|
||||
- `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有):
|
||||
- **纯库**(`ui-slots`、`web-react`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;前三者播种进模块表。
|
||||
- **纯库**(`ui-slots`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;两个客户端库播种进模块表。
|
||||
- **静态到达 entry 包**(`connection`、`runtime`、`ui-theme`、`i18n`、`hmr`):无 `dsh.client` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
|
||||
- **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dsh.client` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
|
||||
- `apps/` 作为对外导出的应用入口,可以由 Client / Host 混合组装。
|
||||
@@ -37,7 +37,7 @@ apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
|
||||
│ consume
|
||||
▼
|
||||
packages/host/* packages/client/*
|
||||
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
|
||||
apiproxy front layer: protocol pure libs: ui-slots / ui-primitives
|
||||
runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
|
||||
webserver Web HTTP carriage client half = src/client/)
|
||||
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
|
||||
@@ -63,8 +63,8 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
|
||||
| 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/:handler + 客户端基类) | 做简单、每个消费方都要;Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api |
|
||||
| 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dsh.client 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
|
||||
| 承载层 | `dsh-host-webserver` | Web HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 |
|
||||
| client 库 | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | slot 注册表核心 / ctx↔React 胶合 / 纯 React 原子组件 | 组件零 cordis 运行时依赖;由壳播种进 loader 模块表 |
|
||||
| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费方、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构笔记 | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy |
|
||||
| client 库 | `dsh-client-ui-slots` / `dsh-client-ui-primitives` | slot 约定 / 纯 React 原子组件 | 由壳播种进 loader 模块表 |
|
||||
| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-ui-renderer` / 功能 UI 包 | 浏览器侧 Cordis 插件树:wire 消费方、核心服务、主题、React 渲染与功能组合——见 Web 客户端架构笔记 | 双入口(node 半边=空 apply;实现在 `src/client/`);跨插件值协作经服务与 slot 完成 |
|
||||
| 应用 | `@deepseek-ai/dsh`(apps/cli)+ `dsh-web-frontend`(apps/web,vite 应用) | bin 粗分发 + 每个应用一个拼装模块(web.ts / headless.ts);vite 应用是 `dsh-client-web` 壳表面之上的薄 main | 各应用使用动态 import,因此不会互相加载;dist 定位等 workspace 知识留在 app |
|
||||
|
||||
#### 命名规则
|
||||
|
||||
@@ -22,7 +22,7 @@ Both ends run cordis. The host is a cordis plugin tree; the browser runs a secon
|
||||
│ ├ GET /plugins/<id>/client.js │ │ │ ui-theme/i18n(fetch bundle,boot 预拉) │
|
||||
│ └ GET / 注入 __DSH_BOOT__ 图 │ │ ├ lazy entries: layout/sidebar/ │
|
||||
│ │ │ │ conversation/trajectory(fetch bundle,按需) │
|
||||
└────────────────────────────────┘ │ ├ render-service(fetch bundle,React 根) │
|
||||
└────────────────────────────────┘ │ ├ ui-renderer(fetch bundle,React 根) │
|
||||
│ └ session scope ×N(观看驱动,惰性建) │
|
||||
│ DOM loading 页 → settled → React UI 一次成型 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
@@ -30,15 +30,15 @@ Both ends run cordis. The host is a cordis plugin tree; the browser runs a secon
|
||||
|
||||
## The client cordis tree and the loading chain
|
||||
|
||||
The loading chain — the two package kinds (plain vs dsh.client plugin), the module-system/plugin-governor split, the two-phase boot over the host-authored entry graph with revisions, and hot reload — is owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md). The load-bearing facts for this document: the browser boots the same vendored `@cordisjs/plugin-loader` as the host with a client module system (`ctx.modules`, `packages/client/modules`) filling its `internal` contract; every unit with product behavior is an entry in the host-authored `__DSH_BOOT__` graph — every production plugin package (infrastructure included) carries the `dsh.client` declaration and arrives as a fetched `./client` tsdown closure bundle, `immediately` rows differing only in boot phase-one prefetch, while plain packages (react family, cordis, the not-yet-promoted libraries) stay shell-bundled, seeded, and invisible to the graph; bundles execute `window.__ModuleLoader__.load({ id, factory })` and their `require` is answered from the lazy CJS module table (seed words + registered factories, materialized and memoized on first require — cross-plugin value imports are a build error, cooperation goes through cordis services); global styles and CSS Modules are inlined in their owning plugin bundle and injected as `<style data-plugin="<id>">` at materialization (CSS Modules also receive hashed names; ownership tags make reload removal possible); hot reload is live in dev graphs — the webserver stat-polls the bundles it serves and broadcasts `rebuilt` SSE frames, and the `client-hmr` plugin swaps one fiber per frame. After `loader.await()` and an all-ACTIVE sweep, the framework-free kernel calls the dynamic render service's `ctx.appShell.mount(container)` once — every entry is created and every fiber reached ACTIVE, with FAILED/PENDING fibers listed loud; there is no partial-availability mode (progressive rendering is deferred work).
|
||||
The loading chain — the two package kinds (plain vs dsh.client plugin), the module-system/plugin-governor split, the two-phase boot over the host-authored entry graph with revisions, and hot reload — is owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md). The load-bearing facts for this document: the browser boots the same vendored `@cordisjs/plugin-loader` as the host with a client module system (`ctx.modules`, `packages/client/modules`) filling its `internal` contract; every unit with product behavior is an entry in the host-authored `__DSH_BOOT__` graph — every production plugin package (infrastructure included) carries the `dsh.client` declaration and arrives as a fetched `./client` tsdown closure bundle, `immediately` rows differing only in boot phase-one prefetch, while plain packages (react family, cordis, the not-yet-promoted libraries) stay shell-bundled, seeded, and invisible to the graph; bundles execute `window.__ModuleLoader__.load({ id, factory })` and their `require` is answered from the lazy CJS module table (seed words + registered factories, materialized and memoized on first require — cross-plugin value imports are a build error, cooperation goes through cordis services); global styles and CSS Modules are inlined in their owning plugin bundle and injected as `<style data-plugin="<id>">` at materialization (CSS Modules also receive hashed names; ownership tags make reload removal possible); hot reload is live in dev graphs — the webserver stat-polls the bundles it serves and broadcasts `rebuilt` SSE frames, and the `client-hmr` plugin swaps one fiber per frame. After `loader.await()` and an all-ACTIVE sweep, the framework-free kernel calls the dynamic UI renderer's `ctx.uiRenderer.mount(container)` once — every entry is created and every fiber reached ACTIVE, with FAILED/PENDING fibers listed loud; there is no partial-availability mode (progressive rendering is deferred work).
|
||||
|
||||
Type universes stay split at the aggregate level — `tsconfig.host.json` is the host program and `tsconfig.client.json` the client program, both referenced by the solution root `tsconfig.json` — because both sides merge cordis `Context` under the same keys (`sessions`, `loader`) with different services; client packages consume the wire vocabulary through pure type subpaths (`@deepseek-ai/dsh-session/types` and kin) so no host augmentation rides into the client program.
|
||||
|
||||
## The slot system: how the page composes
|
||||
|
||||
The slot system has its own note — the [slot system standard](2026-07-22-slot-type-chain-implementation.md) — and this document defers to it entirely. The one-paragraph summary for orientation: render-service renders only `'root'`; a plugin composes UI through a single `register` call that occupies a slot, declares+authorizes its child slots (`children` spec object), declares its store, and injects its business face; component props arrive in four auto-derived shares (`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject), each from its single source of truth. `SlotMap` declaration merging is the type authority and entries carry only the owner share ("whoever injects it, owns its type"); every rendered entry sits in a per-entry error boundary.
|
||||
The slot system has its own note — the [slot system standard](2026-07-22-slot-type-chain-implementation.md) — and this document defers to it entirely. The one-paragraph summary for orientation: ui-renderer renders only `'root'`; a plugin composes UI through a single `register` call that occupies a slot, declares+authorizes its child slots (`children` spec object), declares its store, and injects its business face; component props arrive in four auto-derived shares (`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject), each from its single source of truth. `SlotMap` declaration merging is the type authority and entries carry only the owner share ("whoever injects it, owns its type"); every rendered entry sits in a per-entry error boundary.
|
||||
|
||||
Implementation homes: registry core and the props-share types in `packages/client/ui-slots`, outlet/renderer/uSES bridge in `packages/client/web-react`, and application-level installation and mounting in `packages/client/render-service`.
|
||||
Implementation homes: registry core and the props-share types live in `packages/client/ui-slots`; the outlet renderer, uSES bridge, application-level installation, and root mounting live in `packages/client/ui-renderer`.
|
||||
|
||||
## Services and scope addressing
|
||||
|
||||
@@ -75,18 +75,17 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
|
||||
- **ConversationNodeAssembler** (`runtime/src/client/conversation/`): the Session-owned incremental engine runs independently registered Definitions over raw events. `match(event)` selects `(kind, id)` without Context scans; start/update build Definition state; engine-computed Locations carry Turn/Step closure; backward Context reads record dependencies repaired by later prepends; `buildViewNode(target)` materializes only dirty Contexts. The Chat builder preserves structural order and per-key value identity, `useSession` selectors isolate consumption, and Assistant token publication coalesces to one animation frame. The [Conversation Node decision](2026-08-09-client-conversation-node-assembly.md) owns assembly, while [Tool presentation ownership](2026-08-08-client-tool-presentation-ownership.md) owns recursive Tool rendering.
|
||||
- **ConnectionController** (in `packages/client/connection`): opens the mux/host streams, pumps with for-await, reconnects with exponential backoff (500ms doubling to 10s, jitter, unlimited) behind a generation fence; sinks are injected one-way (the Controller does not know Session). Reconnect = rebuild: `onConnected` → list refresh + per-open-session resync. The object layer faces only `IApiClient`; Web carriage uses HTTP POST for the two client→server quadrants and [one WebSocket per logical stream](2026-08-04-websocket-downlink-carrier.md) for the two server→client quadrants, while the client class family remains the layering note's territory.
|
||||
|
||||
## The React face (`packages/client/web-react` and `packages/client/render-service`)
|
||||
## The React face (`packages/client/ui-renderer`)
|
||||
|
||||
web-react is the static ctx↔React renderer adapter, while the dynamic render-service plugin owns its application-level installation, root mount, and title projection. Business components stay framework-free.
|
||||
The dynamic ui-renderer plugin owns the ctx↔React adapter, application-level installation, root mount, and title projection. Business components receive bound hooks through slot props and do not value-import the renderer.
|
||||
|
||||
- The snapshot store engine **lives in the runtime package** (zustand vanilla with draft-based updates, `flush: 'sync'` by default with opt-in `'raf'` batching, opt-in whole-value localStorage persistence, dev-mode deep freeze — all exported from `runtime`'s `./client` main entry, no subpath): store products are bare observable sources with no hook members. Plugins reach the engine only through `defineStore` declarations per the [slot system standard](2026-07-22-slot-type-chain-implementation.md). web-react composes every hook at the binding site (`bindSnapshotSelector`, per-source cached) from the one data contract React consumes: `ObservableSnapshot<T>` (`getSnapshot`/`subscribe`) — a Session object and a snapshot store both satisfy it. Business plugin packages depend on runtime and ui-slots only; render-service is the sole dynamic consumer that installs web-react's application renderer.
|
||||
- The snapshot store engine **lives in the runtime package** (zustand vanilla with draft-based updates, `flush: 'sync'` by default with opt-in `'raf'` batching, opt-in whole-value localStorage persistence, dev-mode deep freeze — all exported from `runtime`'s `./client` main entry, no subpath): store products are bare observable sources with no hook members. Plugins reach the engine only through `defineStore` declarations per the [slot system standard](2026-07-22-slot-type-chain-implementation.md). ui-renderer composes every hook at the binding site (`bindSnapshotSelector`, per-source cached) from the one data contract React consumes: `ObservableSnapshot<T>` (`getSnapshot`/`subscribe`) — a Session object and a snapshot store both satisfy it.
|
||||
- `bindSnapshotSelector(source)`: binds a source into a typed selector hook over uSES-with-selector. The four uSES contract clauses hold by construction: getSnapshot returns the cached reference; subscribe is a bind-time closure (reference-stable forever); pure CSR passes no server snapshot; equality defaults to `Object.is` with `shallowEqual` opt-in per call.
|
||||
- `useInvoke(fn)`: wraps an async action into a stable trigger plus pending flag; pending rides a per-hook external store read through uSES (no setState on the render path), concurrent invocations are counted, and the invoke reference never changes.
|
||||
- Equality protocol, whole chain: producers use structural sharing; consumers short-circuit with `Object.is` or `shallowEqual`; `React.memo` shallow. Deep comparison is banned everywhere.
|
||||
|
||||
## Directory shape
|
||||
|
||||
Client packages live under `packages/client/*`, with `apps/web` as the thin Vite application over the shell's boot export. Plugin packages keep their browser half under `src/client/`; **every build artifact lands in `lib/`** — the node half as `lib/index.js`/`lib/invariant.js`, the browser bundle as `lib/client.js` (the shared tsdown client preset emits both; there is no `dist/` directory, and `exports["./client"]` points at `./lib/client.js`). `ui-slots`, web-react, runtime, and render-service form the infrastructure direction; feature plugins cooperate through services and slots rather than importing presentation implementations.
|
||||
Client packages live under `packages/client/*`, with `apps/web` as the thin Vite application over the shell's boot export. Plugin packages keep their browser half under `src/client/`; **every build artifact lands in `lib/`** — the node half as `lib/index.js`/`lib/invariant.js`, the browser bundle as `lib/client.js` (the shared tsdown client preset emits both; there is no `dist/` directory, and `exports["./client"]` points at `./lib/client.js`). `ui-slots`, runtime, and ui-renderer form the infrastructure direction; feature plugins cooperate through services and slots rather than importing presentation implementations.
|
||||
|
||||
A multi-domain plugin package additionally splits its client half by future package boundaries — ui-conversation is the exemplar:
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Status: implemented
|
||||
│ ├ GET /plugins/<id>/client.js │ │ │ ui-theme/i18n(fetch bundle,boot 预拉) │
|
||||
│ └ GET / 注入 __DSH_BOOT__ 图 │ │ ├ lazy entries: layout/sidebar/ │
|
||||
│ │ │ │ conversation/trajectory(fetch bundle,按需) │
|
||||
└────────────────────────────────┘ │ ├ render-service(fetch bundle,React 根) │
|
||||
└────────────────────────────────┘ │ ├ ui-renderer(fetch bundle,React 根) │
|
||||
│ └ session scope ×N(观看驱动,惰性建) │
|
||||
│ DOM loading 页 → settled → React UI 一次成型 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
@@ -30,15 +30,15 @@ Status: implemented
|
||||
|
||||
## client cordis 树与装载链
|
||||
|
||||
装载链——两类包(普通包 vs dsh.client 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双阶段 boot、热重载——归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules`,`packages/client/modules`)填上其 `internal` 约定;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包(含基础设施)都携带 `dsh.client` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一阶段预取,而普通包(react 家族、cordis、尚未升格的库)保持打进壳、已播种、对图不可见;bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);全局样式与 CSS Modules 都内联在其持有插件的 bundle 中,物化时注入为 `<style data-plugin="<id>">`(CSS Modules 还会取得哈希类名;归属标签使重载时移除成为可能);热重载已在 dev 图落地——webserver 对自己供给的 bundle 做 stat 轮询并广播 `rebuilt` SSE 帧,`client-hmr` 插件每帧换掉一个 fiber。`loader.await()` 与全 ACTIVE 扫描完成后,不依赖框架的内核会调用动态渲染服务的 `ctx.appShell.mount(container)` 一次——此时每个 entry 已创建、每个 fiber 都到达 ACTIVE,FAILED/PENDING 的 fiber 被大声列出;不存在部分可用模式(渐进渲染为后置工作)。
|
||||
装载链——两类包(普通包 vs dsh.client 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双阶段 boot、热重载——归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules`,`packages/client/modules`)填上其 `internal` 约定;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包(含基础设施)都携带 `dsh.client` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一阶段预取,而普通包(react 家族、cordis、尚未升格的库)保持打进壳、已播种、对图不可见;bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);全局样式与 CSS Modules 都内联在其持有插件的 bundle 中,物化时注入为 `<style data-plugin="<id>">`(CSS Modules 还会取得哈希类名;归属标签使重载时移除成为可能);热重载已在 dev 图落地——webserver 对自己供给的 bundle 做 stat 轮询并广播 `rebuilt` SSE 帧,`client-hmr` 插件每帧换掉一个 fiber。`loader.await()` 与全 ACTIVE 扫描完成后,不依赖框架的内核会调用动态 UI 渲染器的 `ctx.uiRenderer.mount(container)` 一次——此时每个 entry 已创建、每个 fiber 都到达 ACTIVE,FAILED/PENDING 的 fiber 被大声列出;不存在部分可用模式(渐进渲染为后置工作)。
|
||||
|
||||
类型宇宙在聚合层拆分——`tsconfig.host.json` 是 host program、`tsconfig.client.json` 是 client program,二者由 solution 根 `tsconfig.json` 引用,因为两侧都在相同键(`sessions`、`loader`)上对 cordis `Context` 做声明合并且服务不同;client 包经纯类型子路径(`@deepseek-ai/dsh-session/types` 等)消费协议词汇,host 侧的声明合并不会搭车进入 client program。
|
||||
|
||||
## slot 体系:页面怎么拼
|
||||
|
||||
slot 体系有自己的笔记——[slot 体系标准](2026-07-22-slot-type-chain-implementation.md)——本文整体移交给它。此处只留一段定位摘要:render-service 只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——占用 slot、声明并授权子 slot(`children` spec 对象)、声明 store、注入业务面;组件 props 分四份额自动推导到达(`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject),各有唯一真源。`SlotMap` 声明合并仍是类型权威,entry 只携带 owner 份额(「谁注入的,类型归谁」);每个被渲染的注册项都在 per-entry 错误边界之内。
|
||||
slot 体系有自己的笔记——[slot 体系标准](2026-07-22-slot-type-chain-implementation.md)——本文整体移交给它。此处只留一段定位摘要:ui-renderer 只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——占用 slot、声明并授权子 slot(`children` spec 对象)、声明 store、注入业务面;组件 props 分四份额自动推导到达(`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject),各有唯一真源。`SlotMap` 声明合并仍是类型权威,entry 只携带 owner 份额(「谁注入的,类型归谁」);每个被渲染的注册项都在 per-entry 错误边界之内。
|
||||
|
||||
实现的家:注册表核心与 props 份额类型在 `packages/client/ui-slots`,出口组件/渲染器/uSES 桥在 `packages/client/web-react`,应用级安装与挂载在 `packages/client/render-service`。
|
||||
实现的家:注册表核心与 props 份额类型在 `packages/client/ui-slots`;outlet 渲染器、uSES 桥、应用级安装与根挂载在 `packages/client/ui-renderer`。
|
||||
|
||||
## 服务与 scope 寻址
|
||||
|
||||
@@ -75,18 +75,17 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
|
||||
- **ConversationNodeAssembler**(`runtime/src/client/conversation/`):Session 拥有的增量引擎在原始事件上运行各自独立注册的 Definition。`match(event)` 无须扫描 Context 即可选出 `(kind, id)`;start/update 构造 Definition state;引擎计算的 Location 携带 Turn/Step 关闭信息;向前查询 Context 时记录依赖,并由后续 prepend 修复;`buildViewNode(target)` 只物化 dirty Context。Chat builder 保留结构顺序和 per-key value identity,`useSession` selector 负责消费隔离,Assistant token 发布则合并到每个 animation frame 一次。[Conversation Node 决策](2026-08-09-client-conversation-node-assembly.md)拥有组装边界,[Tool 展示所有权](2026-08-08-client-tool-presentation-ownership.md)拥有 Tool 递归渲染。
|
||||
- **ConnectionController**(在 `packages/client/connection`):开 mux/host 双流、for-await 泵入,代际围栏之内指数退避重连(500ms 翻倍至 10s 封顶、抖动、无限重试);sinks 单向注入(Controller 不认识 Session)。重连 = 重建:`onConnected` → 列表刷新 + 各已打开会话 resync。对象层只面向 `IApiClient`;Web 承载以 HTTP POST 载两个 client→server 象限、以[每逻辑流一条 WebSocket](2026-08-04-websocket-downlink-carrier.md)载两个 server→client 象限,客户端类族归分层笔记属地。
|
||||
|
||||
## React 面(`packages/client/web-react` 与 `packages/client/render-service`)
|
||||
## React 面(`packages/client/ui-renderer`)
|
||||
|
||||
web-react 是静态 ctx↔React 渲染适配器,动态 render-service 插件持有其应用级安装、根挂载与标题投影。业务组件保持零框架依赖。
|
||||
动态 ui-renderer 插件持有 ctx↔React 适配器、应用级安装、根挂载与标题投影。业务组件通过 slot props 接收绑定后的钩子,不对渲染器做值 import。
|
||||
|
||||
- 快照 store 引擎**住 runtime 包**(zustand vanilla + 草稿式更新,缺省 `flush: 'sync'`,可选 `'raf'` 合批,可选整值 localStorage 持久化,dev 深冻结——全部从 `runtime` 的 `./client` 主出口导出,无子路径):store 产物是裸的可观察源,不带任何钩子成员。插件只经 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 的 `defineStore` 声明触及引擎。web-react 在绑定处(`bindSnapshotSelector`,按源缓存)从 React 消费的唯一数据约定合成每个钩子:`ObservableSnapshot<T>`(`getSnapshot`/`subscribe`)——Session 对象与快照 store 同构满足它。业务插件包只依赖 runtime 与 ui-slots;render-service 是唯一安装 web-react 应用渲染器的动态消费方。
|
||||
- 快照 store 引擎**住 runtime 包**(zustand vanilla + 草稿式更新,缺省 `flush: 'sync'`,可选 `'raf'` 合批,可选整值 localStorage 持久化,dev 深冻结——全部从 `runtime` 的 `./client` 主出口导出,无子路径):store 产物是裸的可观察源,不带任何钩子成员。插件只经 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 的 `defineStore` 声明触及引擎。ui-renderer 在绑定处(`bindSnapshotSelector`,按源缓存)从 React 消费的唯一数据约定合成每个钩子:`ObservableSnapshot<T>`(`getSnapshot`/`subscribe`)——Session 对象与快照 store 同构满足它。
|
||||
- `bindSnapshotSelector(source)`:把一个源绑定为经 uSES-with-selector 的带类型 selector 钩子。uSES 约定四条按构造成立:getSnapshot 恒返缓存引用;subscribe 是绑定期闭包(引用永稳);纯 CSR 不传 server snapshot;相等性缺省 `Object.is`,按调用可选 `shallowEqual`。
|
||||
- `useInvoke(fn)`:把异步动作包成引用恒定的触发器加 pending 标志;pending 走每个钩子的外部 store 经 uSES 读出(渲染路径零 setState),并发调用计数,invoke 引用永不变。
|
||||
- 相等性协议,全链一致:生产端结构共享;消费方以 `Object.is` 或 `shallowEqual` 短路;`React.memo` 浅比较。深比较全链禁止。
|
||||
|
||||
## 目录形态
|
||||
|
||||
Client 包位于 `packages/client/*`,`apps/web` 是壳 boot 导出之上的薄 Vite 应用。插件包的浏览器半边在 `src/client/` 下;**一切构建产物落 `lib/`**——node 半边为 `lib/index.js`/`lib/invariant.js`,浏览器 bundle 为 `lib/client.js`(共享 tsdown client 预设两者皆出;无 `dist/` 目录,`exports["./client"]` 指向 `./lib/client.js`)。`ui-slots`、web-react、runtime 与 render-service 构成基础设施方向;功能插件通过服务与 slot 协作,不导入展示实现。
|
||||
Client 包位于 `packages/client/*`,`apps/web` 是壳 boot 导出之上的薄 Vite 应用。插件包的浏览器半边在 `src/client/` 下;**一切构建产物落 `lib/`**——node 半边为 `lib/index.js`/`lib/invariant.js`,浏览器 bundle 为 `lib/client.js`(共享 tsdown client 预设两者皆出;无 `dist/` 目录,`exports["./client"]` 指向 `./lib/client.js`)。`ui-slots`、runtime 与 ui-renderer 构成基础设施方向;功能插件通过服务与 slot 协作,不导入展示实现。
|
||||
|
||||
多域插件包的 client 半边还按未来包边界再拆——ui-conversation 即样板:
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ The page is composed at runtime from independently loaded plugins, so the UI nee
|
||||
|
||||
## Decision
|
||||
|
||||
One sentence: **the render-service renders only `'root'`; a plugin composes UI through a single `register` call that simultaneously occupies a slot, declares+authorizes its child slots, declares its store, and injects its business face; components are pure functions whose props arrive in four shares, each auto-derived from its single source of truth.**
|
||||
One sentence: **the ui-renderer renders only `'root'`; a plugin composes UI through a single `register` call that simultaneously occupies a slot, declares+authorizes its child slots, declares its store, and injects its business face; components are pure functions whose props arrive in four shares, each auto-derived from its single source of truth.**
|
||||
|
||||
### 'root' is the only a-priori slot
|
||||
|
||||
`SlotRegistry` (client runtime) declares `'root'` at construction — single/root, `owner: {}` — and its `SlotMap` merge lives in the runtime package. The render-service's entire assembly is `ctx.slots.renderSlot('root', {})`: the only ctx-level render entry; any other key, a missing renderer, or an unregistered root fails loud (no fallback).
|
||||
`SlotRegistry` (client runtime) declares `'root'` at construction — single/root, `owner: {}` — and its `SlotMap` merge lives in the runtime package. The ui-renderer's entire assembly is `ctx.slots.renderSlot('root', {})`: the only ctx-level render entry; any other key, a missing renderer, or an unregistered root fails loud (no fallback).
|
||||
|
||||
### register is the single API; children = declaration + authorization + runtime spec
|
||||
|
||||
@@ -61,7 +61,7 @@ In the type chain, a chain entry's SlotMap shape is `{ kind: 'chain'; scope; own
|
||||
|
||||
### The store seat: framework engine, registrant schema
|
||||
|
||||
The framework owns exactly one subscription machine: the snapshot store engine (zustand vanilla + immer + optional localStorage persistence) lives in the **runtime package** (`./client` main entry — no subpath), producing bare observable sources; web-react binds them into hooks at the outlet (per-source cached uSES binding). What a store *contains* is the registrant's declaration, written as a factory so no module-level handle exists (a module-scoped handle would be a de-facto singleton surviving plugin reloads):
|
||||
The framework owns exactly one subscription machine: the snapshot store engine (zustand vanilla + immer + optional localStorage persistence) lives in the **runtime package** (`./client` main entry — no subpath), producing bare observable sources; ui-renderer binds them into hooks at the outlet (per-source cached uSES binding). What a store *contains* is the registrant's declaration, written as a factory so no module-level handle exists (a module-scoped handle would be a de-facto singleton surviving plugin reloads):
|
||||
|
||||
```ts ignore-check
|
||||
export function createChatStore() {
|
||||
@@ -92,7 +92,7 @@ Hooks are framework-made only: `useSession`, `useSessions`, `useWorkspaces`, `us
|
||||
|
||||
`SessionProvider` is a framework component **delivered as a standard-kit seat**: an entry whose `children` declare a session-scope slot receives it as a prop (type in ui-slots, value injected by the renderer) — components never value-import it. It is self-wired (it reads the runtime's current-session state internally; the assembler passes nothing), render-prop shaped — `children(sessionId)` with an `empty` branch, remounting under `key={sessionId}`. `BindingContext` is machinery-internal; business components see zero React contexts. Inject factories execute inside the outlet on purpose (per-entry error boundaries catch them; a crashing registrant blacks out only its own entry while assembly errors rethrow); the outlet reads tree context as a machinery-only implicit parameter — the "identity from the register closure, situation from the tree position" split.
|
||||
|
||||
Rendering lives behind an installation contract so the runtime stays React-free: `SlotRenderer` (interface in ui-slots, implementation `createSlotRenderer()` in web-react) is installed once at shell boot via `ctx.slots.install(...)`; double install and render-before-install throw. Ownership bookkeeping is a single `Map<key, entry>` in the service — ledger, slots, contributions, render bindings, and store instances all live and die on the one entry axis, which closes the stale-authority window across plugin reloads by construction (a disposed entry's captured `renderSlot` throws a stale-authorization error on entry).
|
||||
Rendering lives behind an installation contract so the runtime stays React-free: `SlotRenderer` (interface in ui-slots, implementation `createSlotRenderer()` in ui-renderer) is installed once at shell boot via `ctx.slots.install(...)`; double install and render-before-install throw. Ownership bookkeeping is a single `Map<key, entry>` in the service — ledger, slots, contributions, render bindings, and store instances all live and die on the one entry axis, which closes the stale-authority window across plugin reloads by construction (a disposed entry's captured `renderSlot` throws a stale-authorization error on entry).
|
||||
|
||||
### Type-chain implementation rulings
|
||||
|
||||
|
||||
+4
-4
@@ -12,11 +12,11 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
一句话:**render-service 只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——这一次调用同时占用 slot、声明并授权子 slot、声明 store、注入业务面;组件是纯函数,props 分四份额到达,每一份额都从各自唯一的真源自动推导。**
|
||||
一句话:**ui-renderer 只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——这一次调用同时占用 slot、声明并授权子 slot、声明 store、注入业务面;组件是纯函数,props 分四份额到达,每一份额都从各自唯一的真源自动推导。**
|
||||
|
||||
### 'root' 是唯一的先验 slot
|
||||
|
||||
`SlotRegistry`(client 运行时)在构造时声明 `'root'`——single/root、`owner: {}`——其 `SlotMap` 合并声明位于运行时包。render-service 的全部装配就是 `ctx.slots.renderSlot('root', {})`:唯一的 ctx 级渲染入口;传任何其他键、渲染器未安装、root 无人注册,一律大声失败(无 fallback)。
|
||||
`SlotRegistry`(client 运行时)在构造时声明 `'root'`——single/root、`owner: {}`——其 `SlotMap` 合并声明位于运行时包。ui-renderer 的全部装配就是 `ctx.slots.renderSlot('root', {})`:唯一的 ctx 级渲染入口;传任何其他键、渲染器未安装、root 无人注册,一律大声失败(无 fallback)。
|
||||
|
||||
### register 是唯一 API;children = 声明+授权+运行时 spec
|
||||
|
||||
@@ -61,7 +61,7 @@ ctx.slots.register({
|
||||
|
||||
### store 席位:引擎归框架,schema 归注册方
|
||||
|
||||
框架只拥有一套订阅机制:快照 store 引擎(zustand vanilla + immer + 可选 localStorage 持久化)住 **运行时包**(`./client` 主出口——无子路径),产出裸的可观察源;web-react 在 outlet 处把它们绑定成钩子(按源缓存的 uSES 绑定)。store 里*装什么*是注册方的声明,且必须写成工厂函数,使模块级句柄根本无从存在(模块级句柄会成为跨插件重载存活的事实单例):
|
||||
框架只拥有一套订阅机制:快照 store 引擎(zustand vanilla + immer + 可选 localStorage 持久化)住 **运行时包**(`./client` 主出口——无子路径),产出裸的可观察源;ui-renderer 在 outlet 处把它们绑定成钩子(按源缓存的 uSES 绑定)。store 里*装什么*是注册方的声明,且必须写成工厂函数,使模块级句柄根本无从存在(模块级句柄会成为跨插件重载存活的事实单例):
|
||||
|
||||
```ts ignore-check
|
||||
export function createChatStore() {
|
||||
@@ -92,7 +92,7 @@ inject 工厂只接收其声明所授权的形参——session slot 获得 `sess
|
||||
|
||||
`SessionProvider` 是框架组件,**以标配席位形式送达**:`children` 里声明了 session scope slot 的 entry 经 prop 收到它(类型住 ui-slots,值由渲染器注入)——组件永不对它做值 import。它框架自接线(内部自读运行时的当前会话状态,装配方零传参),render-prop 形——`children(sessionId)` 外加 `empty` 分支,以 `key={sessionId}` 重挂。`BindingContext` 属机械内部;业务组件可见的 React Context 为零。inject 工厂有意在 outlet 内部执行(per-entry 错误边界接得住它们;崩溃的注册方只黑掉自己那一格,装配错误则重抛);outlet 将树上下文作为仅供框架机制使用的隐式参数读取——即「身份出自 register 闭包、现场出自树位置」的分工。
|
||||
|
||||
渲染位于一份安装约定之后,因此运行时不依赖 React:`SlotRenderer`(接口住 ui-slots,实现 `createSlotRenderer()` 住 web-react)在壳 boot 时经 `ctx.slots.install(...)` 安装一次;双重安装与安装前渲染均 throw。归属记账是服务里的单一 `Map<key, entry>`——账本、slot、贡献、渲染绑定、store 实例全部沿同一条 entry 轴生灭,跨插件重载的陈旧权威窗口由此在构造上关闭(已 dispose 的 entry 所捕获的 `renderSlot`,一进入口即抛陈旧授权(stale-authorization)错误)。
|
||||
渲染位于一份安装约定之后,因此运行时不依赖 React:`SlotRenderer`(接口住 ui-slots,实现 `createSlotRenderer()` 住 ui-renderer)在壳 boot 时经 `ctx.slots.install(...)` 安装一次;双重安装与安装前渲染均 throw。归属记账是服务里的单一 `Map<key, entry>`——账本、slot、贡献、渲染绑定、store 实例全部沿同一条 entry 轴生灭,跨插件重载的陈旧权威窗口由此在构造上关闭(已 dispose 的 entry 所捕获的 `renderSlot`,一进入口即抛陈旧授权(stale-authorization)错误)。
|
||||
|
||||
### 类型链实现裁定
|
||||
|
||||
|
||||
@@ -28,19 +28,19 @@ The first-generation client loader (`createClientLoader`) hand-wrote both layers
|
||||
|
||||
What makes a package a plugin? One rule: **a package is a plugin package once its consumption is cordis dependency injection; until then it is a plain package.** How code reaches the page is not part of the taxonomy — arrival follows from the kind instead of defining it.
|
||||
|
||||
- **Plain packages** are the absolute base the module system itself needs, plus libraries not yet converted to DI: the react family, cordis, `@deepseek-ai/dsh-client-modules` (the module system itself — it can never be a plugin, because modules precede all modules), the web shell kernel, and — for now — ui-slots, web-react, ui-primitives. Plain packages are shell-bundled, seeded into the module table, and invisible to the host graph.
|
||||
- **Plugin packages** are everything else. Each one carries a `dsh.client` manifest declaration (`{ platform, inject, immediately? }`) and one uniform shape: the shared tsdown preset emits `lib/client.js`, and `exports["./client"]` points at that bundle. Each is a governed entry of the host-authored graph, including infrastructure such as connection, runtime, ui-theme, i18n, hmr, and render-service as well as feature packages such as ui-layout, ui-conversation, and ui-attachment.
|
||||
- **Plain packages** are the absolute base the module system itself needs, plus libraries not yet converted to DI: the react family, cordis, `@deepseek-ai/dsh-client-modules` (the module system itself — it can never be a plugin, because modules precede all modules), the web shell kernel, and — for now — ui-slots and ui-primitives. Plain packages are shell-bundled, seeded into the module table, and invisible to the host graph.
|
||||
- **Plugin packages** are everything else. Each one carries a `dsh.client` manifest declaration (`{ platform, inject, immediately? }`) and one uniform shape: the shared tsdown preset emits `lib/client.js`, and `exports["./client"]` points at that bundle. Each is a governed entry of the host-authored graph, including infrastructure such as connection, runtime, ui-theme, i18n, hmr, and ui-renderer as well as feature packages such as ui-layout, ui-conversation, and ui-attachment.
|
||||
|
||||
The manifest owns the package's loading contract: its `inject` dependency edges, plus the optional `immediately` prefetch mark (absent means lazy). The composing app owns only the roster.
|
||||
|
||||
To add a plugin package: declare `dsh.client`, emit the `./client` bundle through the shared preset, add the name to the composing app's roster. Nothing else changes hands.
|
||||
|
||||
When does a plain package become a plugin? The upgrade law, recorded so the migration path stays honest: **a plain package becomes a plugin package when its consumers switch to cordis DI, not before.** Three promotions are queued: ui-slots (the slots machinery now living in runtime — SlotRegistry, the renderer contract, the root slot), web-react (the renderer install moving into its own `apply`), and ui-primitives (once components are served through slots/services). Until then they stay plain, and their symbol exports stay ordinary static imports.
|
||||
When does a plain package become a plugin? The upgrade law, recorded so the migration path stays honest: **a plain package becomes a plugin package when its consumers switch to cordis DI, not before.** Two promotions are queued: ui-slots (the slots machinery now living in runtime — SlotRegistry, the renderer contract, the root slot) and ui-primitives (once components are served through slots/services). Until then they stay plain, and their symbol exports stay ordinary static imports.
|
||||
|
||||
Four edge rules govern imports across the two kinds. None of them depends on any per-package mark:
|
||||
|
||||
- **Plugin ↔ plugin value imports are a build error.** This holds regardless of either side's `immediately` declaration — the rule must not depend on a mark someone can flip. Cooperation goes through cordis inject/services. `import type` is exempt; the type chain is untouched. This rule is why `scopeOf` is a `SessionRuntime` method and why `transportError` lives in `dsh-host-apiproxy`'s wire layer (its `RpcResult` home, inline-safe).
|
||||
- **Plugin → plain package value imports are externals**, judged against the platform list. That list is one constant in the shell (`platform.ts`: react family, cordis, ui-slots, web-react, ui-primitives), imported by both the tsdown preset (for the external judgement) and `seed.ts` (for the table warm-up). One constant, two consumers — the hand-sync drift class stays dead.
|
||||
- **Plugin → plain package value imports are externals**, judged against the platform list. That list is one constant in the shell (`platform.ts`: react family, cordis, ui-slots, ui-primitives), imported by both the tsdown preset (for the external judgement) and `seed.ts` (for the table warm-up). One constant, two consumers — the hand-sync drift class stays dead.
|
||||
- **The purity gate covers every plugin package.** Its three branches: platform imports become externals; INLINE_SAFE wire layers are inlined; any other workspace leak is a build error. The uniform bundle shape is what makes this coverage total — every plugin builds through the same preset, so no package can sit outside the gate.
|
||||
- **The shell is self-sufficient.** Apart from statically adopting the modules package that constructs the module system itself, the kernel value-imports no plugin package. Its loading and failure page uses plain DOM, local state, and local CSS fallbacks, so the fail-loud presentation does not depend on the render service whose failure it may report.
|
||||
|
||||
@@ -72,15 +72,15 @@ What happens between `dsh web` starting and the UI appearing? Three stages: the
|
||||
|
||||
Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted.
|
||||
|
||||
**Phase one — the module face.** The shell builds the module system over the graph, then prefetches every `immediately` row in parallel. Prefetch loads the external script and registers its factory only. A single row's prefetch failure is swallowed here: phase two's import retries the load and owns the loud failure, so one bad row cannot mask the others. `immediately` is a prefetch mark — not a barrier, not an identity. The package declares it, the registry carries it into the row. Infrastructure plugins including connection, runtime, ui-theme, i18n, render-service, and hmr declare it; other UI plugins simply arrive on demand.
|
||||
**Phase one — the module face.** The shell builds the module system over the graph, then prefetches every `immediately` row in parallel. Prefetch loads the external script and registers its factory only. A single row's prefetch failure is swallowed here: phase two's import retries the load and owns the loud failure, so one bad row cannot mask the others. `immediately` is a prefetch mark — not a barrier, not an identity. The package declares it, the registry carries it into the row. Infrastructure plugins including connection, runtime, ui-theme, i18n, ui-renderer, and hmr declare it; other UI plugins simply arrive on demand.
|
||||
|
||||
**Phase two — the plugin face.**
|
||||
|
||||
1. The kernel mounts the vendored Loader and injects the module system as `internal` before any entry exists. Ordering matters: `tree.import`'s bare-import fallback must never run in a browser.
|
||||
2. It creates the statically adopted modules bootstrap entry and one entry per graph row. Render assembly is an ordinary host-graph row provided by `dsh-client-render-service`; the kernel appends no assembly pseudo-entry.
|
||||
2. It creates the statically adopted modules bootstrap entry and one entry per graph row. Render assembly is an ordinary host-graph row provided by `dsh-client-ui-renderer`; the kernel appends no assembly pseudo-entry.
|
||||
3. Creation order carries no semantics; fibers activate through service waiting.
|
||||
4. `settled` = every entry created + `loader.await()` quiescent + an all-ACTIVE sweep. The sweep lists each import-failed, FAILED, or PENDING fiber with its missing services. It exists because cordis inject waits have no timeout — the sweep is the fail-loud floor.
|
||||
5. The framework-free loading page projects real fiber states via `internal/status`. After the sweep, the kernel calls `ctx.appShell.mount(container)` and replaces the page with the real UI in one pass.
|
||||
5. The framework-free loading page projects real fiber states via `internal/status`. After the sweep, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
|
||||
|
||||
### Hot reload: one driver plugin, self-watched bundles
|
||||
|
||||
@@ -110,9 +110,8 @@ The support boundary, stated honestly. Reload is coarse by design: fresh fiber,
|
||||
| vendored `@cordisjs/plugin-loader` | entry governance (same code both sides) | compile-time browserization, kernel-mounted | untouched (vendor policy) |
|
||||
| `dsh-client-modules` | the client module system | lazy CJS table; two-phase boot | plain forever (modules precede modules) |
|
||||
| `dsh-client-web` | module/Loader kernel + framework-free boot page | self-sufficient except for the statically adopted modules bootstrap | keeps shrinking |
|
||||
| `dsh-client-render-service` | React root + slot-renderer assembly | dynamic plugin, declares `immediately` | owns the application mount lifecycle |
|
||||
| `dsh-client-ui-renderer` | ctx↔React adapter + React root + slot-renderer assembly | dynamic plugin, declares `immediately` | owns hook binding and the application mount lifecycle |
|
||||
| `dsh-client-ui-slots` | slot registry core | plain, seeded | promote to plugin; receive runtime's slots machinery |
|
||||
| `dsh-client-web-react` | ctx↔React renderer adapter | plain, seeded; installed by render-service | promote when its consumers switch to DI |
|
||||
| `dsh-client-ui-primitives` | base components | plain, seeded | promote to plugin (components via slots/services) |
|
||||
| `dsh-client-connection` | wire layer | plugin (`dsh.client` + bundle), declares `immediately` | transport swap (Electron IPC carrier) |
|
||||
| `dsh-client-runtime` | session object layer + slots service + store engine | plugin, declares `immediately` | keeps shrinking toward a pure session object layer |
|
||||
|
||||
@@ -28,19 +28,19 @@ host 侧,cordis 插件装载站在 Node 的模块机制之上——require cac
|
||||
|
||||
什么让一个包成为插件?只有一条规则:**一个包的消费方式一旦是 cordis 依赖注入,它就是插件包;在此之前它是普通包。**代码怎么到达页面不属于分类体系——到达方式由包的类别推得,而不是反过来定义类别。
|
||||
|
||||
- **普通包**是模块系统自身所需的绝对基座,加上尚未转成 DI 的库:react 家族、cordis、`@deepseek-ai/dsh-client-modules`(模块系统本身——它永远不可能是插件,因为模块先于一切模块)、web 壳内核,以及——暂时——ui-slots、web-react、ui-primitives。普通包打进壳 bundle、播种进模块表、对 host 图不可见。
|
||||
- **插件包**是其余一切。每个都携带 `dsh.client` manifest(元数据清单)声明(`{ platform, inject, immediately? }`)和同一种统一形态:共享 tsdown 预设产出 `lib/client.js`,`exports["./client"]` 指向该 bundle。每个都是 host 编写的图里受治理的 entry,包括 connection、runtime、ui-theme、i18n、hmr、render-service 等基础设施,也包括 ui-layout、ui-conversation、ui-attachment 等功能包。
|
||||
- **普通包**是模块系统自身所需的绝对基座,加上尚未转成 DI 的库:react 家族、cordis、`@deepseek-ai/dsh-client-modules`(模块系统本身——它永远不可能是插件,因为模块先于一切模块)、web 壳内核,以及——暂时——ui-slots 与 ui-primitives。普通包打进壳 bundle、播种进模块表、对 host 图不可见。
|
||||
- **插件包**是其余一切。每个都携带 `dsh.client` manifest(元数据清单)声明(`{ platform, inject, immediately? }`)和同一种统一形态:共享 tsdown 预设产出 `lib/client.js`,`exports["./client"]` 指向该 bundle。每个都是 host 编写的图里受治理的 entry,包括 connection、runtime、ui-theme、i18n、hmr、ui-renderer 等基础设施,也包括 ui-layout、ui-conversation、ui-attachment 等功能包。
|
||||
|
||||
manifest 拥有包的装载约定:它的 `inject` 依赖边,加可选的 `immediately` 预取标记(缺省即 lazy)。负责组合的 app 只拥有名册。
|
||||
|
||||
新增一个插件包:声明 `dsh.client`,经共享预设产出 `./client` bundle,把包名加进负责组合的 app 的名册。除此之外无需任何交接。
|
||||
|
||||
普通包何时升格为插件?升级法则,记录在案让迁移路径保持诚实:**普通包在其消费方改用 cordis DI 之时升格为插件包,绝不提前。**三项升格在排队:ui-slots(现居 runtime 的 slots 机件——SlotRegistry、渲染器约定、root slot)、web-react(渲染器安装移入自己的 `apply`)、ui-primitives(组件经 slot/服务供给之时)。在那之前它们保持普通包身份,符号导出保持普通的静态 import。
|
||||
普通包何时升格为插件?升级法则,记录在案让迁移路径保持诚实:**普通包在其消费方改用 cordis DI 之时升格为插件包,绝不提前。**两项升格在排队:ui-slots(现居 runtime 的 slots 机件——SlotRegistry、渲染器约定、root slot)与 ui-primitives(组件经 slot/服务供给之时)。在那之前它们保持普通包身份,符号导出保持普通的静态 import。
|
||||
|
||||
四条边规则治理横跨两类包的 import。没有一条依赖任何单包标记:
|
||||
|
||||
- **插件 ↔ 插件的值 import 是构建错误。**与两侧的 `immediately` 声明无关——规则不得依赖一个人人可翻转的标记。协作走 cordis inject/服务。`import type` 豁免;类型链分毫未动。这条规则正是 `scopeOf` 是 `SessionRuntime` 方法、`transportError` 住在 `dsh-host-apiproxy` wire 层(它的 `RpcResult` 老家,内联安全)的原因。
|
||||
- **插件 → 普通包的值 import 外置为 external**,按平台清单判定。清单是壳里的一个常量(`platform.ts`:react 家族、cordis、ui-slots、web-react、ui-primitives),tsdown 预设(external 判定)与 `seed.ts`(模块表预热)都 import 它。一个常量、两个消费方——人肉同步这一漂移缺陷类死透。
|
||||
- **插件 → 普通包的值 import 外置为 external**,按平台清单判定。清单是壳里的一个常量(`platform.ts`:react 家族、cordis、ui-slots、ui-primitives),tsdown 预设(external 判定)与 `seed.ts`(模块表预热)都 import 它。一个常量、两个消费方——人肉同步这一漂移缺陷类死透。
|
||||
- **纯度门禁覆盖每个插件包。**它的三条分支:平台 import 外置为 external;INLINE_SAFE wire 层内联;其余任何 workspace 泄漏即构建错误。正是统一的 bundle 形态让这一覆盖不留死角——每个插件都经同一预设构建,没有包能坐在门禁之外。
|
||||
- **壳自足。**除了静态接纳负责构造模块系统自身的 modules 包,内核不对任何插件包执行值 import。加载与失败页面只使用原生 DOM、本地状态和本地 CSS 回退,因此大声失败的呈现不依赖它可能报告其失败的渲染服务。
|
||||
|
||||
@@ -72,15 +72,15 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
|
||||
|
||||
为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个在仓库中声明了 dsh.client 的包,不代表这次部署要挂载它,扫描发现无从替人做这个决定;node 半只扫描配置树实际挂载了的东西。
|
||||
|
||||
**第一阶段——模块面。**壳在图之上建起模块系统,然后并行预取每个 `immediately` 行。预取即加载外部脚本,只登记工厂。单行预取失败在这里被吞下:第二阶段 import 时会重试加载并拥有那次大声失败,因此一个坏行藏不住其他行。`immediately` 是预取标记——不是屏障,不是身份。包声明它,注册表把它带进图行。connection、runtime、ui-theme、i18n、render-service、hmr 等基础设施插件声明它;其余 UI 插件则按需到达。
|
||||
**第一阶段——模块面。**壳在图之上建起模块系统,然后并行预取每个 `immediately` 行。预取即加载外部脚本,只登记工厂。单行预取失败在这里被吞下:第二阶段 import 时会重试加载并拥有那次大声失败,因此一个坏行藏不住其他行。`immediately` 是预取标记——不是屏障,不是身份。包声明它,注册表把它带进图行。connection、runtime、ui-theme、i18n、ui-renderer、hmr 等基础设施插件声明它;其余 UI 插件则按需到达。
|
||||
|
||||
**第二阶段——插件面。**
|
||||
|
||||
1. 内核挂载 vendored Loader,在任何 entry 存在之前就把模块系统注入为 `internal`。顺序有讲究:`tree.import` 的裸 import 兜底分支在浏览器里绝不能跑到。
|
||||
2. 它创建静态接纳的 modules 启动 entry,并为图中每一行创建 entry。渲染组装是由 `dsh-client-render-service` 提供的普通 host 图行;内核不追加组装伪 entry。
|
||||
2. 它创建静态接纳的 modules 启动 entry,并为图中每一行创建 entry。渲染组装是由 `dsh-client-ui-renderer` 提供的普通 host 图行;内核不追加组装伪 entry。
|
||||
3. 创建顺序不携带任何语义;fiber 经服务等待激活。
|
||||
4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 一次全 ACTIVE 扫描。扫描列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其缺失的服务。它存在的理由:cordis 的 inject 等待没有超时——这次扫描就是大声失败的兜底线。
|
||||
5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。检查完成后,内核调用 `ctx.appShell.mount(container)`,一次切换到真实 UI。
|
||||
5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。检查完成后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
|
||||
|
||||
### 热重载:一个驱动插件,自行监视的 bundle
|
||||
|
||||
@@ -110,9 +110,8 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
|
||||
| vendored `@cordisjs/plugin-loader` | entry 治理(两侧同一份代码) | 编译期浏览器化,内核挂载 | 不动(vendor 政策) |
|
||||
| `dsh-client-modules` | client 模块系统 | lazy CJS 模块表;双阶段 boot | 永为普通包(模块先于模块) |
|
||||
| `dsh-client-web` | 模块/Loader 内核 + 不依赖框架的启动页 | 除静态接纳 modules 启动项外保持自足 | 持续缩小 |
|
||||
| `dsh-client-render-service` | React 根 + slot 渲染器组装 | 动态插件,声明 `immediately` | 持有应用挂载生命周期 |
|
||||
| `dsh-client-ui-renderer` | ctx↔React 适配器 + React 根 + slot 渲染器组装 | 动态插件,声明 `immediately` | 持有钩子绑定与应用挂载生命周期 |
|
||||
| `dsh-client-ui-slots` | slot 注册表核心 | 普通包,已播种 | 升格为插件;接收 runtime 的 slots 机件 |
|
||||
| `dsh-client-web-react` | ctx↔React 渲染适配器 | 普通包,已播种;由 render-service 安装 | 消费方改用 DI 时再升格 |
|
||||
| `dsh-client-ui-primitives` | 基础组件 | 普通包,已播种 | 升格为插件(组件经 slot/服务供给) |
|
||||
| `dsh-client-connection` | wire 层 | 插件(dsh.client + bundle),声明 `immediately` | 传输替换(Electron IPC 载体) |
|
||||
| `dsh-client-runtime` | 会话对象层 + slots 服务 + store 引擎 | 插件,声明 `immediately` | 持续缩向纯会话对象层 |
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ Session 实例与 scope 同生命周期,存活资格 = host listed(一个判
|
||||
|
||||
### 逐会话供数:`sessions.provide` 标准件通道
|
||||
|
||||
会话 slot 组件「自己拿会话数据」的唯一供数路径。插件以静态描述符 `sessions.provide({hooks, props, resolve})` 声明固定键表(重名 key 注册时 throw),`resolve(binding)` 在确定会话下物化值并随 scope 拆;web-react `standardKit` 统一循环把 hooks 格绑成 `use<Name>` 选择器钩子(`observableHook`→uSES,防 tearing)、props 格原样透传。
|
||||
会话 slot 组件「自己拿会话数据」的唯一供数路径。插件以静态描述符 `sessions.provide({hooks, props, resolve})` 声明固定键表(重名 key 注册时 throw),`resolve(binding)` 在确定会话下物化值并随 scope 拆;ui-renderer `standardKit` 统一循环把 hooks 格绑成 `use<Name>` 选择器钩子(`observableHook`→uSES,防 tearing)、props 格原样透传。
|
||||
|
||||
slot scope 是闭集 `root | session-maybe | session`:
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ A new curated `EVENT_WALK_EXEMPTIONS` map names every declared event the project
|
||||
|
||||
The partition judgment moved out of `computeOutputs` into the pure `walkPartitionProblems(input, maps)` so every acceptance path is provable by unit test without running the Typert projection; `computeOutputs` feeds it the rendered model plus the independent scan and keeps aggregating page-splice errors as before.
|
||||
|
||||
The audit that motivated this found the host face already complete: 48 rendered services + 10 walk exemptions covered all 58 host-visible Context keys, all 49 host events rendered, and every type name in every rendered signature is classified by the existing fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` check. The 25 findings (12 events, 13 keys) were all client-face; each now carries a named exemption pointing at its owning README, consistent with the existing `appShell`/`connection` precedent.
|
||||
The audit that motivated this found the host face already complete: 48 rendered services + 10 walk exemptions covered all 58 host-visible Context keys, all 49 host events rendered, and every type name in every rendered signature is classified by the existing fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` check. The 25 findings (12 events, 13 keys) were all client-face; each now carries a named exemption pointing at its owning README, consistent with the existing `uiRenderer`/`connection` precedent.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Status: implemented
|
||||
|
||||
分区判定从 `computeOutputs` 中提取为纯函数 `walkPartitionProblems(input, maps)`,使每条验收路径都能以单元测试证明而无需运行 Typert 投影;`computeOutputs` 向它馈送渲染模型加独立扫描结果,页面拼接错误的聚合方式保持不变。
|
||||
|
||||
促成本决定的审计发现 host face 本已完备:48 个渲染服务 + 10 条 walk 豁免覆盖全部 58 个 host 可见 Context key,49 个 host 事件全部渲染,且每个渲染签名中的每个类型名都已被既有的 fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` 检查分类。25 条发现(12 事件、13 key)全部在 client face;现在每条都带指向其所属 README 的具名豁免,与既有的 `appShell`/`connection` 先例一致。
|
||||
促成本决定的审计发现 host face 本已完备:48 个渲染服务 + 10 条 walk 豁免覆盖全部 58 个 host 可见 Context key,49 个 host 事件全部渲染,且每个渲染签名中的每个类型名都已被既有的 fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` 检查分类。25 条发现(12 事件、13 key)全部在 client face;现在每条都带指向其所属 README 的具名豁免,与既有的 `uiRenderer`/`connection` 先例一致。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
+5
-5
@@ -12,15 +12,15 @@ The loading and failure page has the opposite requirement: it must remain usable
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-client-web` is a framework-free boot kernel. It draws its loading and failure page with DOM operations and local CSS fallbacks, constructs the client module system and Cordis Loader, creates the statically adopted modules bootstrap entry plus every host-graph entry, and waits until every fiber is ACTIVE. It then resolves `ctx.appShell` and hands the existing container to `mount()`.
|
||||
`@deepseek-ai/dsh-client-web` is a framework-free boot kernel. It draws its loading and failure page with DOM operations and local CSS fallbacks, constructs the client module system and Cordis Loader, creates the statically adopted modules bootstrap entry plus every host-graph entry, and waits until every fiber is ACTIVE. It then resolves `ctx.uiRenderer` and hands the existing container to `mount()`.
|
||||
|
||||
`@deepseek-ai/dsh-client-render-service` is an `immediately` dynamic client plugin. After `slots`, `sessions`, and `layout` activate, it installs the slot renderer, provides `ctx.appShell`, creates the React root on `mount()`, projects the selected session title, and performs the sole context-level `renderSlot('root')` call. Its service, renderer installation, and React root all dispose with their owners.
|
||||
`@deepseek-ai/dsh-client-ui-renderer` is an `immediately` dynamic client plugin. It owns the React slot outlets, SessionProvider, and observable-to-uSES binding. After `slots`, `sessions`, and `layout` activate, it installs the slot renderer, provides `ctx.uiRenderer`, creates the React root on `mount()`, projects the selected session title, and performs the sole context-level `renderSlot('root')` call. Its service, renderer installation, and React root all dispose with their owners.
|
||||
|
||||
`ui-conversation` declares `conversation.input.attachments` and `conversation.message.images` and supplies attachment data, callbacks, authorized image loading, and its locale seat. `ui-attachment` waits on those declarations through `ctx.slots.inject()` and registers the draft rail/drop target and historical image gallery/lightbox. The React implementations remain internal package values; cross-plugin composition uses slots. This package integration supersedes the direct-import ruling in the [attachment display note](../feature/2026-08-11-web-attachment-display-alignment.md) without changing that note's visual and interaction decisions.
|
||||
|
||||
ui-theme imports its five global stylesheets from its client entry. The shared client-bundle preset compiles ordinary CSS as well as CSS Modules and injects plugin-owned style tags at bundle materialization, so unloading or reloading ui-theme removes or replaces its global CSS with the same lifecycle as its service. The web kernel retains only mount defaults and the self-contained boot-page palette.
|
||||
|
||||
React, React DOM, Cordis, ui-slots, ui-primitives, and web-react remain static platform modules with one browser identity. Dynamic ownership determines which graph entry creates rendering effects; it does not duplicate these platform runtimes.
|
||||
React, React DOM, Cordis, ui-slots, and ui-primitives remain static platform modules with one browser identity. The dynamic ui-renderer bundle consumes those shared modules and owns the rendering effects.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -34,10 +34,10 @@ Component tests pin the boot page, document title, application tree, attachment
|
||||
|
||||
**Keep ui-theme styles in the shell's base stylesheet.** Rejected because theme CSS would remain active when the theme plugin is absent or failed and would not participate in plugin reload cleanup.
|
||||
|
||||
**Render the failure page with React.** Rejected because a render-service or React-tree failure must not remove the only diagnostic available in the browser.
|
||||
**Render the failure page with React.** Rejected because a ui-renderer or React-tree failure must not remove the only diagnostic available in the browser.
|
||||
|
||||
## Consequences
|
||||
|
||||
The host graph contains every dynamic rendering owner, and HMR replaces attachment presentation, render assembly, and theme CSS through plugin lifecycle. A render-service failure leaves a readable DOM failure page instead of a blank React mount. Omitting ui-attachment deliberately leaves its optional slots empty; the shipped web composition includes it, and a configured entry that fails activation prevents the full-application handoff.
|
||||
The host graph contains every dynamic rendering owner, and HMR replaces attachment presentation, render assembly, and theme CSS through plugin lifecycle. A ui-renderer failure leaves a readable DOM failure page instead of a blank React mount. Omitting ui-attachment deliberately leaves its optional slots empty; the shipped web composition includes it, and a configured entry that fails activation prevents the full-application handoff.
|
||||
|
||||
The application still waits for the complete client roster before its first React frame. The shell still statically bundles the platform module identities, and the boot page maintains a small private light/dark palette because ui-theme CSS is unavailable until that plugin materializes.
|
||||
|
||||
+3
-3
@@ -12,15 +12,15 @@ Status: implemented
|
||||
|
||||
## 决定
|
||||
|
||||
`@deepseek-ai/dsh-client-web` 是不依赖框架的启动内核。它通过 DOM 操作与本地 CSS 回退绘制加载和失败页面,构造客户端模块系统与 Cordis Loader,创建静态接纳的 modules 启动 entry 和宿主图中的每个 entry,并等待所有 fiber 进入 ACTIVE。随后它解析 `ctx.appShell`,把现有容器交给 `mount()`。
|
||||
`@deepseek-ai/dsh-client-web` 是不依赖框架的启动内核。它通过 DOM 操作与本地 CSS 回退绘制加载和失败页面,构造客户端模块系统与 Cordis Loader,创建静态接纳的 modules 启动 entry 和宿主图中的每个 entry,并等待所有 fiber 进入 ACTIVE。随后它解析 `ctx.uiRenderer`,把现有容器交给 `mount()`。
|
||||
|
||||
`@deepseek-ai/dsh-client-render-service` 是带 `immediately` 标记的动态客户端插件。`slots`、`sessions` 与 `layout` 激活后,它安装 slot 渲染器、提供 `ctx.appShell`、在 `mount()` 时创建 React 根、投影当前会话标题,并执行唯一一次上下文级 `renderSlot('root')` 调用。其服务、渲染器安装和 React 根都随各自持有方 dispose。
|
||||
`@deepseek-ai/dsh-client-ui-renderer` 是带 `immediately` 标记的动态客户端插件。它持有 React slot outlet、SessionProvider 与 observable 到 uSES 的绑定。`slots`、`sessions` 与 `layout` 激活后,它安装 slot 渲染器、提供 `ctx.uiRenderer`、在 `mount()` 时创建 React 根、投影当前会话标题,并执行唯一一次上下文级 `renderSlot('root')` 调用。其服务、渲染器安装和 React 根都随各自持有方 dispose。
|
||||
|
||||
`ui-conversation` 声明 `conversation.input.attachments` 与 `conversation.message.images`,并提供附件数据、回调、经会话授权的图片加载及其 locale seat。`ui-attachment` 通过 `ctx.slots.inject()` 等待这些声明,再注册草稿附件栏/拖放目标和历史图片画廊/灯箱。React 实现仍是包内值;跨插件组合通过 slot 完成。这项包集成决策取代[附件展示 Note](../feature/2026-08-11-web-attachment-display-alignment.md)中的直接导入规则,但不改变该 Note 的视觉与交互决策。
|
||||
|
||||
ui-theme 从客户端 entry 导入自己的五份全局样式表。共享客户端 bundle 预设会编译普通 CSS 与 CSS Modules,并在 bundle 物化时注入插件持有的 style 标签,因此卸载或重载 ui-theme 时,其全局 CSS 会随服务的同一生命周期删除或替换。Web 内核只保留挂载默认值与自给自足的启动页配色。
|
||||
|
||||
React、React DOM、Cordis、ui-slots、ui-primitives 与 web-react 仍是保持单一浏览器身份的静态平台模块。动态归属决定哪个图 entry 创建渲染副作用,并不会复制这些平台运行时。
|
||||
React、React DOM、Cordis、ui-slots 与 ui-primitives 仍是保持单一浏览器身份的静态平台模块。动态 ui-renderer bundle 消费这些共享模块并持有渲染副作用。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ A domain's input event set is its own choice — that is the general rule this e
|
||||
|
||||
### React: `useProjection`, the fifth framework hook seat
|
||||
|
||||
The existing four seats cannot host this state (store discipline bans business objects; inject bans hooks; `ConversationSnapshot` is being evacuated). `useProjection` becomes a framework seat, minted in web-react (the one hook constructor), delivered through the same standard-kit channel as `useSession` (`provideInfo` → SessionProvider → props):
|
||||
The existing four seats cannot host this state (store discipline bans business objects; inject bans hooks; `ConversationSnapshot` is being evacuated). `useProjection` becomes a framework seat, minted in ui-renderer (the one hook constructor), delivered through the same standard-kit channel as `useSession` (`provideInfo` → SessionProvider → props):
|
||||
|
||||
```ts ignore-check
|
||||
type UseProjection = {
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ plan mode 完整演示了这套模式——触发路径、运行面、回放面
|
||||
|
||||
### React:`useProjection`,第五个框架钩子席位
|
||||
|
||||
既有四个席位都装不下这份状态(store 纪律禁止业务对象;inject 禁止钩子;`ConversationSnapshot` 正在被清退)。`useProjection` 成为一个框架席位,在 web-react(唯一的钩子铸造点)铸造,经与 `useSession` 相同的标准套件通道(`provideInfo` → SessionProvider → props)送达:
|
||||
既有四个席位都装不下这份状态(store 纪律禁止业务对象;inject 禁止钩子;`ConversationSnapshot` 正在被清退)。`useProjection` 成为一个框架席位,在 ui-renderer(唯一的钩子铸造点)铸造,经与 `useSession` 相同的标准套件通道(`provideInfo` → SessionProvider → props)送达:
|
||||
|
||||
```ts ignore-check
|
||||
type UseProjection = {
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"@deepseek-ai/dsh-client-modules": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-cmdline": "workspace:^",
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Web application entry: thin bootstrap over the shell library. Everything —
|
||||
* module-table seeding, the boot page, and the render-service handoff — lives
|
||||
* module-table seeding, the boot page, and the UI-renderer handoff — lives
|
||||
* in @deepseek-ai/dsh-client-web; this file only finds the mount point.
|
||||
*/
|
||||
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
|
||||
|
||||
@@ -28,7 +28,7 @@ const PLUGINS: readonly (WebBootEntry & { bundlePath: string })[] = [
|
||||
{ id: '@deepseek-ai/dsh-client-ui-theme', bundlePath: 'packages/client/ui-theme/lib/client.js', url: '/plugins/ui-theme.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-api-remotes'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-locale', bundlePath: 'packages/client/locale/lib/client.js', url: '/plugins/locale.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-api-remotes'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-layout', bundlePath: 'packages/client/ui-layout/lib/client.js', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
||||
{ id: '@deepseek-ai/dsh-client-render-service', bundlePath: 'packages/client/render-service/lib/client.js', url: '/plugins/render-service.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-layout'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-renderer', bundlePath: 'packages/client/ui-renderer/lib/client.js', url: '/plugins/ui-renderer.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-layout'], immediately: true },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-sidebar', bundlePath: 'packages/client/ui-sidebar/lib/client.js', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-conversation', bundlePath: 'packages/client/ui-conversation/lib/client.js', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
||||
{ id: '@deepseek-ai/dsh-client-ui-attachment', bundlePath: 'packages/client/ui-attachment/lib/client.js', url: '/plugins/ui-attachment.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
|
||||
@@ -140,7 +140,6 @@ export default defineConfig({
|
||||
// import; the two process probes are mapped by `define` below.
|
||||
{ find: /^node:module$/, replacement: src('./src/node-module-stub.ts') },
|
||||
{ find: /^@deepseek-ai\/dsh-client-web$/, replacement: src('../../packages/client/web/src/boot.ts') },
|
||||
{ find: /^@deepseek-ai\/dsh-client-web-react$/, replacement: src('../../packages/client/web-react/src/index.ts') },
|
||||
{ find: /^@deepseek-ai\/dsh-client-ui-slots$/, replacement: src('../../packages/client/ui-slots/src/index.ts') },
|
||||
{ find: /^@deepseek-ai\/dsh-client-ui-primitives$/, replacement: src('../../packages/client/ui-primitives/src/index.ts') },
|
||||
{ find: /^@deepseek-ai\/dsh-client-modules\/client$/, replacement: src('../../packages/client/modules/src/client/index.ts') },
|
||||
|
||||
@@ -209,16 +209,6 @@
|
||||
"tests/**/*.tsx"
|
||||
]
|
||||
},
|
||||
"packages/client/web-react": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.tsx"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"tests/**/*.tsx"
|
||||
]
|
||||
},
|
||||
"packages/client/ui-layout": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
@@ -698,12 +688,10 @@
|
||||
"packages/client/web": {
|
||||
"project": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
},
|
||||
"packages/client/render-service": {
|
||||
"packages/client/ui-renderer": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.tsx"
|
||||
],
|
||||
@@ -737,7 +725,6 @@
|
||||
"ignoreDependencies": [
|
||||
"@deepseek-ai/dsh-client-ui-primitives",
|
||||
"@deepseek-ai/dsh-client-ui-slots",
|
||||
"@deepseek-ai/dsh-client-web-react",
|
||||
"@types/react",
|
||||
"@types/react-dom",
|
||||
"react",
|
||||
|
||||
@@ -180,8 +180,8 @@
|
||||
- id: ui-layout
|
||||
name: '@deepseek-ai/dsh-client-ui-layout'
|
||||
|
||||
- id: render-service
|
||||
name: '@deepseek-ai/dsh-client-render-service'
|
||||
- id: ui-renderer
|
||||
name: '@deepseek-ai/dsh-client-ui-renderer'
|
||||
|
||||
- id: ui-sidebar
|
||||
name: '@deepseek-ai/dsh-client-ui-sidebar'
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"@deepseek-ai/dsh-client-hmr": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-modules": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-render-service": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-attachment": "workspace:^",
|
||||
|
||||
@@ -44,7 +44,7 @@ The `/client` entrypoint of a UI plugin package is its public browser API, not a
|
||||
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):
|
||||
|
||||
1. **Data object layer** (`runtime`, React-free): `ConnectionController` → `SessionManager` → `Session` own all business state (event windows, streaming accumulation, reconnect machine), and the snapshot-store engine (zustand/immer, `defineStore`, `shallowEqual`) lives here too — store products are bare observable sources with no hook members. Zero React imports — grep-assertable.
|
||||
2. **Render machinery** (`web-react`, shell-only glue): 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; business plugin packages carry no web-react dependency at all.
|
||||
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.
|
||||
3. **Presentation components** (plugin packages' `src/client/`, pure props): consumables, expected to be rewritten wholesale. Business logic must not leak into them; everything arrives through the four props shares.
|
||||
|
||||
Non-negotiables across the layers:
|
||||
|
||||
@@ -7,9 +7,8 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha
|
||||
| Package | Purpose |
|
||||
|---|---|
|
||||
| [`web/`](web/README.md) | Boots the browser shell from the client entry graph. |
|
||||
| [`render-service/`](render-service/README.md) | Mounts the assembled React application after client boot settles. |
|
||||
| [`ui-renderer/`](ui-renderer/README.md) | Binds slot data to React and mounts the assembled application after client boot settles. |
|
||||
| [`modules/`](modules/README.md) | Loads browser-side client modules. |
|
||||
| [`web-react/`](web-react/README.md) | Connects the shell runtime to React rendering. |
|
||||
| [`connection/`](connection/README.md) | Maintains browser-host RPC communication and event delivery. |
|
||||
| [`runtime/`](runtime/README.md) | Provides shared client services for sessions, workspaces, and UI composition. |
|
||||
| [`hmr/`](hmr/README.md) | Refreshes client plugins during development. |
|
||||
|
||||
@@ -7,9 +7,8 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U
|
||||
| 包 | 目的 |
|
||||
|---|---|
|
||||
| [`web/`](web/README.md) | 从客户端条目图启动浏览器 shell。 |
|
||||
| [`render-service/`](render-service/README.md) | 在客户端启动稳定后挂载组装完成的 React 应用。 |
|
||||
| [`ui-renderer/`](ui-renderer/README.md) | 将 slot 数据绑定到 React,并在客户端启动稳定后挂载组装完成的应用。 |
|
||||
| [`modules/`](modules/README.md) | 加载浏览器侧客户端模块。 |
|
||||
| [`web-react/`](web-react/README.md) | 连接 shell 运行时与 React 渲染。 |
|
||||
| [`connection/`](connection/README.md) | 维护浏览器与宿主之间的 RPC 通信和事件传递。 |
|
||||
| [`runtime/`](runtime/README.md) | 为会话、工作区和 UI 组合提供共享客户端服务。 |
|
||||
| [`hmr/`](hmr/README.md) | 在开发期间刷新客户端插件。 |
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { LanguageRow } from '../src/client/LanguageRow.tsx'
|
||||
import type { LanguageRowComponentProps } from '../src/client/LanguageRow.tsx'
|
||||
import { createLanguageRowStore } from '../src/client/settings-store.ts'
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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 packages/client/render-service/README.md
|
||||
README.md: 22c46ad6586821f859d86512eef5aba65f5d0af6
|
||||
README.zh.md: 8815a93cdc847e855e8be7bec20bf873770063a8
|
||||
@@ -1,19 +0,0 @@
|
||||
# @deepseek-ai/dsh-client-render-service
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The browser Cordis plugin that owns React mounting. [`dsh-client-web`](../web/README.md) renders a framework-free boot page and loads the complete client plugin roster; after every entry activates, it calls `ctx.appShell.mount(container)`. This package provides that service, installs the slot renderer, creates the React root, and returns its unmount disposer.
|
||||
|
||||
The plugin activates after `slots`, `sessions`, and `layout`. Its application tree projects the selected session title and performs the sole ctx-level `renderSlot('root')` call. React, React DOM, ui-slots, ui-primitives, and web-react retain one browser identity through the web shell's static module table; this package arrives as a dynamic client bundle.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None. The render service assembles browser UI and contributes no model-visible input.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None; this package neither assembles nor sends provider requests.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **The first application frame waits for every client entry** — the boot kernel hands over the mount point only after the loader roster settles. Per-region readiness remains deferred.
|
||||
@@ -1,19 +0,0 @@
|
||||
# @deepseek-ai/dsh-client-render-service
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
负责 React 挂载的浏览器 Cordis 插件。[`dsh-client-web`](../web/README.md) 渲染不依赖框架的启动页并加载完整的客户端插件名册;所有 entry 激活后,它调用 `ctx.appShell.mount(container)`。本包提供该服务、安装 slot 渲染器、创建 React 根,并返回卸载 disposer。
|
||||
|
||||
插件在 `slots`、`sessions` 和 `layout` 就绪后激活。它的应用树投影当前会话标题,并执行全程序唯一一次 ctx 级 `renderSlot('root')` 调用。React、React DOM、ui-slots、ui-primitives 和 web-react 通过 web 外壳的静态模块表保持同一浏览器身份;本包则以动态客户端 bundle 到达。
|
||||
|
||||
## 模型体验
|
||||
|
||||
无。渲染服务只组装浏览器 UI,不贡献模型可见输入。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无;本包既不组装也不发送提供方请求。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **应用首帧会等待全部客户端 entry**——启动内核只在 loader 名册稳定后交出挂载点。按区域就绪仍属暂缓事项。
|
||||
@@ -1,3 +0,0 @@
|
||||
import { clientBundle } from '../tsdown.client.ts'
|
||||
|
||||
export default clientBundle('@deepseek-ai/dsh-client-render-service', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
@@ -24,7 +24,7 @@ Workspace and Session lists have independent monotone `pending` → `ready` base
|
||||
|
||||
`WorkspaceListState.archivedSessionIds` mirrors the Host's registry-global archive set (a `readonly SessionId[]` in Host order, replaced only when membership changes; consumers needing O(1) lookups build a transient Set). It is full-snapshot state: the `workspace.list` baseline, the `archiveSession` unary echo, and the `host/archived-sessions-changed` frame each install the complete set. `WorkspaceRuntime.archiveSession(sessionId)` archives over the wire; the projection sweep clears the current selection into the New Session view state whenever it lands in the archive set — one rule covering the local echo, another tab's frame, and a reconnect baseline restoring a selection archived while this client was away. A set installed while a `workspace.list` request is in flight also supersedes that stale baseline's set. Grouping surfaces hide members everywhere while the session rows stay in the list store.
|
||||
|
||||
SlotRegistry gives the renderer separate bare observables for `useSessions` and `useWorkspaces`; web-react creates the hooks. Workspace business state does not enter `SessionListState` or an entry store.
|
||||
SlotRegistry gives the renderer separate bare observables for `useSessions` and `useWorkspaces`; ui-renderer creates the hooks. Workspace business state does not enter `SessionListState` or an entry store.
|
||||
|
||||
`indexSubagentDescendants()` derives per-parent total and running descendant counts from the retained list mirror. It follows only uninterrupted `origin: 'subagent'` ancestry, so an ordinary fork starts a separate ownership subtree; cycles stop without throwing, and a missing parent remains a harmless key until its summary arrives.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Workspace 和 Session 列表各自具有单调的 `pending` → `ready` 基线
|
||||
|
||||
`WorkspaceListState.archivedSessionIds` 镜像 Host 的注册表级全局归档集合(一个按 Host 顺序的 `readonly SessionId[]`,仅在成员变化时才替换;需要 O(1) 查询的消费方自建临时 Set)。它是全快照状态:`workspace.list` 基线、`archiveSession` 一元回声和 `host/archived-sessions-changed` 帧各自安装完整集合。`WorkspaceRuntime.archiveSession(sessionId)` 通过 wire 归档;投影层在当前 selection 落入归档集合时统一清空为 New Session 视图状态——一条规则同时覆盖本地回声、其他标签页的帧、以及重连基线恢复出一个离线期间被归档的 selection。在 `workspace.list` 请求进行中安装的集合还会取代该过期基线携带的集合。各分组视图在所有位置隐藏集合成员,而会话行本身仍留在列表 store 中。
|
||||
|
||||
SlotRegistry 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 observable;web-react 创建钩子。Workspace 业务状态不会进入 `SessionListState` 或条目 store。
|
||||
SlotRegistry 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 observable;ui-renderer 创建钩子。Workspace 业务状态不会进入 `SessionListState` 或条目 store。
|
||||
|
||||
`indexSubagentDescendants()` 从保留的列表镜像中派生每个 parent 的后代总数与运行中后代数。它只沿不间断的 `origin: 'subagent'` 祖先链追踪,因此普通 fork 会开启独立的归属子树;遇到环时,追踪会停止但不会抛出异常,缺失的 parent 则会保留为无害的键,直至其摘要到达。
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* shell over it: {@link defineStore} bakes an init/persist/actions literal
|
||||
* into a {@link StoreHandle}, the registration-side store seat of slot
|
||||
* terminals. Lives in the React-free runtime (the data layer owns its
|
||||
* engine; web-react is shell-only React
|
||||
* engine; ui-renderer is shell-only React
|
||||
* glue): engine products are bare observables — subscribe/getSnapshot/
|
||||
* update/set, NO selector hook. Hook synthesis is web-react's (the one
|
||||
* update/set, NO selector hook. Hook synthesis is ui-renderer's (the one
|
||||
* uSES bridge, cached per source at the binding site).
|
||||
*/
|
||||
import { createStore, type StoreApi } from 'zustand/vanilla'
|
||||
@@ -26,7 +26,7 @@ export type {
|
||||
/** Minimal observable snapshot source: Session objects and snapshot stores both satisfy it. */
|
||||
export interface ObservableSnapshot<T> { getSnapshot(): T; subscribe(fn: () => void): () => void }
|
||||
|
||||
/** Writable snapshot store (bare data face; React selector hooks are synthesized in web-react). */
|
||||
/** Writable snapshot store (bare data face; React selector hooks are synthesized in ui-renderer). */
|
||||
export interface SnapshotStore<T> extends ObservableSnapshot<T> {
|
||||
/**
|
||||
* Mutate the state through an immer draft.
|
||||
|
||||
@@ -66,7 +66,7 @@ export type { WorkspaceListState } from './workspaces/service.ts'
|
||||
export type {
|
||||
DirectoryEntry, DirectoryListing, WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
// Runtime owns the snapshot store; web-react only binds it to React.
|
||||
// Runtime owns the snapshot store; ui-renderer only binds it to React.
|
||||
export { createSnapshotStore, defineStore, shallowEqual } from './contract/store.ts'
|
||||
export type {
|
||||
EngineStoreHandle, EngineStoreInstance, ObservableSnapshot, SnapshotStore,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* page's projections block and updated by `session/projection` push frames,
|
||||
* under the single rule **higher seq wins**. No client-side domain folding
|
||||
* exists: a domain ships projection support with zero client code. Per-key
|
||||
* bare observable faces feed `useProjection` (web-react binds them).
|
||||
* bare observable faces feed `useProjection` (ui-renderer binds them).
|
||||
*/
|
||||
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
|
||||
import type { ObservableSnapshot } from '../contract/store.ts'
|
||||
|
||||
@@ -205,7 +205,7 @@ export class SlotRegistry extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the shell's renderer (web-react's createSlotRenderer product).
|
||||
* Install the shell's renderer (ui-renderer's createSlotRenderer product).
|
||||
* Boot-once: a second install throws. Runs through the caller's ctx.effect,
|
||||
* so shell fiber unload uninstalls the renderer.
|
||||
* @param renderer - the outlet machinery implementing SlotRenderer.
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
{
|
||||
"path": "../ui-slots"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../../host/apiproxy"
|
||||
},
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
@@ -71,7 +70,6 @@
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { AgentPresetLabel } from '../src/client/AgentPresetLabel.tsx'
|
||||
import type { AgentPresetLabelProps } from '../src/client/AgentPresetLabel.tsx'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { AgentPresetSection } from '../src/client/AgentPresetSection.tsx'
|
||||
import type { AgentPresetSectionProps } from '../src/client/AgentPresetSection.tsx'
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
{
|
||||
"path": "../ui-slots"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
AssistantMessageNode, ConversationSnapshot, SessionId, ToolResultNode,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { EMPTY_CONVERSATION_VIEWS } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
ModelRetryNode, RunningToolCall, SessionId, SessionListState, ToolCallBlock, ToolResultNode, TurnErrorNode,
|
||||
TurnMaxTokensNode, UserMessageNode, WorkspaceListState,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CONVERSATION_VIEWS, PendingWait,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { EnterBehaviorRow } from '../src/client/settings/EnterBehaviorRow.tsx'
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { ConversationSnapshot, SessionId, SessionListState, WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SessionProviderComponent } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { DetailsSlotProps, DetailsToolOwnerProps, SelectionTarget } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
@@ -23,7 +23,7 @@ import { SessionInputShell } from '../src/client/input/facade.ts'
|
||||
import { InputBar } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// owned draft, and the hero workspace picker (switching = retargetWorkspace).
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { TodoItem } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
{
|
||||
"path": "../ui-primitives"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../runtime"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* store instance (createLayoutStore().create() — the test-sanctioned engine
|
||||
* path), a recording renderSlot stub, and a render-prop SessionProvider stub
|
||||
* (the real one is framework-wired to the renderer host; its own behavior is
|
||||
* web-react's spec territory). Drag sequences (pointer capture + rAF flush),
|
||||
* ui-renderer's spec territory). Drag sequences (pointer capture + rAF flush),
|
||||
* concession response to viewport change, and details staying mounted at
|
||||
* zero width are the preserved behavior assertions. jsdom has no layout
|
||||
* engine, so the frame width comes from a mocked getBoundingClientRect and
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
{
|
||||
"path": "../ui-primitives"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../runtime"
|
||||
},
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-permission-presets": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { SettingsSchemaService } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import { PermissionRow, type PermissionRowProps } from '../src/client/PermissionRow.tsx'
|
||||
|
||||
@@ -29,9 +29,6 @@
|
||||
{
|
||||
"path": "../ui-slots"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../../interaction/permission-presets"
|
||||
},
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { PlanProjection } from '@deepseek-ai/dsh-plan-mode/client'
|
||||
import { PlanChip, type PlanChipProps } from '../src/client/PlanModeControl.tsx'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
|
||||
@@ -29,9 +29,6 @@
|
||||
{
|
||||
"path": "../ui-slots"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../../plan/plan-mode"
|
||||
},
|
||||
|
||||
+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 packages/client/web-react/README.md
|
||||
README.md: 4f52e08ba53b51ce2c095f3fefd86a0dae3dfa9c
|
||||
README.zh.md: dff1fbe09e80fa2fcc058d1d8cf54de8c8db231f
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-renderer/README.md
|
||||
README.md: 0f0a17d6167228b229b623f6b461c0f413e08a61
|
||||
README.zh.md: 85714b4b69d92d3ea8f6a5c4b99f3faba9ce3f15
|
||||
@@ -0,0 +1,20 @@
|
||||
# @deepseek-ai/dsh-client-ui-renderer
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The browser Cordis plugin that owns the React rendering layer. [`dsh-client-web`](../web/README.md) renders a framework-free boot page and loads the complete client plugin roster; after every entry activates, it calls `ctx.uiRenderer.mount(container)`. This package provides that service, installs the slot renderer, creates the React root, and returns its unmount disposer.
|
||||
|
||||
The client entry also owns the React implementation of slot outlets, session providers, and observable-to-uSES binding. Business plugins pass bare observable sources through typed slot `hooks`; the renderer binds them at the outlet. The plugin activates after `slots`, `sessions`, and `layout`, projects the selected session title, and performs the sole context-level `renderSlot('root')` call. React, React DOM, Cordis, ui-slots, and ui-primitives retain one browser identity through the web shell's static module table; this package arrives as a dynamic client bundle.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None. The UI renderer assembles browser UI and contributes no model-visible input.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
None; this package neither assembles nor sends provider requests.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **The first application frame waits for every client entry** — the boot kernel hands over the mount point only after the loader roster settles. Per-region readiness remains deferred.
|
||||
- **Slot rendering has no Suspense integration or per-entry lazy loading** — the complete plugin roster settles before the renderer mounts the root.
|
||||
@@ -0,0 +1,20 @@
|
||||
# @deepseek-ai/dsh-client-ui-renderer
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
负责 React 渲染层的浏览器 Cordis 插件。[`dsh-client-web`](../web/README.md) 渲染不依赖框架的启动页并加载完整的客户端插件名册;所有 entry 激活后,它调用 `ctx.uiRenderer.mount(container)`。本包提供该服务、安装 slot 渲染器、创建 React 根,并返回卸载 disposer。
|
||||
|
||||
client entry 还持有 slot outlet、会话 provider 以及 observable 到 uSES 绑定的 React 实现。业务插件通过带类型的 slot `hooks` 传递裸 observable source;渲染器在 outlet 处完成绑定。插件在 `slots`、`sessions` 和 `layout` 就绪后激活,投影当前会话标题,并执行全程序唯一一次上下文级 `renderSlot('root')` 调用。React、React DOM、Cordis、ui-slots 和 ui-primitives 通过 web 外壳的静态模块表保持同一浏览器身份;本包则以动态客户端 bundle 到达。
|
||||
|
||||
## 模型体验
|
||||
|
||||
无。UI 渲染器只组装浏览器 UI,不贡献模型可见输入。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无;本包既不组装也不发送提供方请求。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **应用首帧会等待全部客户端 entry**——启动内核只在 loader 名册稳定后交出挂载点。按区域就绪仍属暂缓事项。
|
||||
- **slot 渲染没有 Suspense 集成或逐 entry 惰性加载**——完整插件名册稳定后,渲染器才挂载根节点。
|
||||
+8
-9
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-render-service",
|
||||
"description": "Browser render service: installs the slot renderer, provides ctx.appShell, and mounts the assembled React application",
|
||||
"version": "0.1.0-rc.6",
|
||||
"name": "@deepseek-ai/dsh-client-ui-renderer",
|
||||
"description": "Browser UI renderer: React slot bindings, ctx.uiRenderer, and the assembled application root",
|
||||
"version": "0.1.0-rc.7",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/render-service"
|
||||
"directory": "packages/client/ui-renderer"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
@@ -32,8 +32,7 @@
|
||||
"dsh": {
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-client-ui-layout"
|
||||
"@deepseek-ai/dsh-client-runtime"
|
||||
],
|
||||
"platform": "web",
|
||||
"immediately": true
|
||||
@@ -44,18 +43,18 @@
|
||||
"watch": "tsdown --watch"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@types/react-dom": "~18.3.0",
|
||||
+4
-4
@@ -4,11 +4,11 @@
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from './bind.ts'
|
||||
import { DocumentTitle } from './DocumentTitle.tsx'
|
||||
import type {} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** Inputs available after the render service's inject set activates. */
|
||||
/** Inputs available after the UI renderer's inject set activates. */
|
||||
export interface AssemblyDeps {
|
||||
/** Client context carrying the slots and sessions services. */
|
||||
ctx: Context
|
||||
@@ -16,13 +16,13 @@ export interface AssemblyDeps {
|
||||
|
||||
/**
|
||||
* Build the assembled application factory.
|
||||
* @param deps - Active render-service dependencies.
|
||||
* @param deps - Active UI-renderer dependencies.
|
||||
* @returns Factory producing the application React tree.
|
||||
*/
|
||||
export function buildRenderApp(deps: AssemblyDeps): () => ReactNode {
|
||||
const { ctx } = deps
|
||||
const sessions = ctx.get('sessions')
|
||||
if (sessions === undefined) throw new Error('render service: sessions service unavailable')
|
||||
if (sessions === undefined) throw new Error('ui renderer: sessions service unavailable')
|
||||
const useSessions = bindSnapshotSelector(sessions.list)
|
||||
const SessionDocumentTitle = (): ReactNode => {
|
||||
const title = useSessions((state) => {
|
||||
+21
-7
@@ -1,15 +1,29 @@
|
||||
/**
|
||||
* Browser render service. It installs the slot renderer after its Cordis
|
||||
* Browser UI renderer. It installs the slot renderer after its Cordis
|
||||
* dependencies activate and exposes the mount operation used by the web boot
|
||||
* kernel after the complete client roster settles.
|
||||
*/
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { createSlotRenderer } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { createSlotRenderer } from './scoped-slots.tsx'
|
||||
import { buildRenderApp } from './app.tsx'
|
||||
|
||||
export { bindSnapshotSelector } from './bind.ts'
|
||||
|
||||
/** Selector hook over a session's conversation snapshot. */
|
||||
export type UseSession<Snap extends object = object> = SnapshotSelectorHook<Snap>
|
||||
|
||||
export type {
|
||||
ChainRenderOpts, HostObservable, RenderOpts, SessionProvideInfo, SnapshotSelectorHook,
|
||||
SlotRenderer, SlotRendererHost, StoreInstanceLike,
|
||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
export { SlotOwnershipError, StaleAuthorizationError } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
export { createSlotRenderer } from './scoped-slots.tsx'
|
||||
export { SessionProvider, SlotAssemblyError, type SessionProviderProps } from './session-provider.tsx'
|
||||
|
||||
/** Mount operation exposed to the framework-free boot kernel. */
|
||||
export interface AppShellService {
|
||||
export interface UiRendererService {
|
||||
/**
|
||||
* Mount the assembled application into the supplied element.
|
||||
* @param container - Application mount point.
|
||||
@@ -20,13 +34,13 @@ export interface AppShellService {
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
/** Mount face provided after the render service activates. */
|
||||
appShell: AppShellService
|
||||
/** Mount face provided after the UI renderer activates. */
|
||||
uiRenderer: UiRendererService
|
||||
}
|
||||
}
|
||||
|
||||
/** Services required before application assembly. */
|
||||
export const inject = ['slots', 'sessions', 'layout']
|
||||
export const inject = ['slots', 'sessions']
|
||||
|
||||
/**
|
||||
* Install the slot renderer and provide the application mount face.
|
||||
@@ -34,7 +48,7 @@ export const inject = ['slots', 'sessions', 'layout']
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.slots.install(createSlotRenderer())
|
||||
ctx.reflect.provide('appShell', {
|
||||
ctx.reflect.provide('uiRenderer', {
|
||||
mount: (container: HTMLElement): (() => void) => {
|
||||
const root = createRoot(container)
|
||||
root.render(buildRenderApp({ ctx })())
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/** Host loader entry for the browser-only render service. */
|
||||
/** Host loader entry for the browser-only UI renderer. */
|
||||
|
||||
/** Provides no host-side behavior. */
|
||||
export function apply(): void {}
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-client-render-service`.
|
||||
* @module @deepseek-ai/dsh-client-render-service/invariant
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-renderer`.
|
||||
* @module @deepseek-ai/dsh-client-ui-renderer/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-client-render-service'
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-renderer'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'client-render-service-invariant'
|
||||
export const name = 'client-ui-renderer-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { act, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
import type { HostObservable as ObservableSnapshot, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
|
||||
// Keep equality local: this suite asserts the eq parameter contract without
|
||||
// adding a reverse dependency from web-react to runtime.
|
||||
// adding a reverse dependency from the UI renderer to runtime.
|
||||
const shallowEqual = (a: Record<string, unknown>, b: Record<string, unknown>): boolean =>
|
||||
Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(k => Object.is(a[k], b[k]))
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { act, render } from '@testing-library/react'
|
||||
import { SlotCore, type PropsRenderSlots, type SlotRendererHost } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { createSlotRenderer, StaleAuthorizationError } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSlotRenderer, StaleAuthorizationError } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface SlotMap {
|
||||
+2
-2
@@ -17,7 +17,7 @@ import {
|
||||
createSlotRenderer, SessionProvider, SlotOwnershipError, StaleAuthorizationError,
|
||||
type RenderOpts, type SessionProvideInfo,
|
||||
type SlotRendererHost, type StoreInstanceLike,
|
||||
} from '@deepseek-ai/dsh-client-web-react'
|
||||
} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
|
||||
type AnyProps = Record<string, unknown>
|
||||
type RenderSlotFn = (key: string, owner: object, opts?: RenderOpts) => ReactNode
|
||||
@@ -32,7 +32,7 @@ const entryOf = (partial: Omit<StoredEntry, 'options'> & { options?: StoredEntry
|
||||
* create(scopeKey?) + instance with clearPersisted): the machinery consumes
|
||||
* only the StoreInstanceLike face (bare snapshot source + baked actions),
|
||||
* but entry.store is typed to the full contract — the real defineStore lives
|
||||
* in runtime, which web-react tests must not import (dependency direction).
|
||||
* in runtime, which UI-renderer tests must not import (dependency direction).
|
||||
*/
|
||||
function miniStore<T extends object>(
|
||||
init: () => T,
|
||||
+1
-1
@@ -13,7 +13,7 @@ import type { SessionMaybeProvideInfo, StoredEntry } from '@deepseek-ai/dsh-clie
|
||||
import {
|
||||
createSlotRenderer, SessionProvider,
|
||||
type SessionProvideInfo, type SlotRendererHost,
|
||||
} from '@deepseek-ai/dsh-client-web-react'
|
||||
} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
|
||||
function observable<T>(initial: T) {
|
||||
let value = initial
|
||||
+1
-1
@@ -10,7 +10,7 @@ import type { SlotEntryDef, SlotSpec, StoredEntry } from '@deepseek-ai/dsh-clien
|
||||
import {
|
||||
createSlotRenderer, StaleAuthorizationError,
|
||||
type RenderOpts, type SlotRendererHost,
|
||||
} from '@deepseek-ai/dsh-client-web-react'
|
||||
} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
|
||||
type RenderSlotFn = (key: string, owner: object, opts?: RenderOpts) => ReactNode
|
||||
type DeclaredSpec = SlotSpec<SlotEntryDef>
|
||||
+9
-10
@@ -1,11 +1,11 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { act, cleanup } from '@testing-library/react'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { TestSessions, TestWorkspaces } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { Stabilizer } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import * as RenderService from '../src/client/index.ts'
|
||||
import * as UiRenderer from '../src/client/index.ts'
|
||||
|
||||
const mounted: (() => void)[] = []
|
||||
|
||||
@@ -23,8 +23,7 @@ async function bench() {
|
||||
const slots = ctx.get('slots') as SlotRegistry
|
||||
ctx.provide('sessions', new TestSessions(stabilize, ctx))
|
||||
ctx.provide('workspaces', new TestWorkspaces(stabilize))
|
||||
ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
||||
const fiber = ctx.plugin({ inject: [...RenderService.inject], apply: RenderService.apply })
|
||||
const fiber = ctx.plugin({ inject: [...UiRenderer.inject], apply: UiRenderer.apply })
|
||||
await fiber.await()
|
||||
return { ctx, slots, fiber }
|
||||
}
|
||||
@@ -35,14 +34,14 @@ function container(): HTMLElement {
|
||||
return el
|
||||
}
|
||||
|
||||
describe('render service plugin', () => {
|
||||
describe('UI renderer plugin', () => {
|
||||
it('installs the renderer and mounts the assembled application', async () => {
|
||||
const { ctx, slots } = await bench()
|
||||
slots.register({ name: 'root' }, () => <div data-testid="root-probe" />)
|
||||
const shell = ctx.get('appShell')
|
||||
expect(shell).toBeDefined()
|
||||
const renderer = ctx.get('uiRenderer')
|
||||
expect(renderer).toBeDefined()
|
||||
const el = container()
|
||||
act(() => { mounted.push(shell!.mount(el)) })
|
||||
act(() => { mounted.push(renderer!.mount(el)) })
|
||||
expect(el.querySelector('[data-testid="root-probe"]')).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -51,7 +50,7 @@ describe('render service plugin', () => {
|
||||
slots.register({ name: 'root' }, () => <div data-testid="root-probe" />)
|
||||
const el = container()
|
||||
let unmount: () => void = () => {}
|
||||
act(() => { unmount = ctx.get('appShell')!.mount(el) })
|
||||
act(() => { unmount = ctx.get('uiRenderer')!.mount(el) })
|
||||
act(() => { unmount() })
|
||||
expect(el.querySelector('[data-testid="root-probe"]')).toBeNull()
|
||||
})
|
||||
@@ -59,7 +58,7 @@ describe('render service plugin', () => {
|
||||
it('retracts the service and renderer with its fiber', async () => {
|
||||
const { ctx, slots, fiber } = await bench()
|
||||
await stabilize(() => fiber.dispose())
|
||||
expect(ctx.get('appShell')).toBeUndefined()
|
||||
expect(ctx.get('uiRenderer')).toBeUndefined()
|
||||
expect(() => slots.renderSlot('root', {})).toThrow('not installed')
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { act, render } from '@testing-library/react'
|
||||
import type { SessionMaybeProvideInfo, StoredEntry } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { createSlotRenderer, type SlotRendererHost } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSlotRenderer, type SlotRendererHost } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
|
||||
function observable<T>(initial: T) {
|
||||
let value = initial
|
||||
-3
@@ -14,9 +14,6 @@
|
||||
{
|
||||
"path": "../ui-slots"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../runtime"
|
||||
},
|
||||
@@ -0,0 +1,3 @@
|
||||
import { clientBundle } from '../tsdown.client.ts'
|
||||
|
||||
export default clientBundle('@deepseek-ai/dsh-client-ui-renderer', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
@@ -61,7 +61,6 @@
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
@@ -76,7 +75,6 @@
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
|
||||
@@ -3,22 +3,23 @@
|
||||
import { useEffect } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { SettingsDocumentState, SettingsDocumentStore } from './settings-document-store.ts'
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { SettingsDocumentStore } from './settings-document-store.ts'
|
||||
import css from './SettingsDocumentAction.module.css'
|
||||
|
||||
/** Registrant-owned dependencies of {@link SettingsDocumentAction}. */
|
||||
export interface SettingsDocumentActionInjected {
|
||||
/** Provider metadata and action state owner. */
|
||||
controller: SettingsDocumentStore
|
||||
/** Bound selector hook for the controller snapshot. */
|
||||
useSnapshot: SnapshotSelectorHook<SettingsDocumentState>
|
||||
hooks: {
|
||||
/** Controller snapshot bound by the UI renderer as useSnapshot. */
|
||||
snapshot: SettingsDocumentStore['store']
|
||||
}
|
||||
}
|
||||
|
||||
/** Header-action owner share, localized copy, and the registrant's state face. */
|
||||
export type SettingsDocumentActionProps =
|
||||
PropsRuntime<'settings.action'> & PropsLocale<'settings'> & SettingsDocumentActionInjected
|
||||
PropsRuntime<'settings.action'> & PropsLocale<'settings'> & InjectFace<SettingsDocumentActionInjected>
|
||||
|
||||
/**
|
||||
* Render the open-document action only after Host metadata confirms document availability.
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
// Type-only: the settings slot declarations plus the ctx.settingsScope Context
|
||||
// merge. Cross-plugin collaboration goes through the service, never a value
|
||||
// import (client bundle purity gate).
|
||||
@@ -74,10 +73,10 @@ export function apply(ctx: ClientContext): void {
|
||||
: undefined
|
||||
const documentInjected = documentController === undefined
|
||||
? undefined
|
||||
: (() => {
|
||||
const useSnapshot = bindSnapshotSelector(documentController.store)
|
||||
return (): SettingsDocumentActionInjected => ({ controller: documentController, useSnapshot })
|
||||
})()
|
||||
: (): SettingsDocumentActionInjected => ({
|
||||
controller: documentController,
|
||||
hooks: { snapshot: documentController.store },
|
||||
})
|
||||
ctx.effect(() => ctx.on('connection/reset', () => {
|
||||
refreshDocumentIfLoaded(documentController)
|
||||
}), 'ui-settings-general: metadata invalidations')
|
||||
|
||||
@@ -97,7 +97,7 @@ describe('ui-settings-general apply', () => {
|
||||
const action = before.slots.entries('settings.action')[0]!
|
||||
const actionInjected = (action.inject as unknown as () => SettingsDocumentActionInjected)()
|
||||
expect(actionInjected.controller.store.getSnapshot().status).toBe('idle')
|
||||
expect(actionInjected.useSnapshot).toEqual(expect.any(Function))
|
||||
expect(actionInjected.hooks.snapshot).toBe(actionInjected.controller.store)
|
||||
// Copy rides the standard locale seat: every seat declares the namespace.
|
||||
for (const [name] of SEATS) {
|
||||
expect(before.slots.entries(name)[0]!.locale).toBe('settings')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { GeneralSectionComponentProps } from '../src/client/GeneralSection.tsx'
|
||||
import { GeneralSection } from '../src/client/GeneralSection.tsx'
|
||||
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
{
|
||||
"path": "../ui-settings"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../locale"
|
||||
},
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
|
||||
@@ -16,10 +16,10 @@ import { useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { Button, IconPlusOutline16, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { CustomProviderCard } from './CustomProviderCard.tsx'
|
||||
import { deriveKeyRef, messageOf, protocolChoices, providerUsable } from './store.ts'
|
||||
import type { ModelsSettingsState, ModelsSettingsStore, ProviderRow } from './store.ts'
|
||||
import type { ModelsSettingsStore, ProviderRow } from './store.ts'
|
||||
import { ProviderEditor, type ProviderEditorProps } from './ProviderEditor.tsx'
|
||||
import type { en } from './locales.ts'
|
||||
import styles from './ModelsSection.module.css'
|
||||
@@ -28,8 +28,10 @@ import styles from './ModelsSection.module.css'
|
||||
export interface ModelsSectionInjected {
|
||||
/** The page store (loaded on mount, refreshed on pushed invalidations). */
|
||||
controller: ModelsSettingsStore
|
||||
/** uSES subscription hook bound to the store. */
|
||||
useSnapshot: SnapshotSelectorHook<ModelsSettingsState>
|
||||
hooks: {
|
||||
/** Page snapshot bound by the UI renderer as useSnapshot. */
|
||||
snapshot: ModelsSettingsStore['store']
|
||||
}
|
||||
/** Wire faces the editor writes through. */
|
||||
api: Pick<IApiClient, 'settings' | 'credentials' | 'llm'>
|
||||
/** Section copy. */
|
||||
@@ -40,7 +42,9 @@ export interface ModelsSectionInjected {
|
||||
* Props delivered by the slot outlet: the inject face spread flat (the
|
||||
* renderer erases the share boundary at the render call).
|
||||
*/
|
||||
export type ModelsSectionProps = Partial<ModelsSectionInjected>
|
||||
export type ModelsSectionProps = Partial<InjectFace<ModelsSectionInjected>>
|
||||
|
||||
type ModelsSectionFace = InjectFace<ModelsSectionInjected>
|
||||
|
||||
/** Provider identity shared by row actions and confirmation copy. */
|
||||
export interface ProviderIdentity {
|
||||
@@ -174,7 +178,7 @@ export function ModelsSection(props: ModelsSectionProps): ReactNode {
|
||||
return <Loaded injected={{ controller, useSnapshot, api, t }} />
|
||||
}
|
||||
|
||||
function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
|
||||
function Loaded({ injected }: { injected: ModelsSectionFace }): ReactNode {
|
||||
const { controller, api, t } = injected
|
||||
const state = injected.useSnapshot(snapshot => snapshot)
|
||||
const [editing, setEditing] = useState<EditorTarget | undefined>(undefined)
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
// Type-only: pulls the shell's SlotMap merge (the 'settings.section' entry).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
||||
@@ -69,13 +68,12 @@ export function apply(ctx: ClientContext): void {
|
||||
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const controller = new ModelsSettingsStore(connection.api, ctx.settingsSchema)
|
||||
const useSnapshot = bindSnapshotSelector(controller.store)
|
||||
// Registration-time text (the nav label thunk) and the inject faces share
|
||||
// one bound translate; copy freshness rides the locale revision.
|
||||
const t = ctx.locale.bind(NS) as ModelsSectionInjected['t']
|
||||
const injected = (): ModelsSectionInjected => ({
|
||||
controller,
|
||||
useSnapshot,
|
||||
hooks: { snapshot: controller.store },
|
||||
api: connection.api,
|
||||
t,
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('ui-settings-models apply', () => {
|
||||
expect(injected.t('nav')).toBe('模型')
|
||||
expect(injected.t('deleteTitle')).toBe('删除 {provider}?')
|
||||
expect(typeof injected.controller.load).toBe('function')
|
||||
expect(typeof injected.useSnapshot).toBe('function')
|
||||
expect(injected.hooks.snapshot).toBe(injected.controller.store)
|
||||
expect(injected.api).toBeDefined()
|
||||
const onboarding = before.slots.entries('settings.onboarding')
|
||||
expect(onboarding).toHaveLength(2)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Schema from '@deepseek-ai/schemastery'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import {
|
||||
ModelsSection, needsSetup, providerCopy, providerTargetLabel, removeProviderProfile,
|
||||
@@ -188,7 +188,7 @@ async function mountFace(scripted: ReturnType<typeof scriptedFace>) {
|
||||
const { face, update, replace, mutate, set, unset } = scripted
|
||||
const controller = new ModelsSettingsStore(face as unknown as WireFace, settingsSchema)
|
||||
await controller.load()
|
||||
const injected: ModelsSectionInjected = {
|
||||
const injected: ModelsSectionProps = {
|
||||
controller,
|
||||
useSnapshot: bindSnapshotSelector(controller.store),
|
||||
api: face as never,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-libra
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Schema from '@deepseek-ai/schemastery'
|
||||
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { DeepSeekOnboardingDialog } from '../src/client/DeepSeekOnboardingDialog.tsx'
|
||||
import type { DeepSeekOnboardingDialogProps } from '../src/client/DeepSeekOnboardingDialog.tsx'
|
||||
import { ModelsSettingsStore } from '../src/client/store.ts'
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Schema from '@deepseek-ai/schemastery'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { ModelsSection, providerCopy } from '../src/client/ModelsSection.tsx'
|
||||
import type { ModelsSectionInjected } from '../src/client/ModelsSection.tsx'
|
||||
import type { ModelsSectionInjected, ModelsSectionProps } from '../src/client/ModelsSection.tsx'
|
||||
import { CustomProviderCard } from '../src/client/CustomProviderCard.tsx'
|
||||
import { formatCapacity, parseCapacity } from '../src/client/DeepSeekModelsEditor.tsx'
|
||||
import { ModelsSettingsStore, deriveKeyRef, protocolChoices } from '../src/client/store.ts'
|
||||
@@ -142,7 +142,7 @@ async function mountSection(options: Parameters<typeof scriptedFace>[0] = {}) {
|
||||
const scripted = scriptedFace(options)
|
||||
const controller = new ModelsSettingsStore(scripted.face as unknown as WireFace, settingsSchema)
|
||||
await controller.load()
|
||||
const injected: ModelsSectionInjected = {
|
||||
const injected: ModelsSectionProps = {
|
||||
controller,
|
||||
useSnapshot: bindSnapshotSelector(controller.store),
|
||||
api: scripted.face as never,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx'
|
||||
import type { WelcomeNoticeProps } from '../src/client/WelcomeNotice.tsx'
|
||||
import { WelcomeNoticeStore } from '../src/client/welcome-store.ts'
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
{
|
||||
"path": "../ui-primitives"
|
||||
},
|
||||
{
|
||||
"path": "../web-react"
|
||||
},
|
||||
{
|
||||
"path": "../ui-settings"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user