Merge branch 'worktree/ci-native-windows-20260808' into worktree/ci-native-windows-coverage-20260808
This commit is contained in:
@@ -22,7 +22,7 @@ How live data reaches render code, and what may cross a business boundary:
|
||||
|
||||
1. **Everything a render reads that can change outside React arrives through a framework hook** (rule 4 above). Event-handler code may read live snapshots (e.g. `keyboard.snapshot`); render code subscribes.
|
||||
2. **Business components contain no subscription machinery** — no `useSyncExternalStore`, no manual subscribe wiring, no mirroring an external snapshot into local state or a second store. Give each reactive fact its owning channel instead: registrant-private → the inject `hooks` compartment; cross-entry or remount-surviving → a declared store; per-session standard → `sessions.provide`.
|
||||
3. **Data-access ladder** — resolve needs in this order: framework hooks (standing seats + provide/inject-bound `use<Name>`) → a declared store (`useStore`/`actions`) → inject callbacks → anything else is a new framework seam and needs main-thread arbitration.
|
||||
3. **Data-access ladder** — resolve needs in this order: framework hooks (standing seats + provide/inject-bound `use<Name>`) → a declared store (`useStore`/`actions`) → inject callbacks → anything else is a new framework extension point and needs main-thread arbitration.
|
||||
4. **Contract currency is JSON-able data and callbacks.** Everything crossing a business boundary (owner props, inject faces, store state, provide contributions) is plain serializable data or a callback over such data; the inject `hooks` compartment is the one sanctioned carrier of bare observables, and components never see those either. ReactNode is not a currency: route render content through a slot; no new ReactNode-valued owner props or inject members (the composer's existing `accessory`/`overlay`/`leftItems`/`rightItems` seats are grandfathered and get migrated to slots progressively).
|
||||
5. **An observable source keeps two identities stable**: the source object itself (hook binding is cached per source), and its snapshot between changes (`getSnapshot` returns the same reference until the fact moves).
|
||||
6. **Whoever rebuilds a published value republishes it through the same source in the same step**, and a registration path that can run after consumers exist notifies the live consumers as part of registering.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/connection/README.md
|
||||
README.md: 161e34c4b6018625fb690e178eb9a9f8ac0ef21b
|
||||
README.zh.md: 731daa15c8944febae1712788a9ae017d4756960
|
||||
README.md: dba01cb8c487dbc670a471053f8cd12dd277ef4c
|
||||
README.zh.md: d12ef605f1113411a59c2d73c2bf105dfb021be8
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` seam, and the loop's sink/config types. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single `/api` route and its Fetch bridge; a registered TypeRT interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md); the protocol contract is api-contracts v3 §3.
|
||||
Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` abstraction, and the loop's sink/config types. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The Host half owns the single `/api` route and its Fetch bridge; a registered TypeRT interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md); the protocol contract is api-contracts v3 §3.
|
||||
|
||||
## /api browser-trust fence
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 单消费方流循环启动器);导出表层携带协议约定类型、`AbstractApiClient` seam,以及循环的 sink/配置类型。浏览器载体以 HTTP POST 发送 unary/respond,并为 `events.mux` 与 `events.host` 各开一条只下行的 WebSocket;进程内载体满足同一双流抽象。Host half 持有唯一 `/api` route 及其 Fetch bridge;已注册的 TypeRT interceptor 会先认领自己的 Remote endpoint,未认领请求再回退 API Proxy。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory`、`host.openPath`,以及整个配置面——`settings.describe`/`openDocument`/`update`/`replace`/`mutate` 与 `credentials.describe`/`set`/`unset`;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处)以空信任表过信任 fence,从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部;apply 负责选择并驱动它们。下行边界见 [WebSocket 下行载体 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md);协议约定见 api-contracts v3 §3。
|
||||
协议消费层:客户端插件的 apply 会挂载 `ctx.connection`(共享 API 客户端 + 当前页面的 loopback 状态 + 单消费方流循环启动器);导出表层携带协议约定类型、`AbstractApiClient` 抽象,以及循环的 sink/配置类型。浏览器载体以 HTTP POST 发送 unary/respond,并为 `events.mux` 与 `events.host` 各开一条只下行的 WebSocket;进程内载体满足同一双流抽象。Host half 持有唯一 `/api` route 及其 Fetch bridge;已注册的 TypeRT interceptor 会先认领自己的 Remote endpoint,未认领请求再回退 API Proxy。Loopback hostname 判定逻辑留在包内部:`/api` Host fence 与 WebSocket upgrade 会直接使用它,其他客户端插件则消费派生的 `ctx.connection.isLoopback` 状态。node 半侧的 `/api` 路由让特权方法集(`host.pickDirectory`、`host.openPath`,以及整个配置面——`settings.describe`/`openDocument`/`update`/`replace`/`mutate` 与 `credentials.describe`/`set`/`unset`;读取与原生操作也在内,因为 describe 会返回已暴露的配置、打开操作会作用于 Host 桌面,而探测任意引用会报出某条凭据来自何处)以空信任表过信任 fence,从而钉在回环——已声明的 `trustedHosts` 授权可达其余全部方法,而这些方法在真正的认证层出现之前仍只限回环本机。平台载体与 ConnectionController 循环属于包内部;apply 负责选择并驱动它们。下行边界见 [WebSocket 下行载体 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md);协议约定见 api-contracts v3 §3。
|
||||
|
||||
## /api 浏览器信任栅栏
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Central contract re-export point: every contract import inside
|
||||
// web-runtime goes through this single file.
|
||||
// Types and runtime protocol helpers/bounds come from the apiproxy api/ layer
|
||||
// (zero Node deps, browser-safe); AbstractApiClient is the client seam.
|
||||
// (zero Node deps, browser-safe); AbstractApiClient is the client boundary.
|
||||
// NEVER import the package root: it drags bootHost/cordis into the browser bundle.
|
||||
// The ./api and ./client subpath exports are the browser-safe channels added for this.
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ export class LocaleService {
|
||||
* the typed form: each dictionary is checked against the namespace's
|
||||
* {@link LocaleNamespaceMap} key union (a missing or extra key is a
|
||||
* compile error), and every shipped locale is required (bilingual balance
|
||||
* enforced at the seam). Duplicate (ns, locale) throws (single occupant; a
|
||||
* enforced at registration). Duplicate (ns, locale) throws (single occupant; a
|
||||
* namespace's texts have one owner). Registration bumps the revision so
|
||||
* mounted outlets pick up late-arriving dictionaries.
|
||||
* @param ns - a namespace merged into LocaleNamespaceMap.
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('LocaleService', () => {
|
||||
expect(t('own')).toBe('自有')
|
||||
// common itself must not recurse: a miss inside common echoes the key.
|
||||
// (Wide-string ns hits the untyped bind overload — the typed one rejects
|
||||
// unknown keys at compile time, which is the point of the seam.)
|
||||
// unknown keys at compile time, which is the point of the typed registry contract.)
|
||||
expect(svc.bind('common' as string)('nope')).toBe('nope')
|
||||
})
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/modules/README.md
|
||||
README.md: 7d661c806955d0fac021dd6620994aab83c0f773
|
||||
README.zh.md: aec59474fbf01857380709ebecb6ae99a6ca8174
|
||||
README.md: 1d327c7252f4b3001ad758b7a4db01e9907c3060
|
||||
README.zh.md: a97672b909c98367e8c1287e3b341fb612f2d110
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Client module system: the browser peer of Node's internal ESM loader, built as a lazy CJS table. The web shell mounts the vendored cordis Loader for entry governance (fiber lifecycle, inject waiting, update/refresh) and injects this package's `ClientModuleLoader` as its `internal` seam — the vendored side's only consumption point is `EntryTree.import`, so replacing `internal` replaces exactly "how plugin code arrives" and nothing else.
|
||||
Client module system: the browser peer of Node's internal ESM loader, built as a lazy CJS table. The web shell mounts the vendored cordis Loader for entry governance (fiber lifecycle, inject waiting, update/refresh) and injects this package's `ClientModuleLoader` through its `internal` contract — the vendored side's only consumption point is `EntryTree.import`, so replacing `internal` replaces exactly "how plugin code arrives" and nothing else.
|
||||
|
||||
Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → export surface, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively, so load order needs no external sequencing; require cycles throw (factory-form CJS cannot deliver partial exports). `<id>/client` and the bare id name the same surface (a plugin bundle IS its package's client half).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
客户端模块系统:Node 内部 ESM loader 的浏览器端对等实现,以惰性 CJS 表实现。web 外壳挂载 vendored cordis Loader 来治理配置项(fiber 生命周期、inject 等待、update/refresh),并把该包(package)的 `ClientModuleLoader` 作为其 `internal` seam 注入;vendored 一侧唯一的消费点是 `EntryTree.import`,因此替换 `internal` 恰好只会替换「插件代码如何到达」,不会改变其他内容。
|
||||
客户端模块系统:Node 内部 ESM loader 的浏览器端对等实现,以惰性 CJS 表实现。web 外壳挂载 vendored cordis Loader 来治理配置项(fiber 生命周期、inject 等待、update/refresh),并通过其 `internal` 约定注入该包(package)的 `ClientModuleLoader`;vendored 一侧唯一的消费点是 `EntryTree.import`,因此替换 `internal` 恰好只会替换「插件代码如何到达」,不会改变其他内容。
|
||||
|
||||
惰性 CJS 模型(web2):执行插件组合包只会注册其 factory(`window.__ModuleLoader__.load({id, factory})`);每个模块主体的副作用(包括 CSS 注入)都位于 factory 闭包中,在物化时运行(`factory(require)` → 导出表层,并在 `loadCache` 中记忆化),不会在脚本执行时运行。如果 factory 依赖另一个已注册但尚未物化的模块,系统会递归物化它,因此加载顺序无需外部编排;require 循环会抛出异常(factory 形式的 CJS 无法提供部分导出)。`<id>/client` 与裸 id 指向同一表层(一个插件组合包就是其包的客户端侧)。
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Client module system: the browser peer of Node's internal ESM loader, built
|
||||
* as a lazy CJS table. The vendored cordis Loader consumes this object
|
||||
* through its `internal` seam (the only call site is `EntryTree.import` →
|
||||
* through its `internal` contract (the only call site is `EntryTree.import` →
|
||||
* `internal.import`), which keeps entry governance (fiber lifecycle, inject
|
||||
* waiting, update/refresh) entirely on the vendored side while this package
|
||||
* owns code arrival.
|
||||
@@ -25,7 +25,7 @@
|
||||
* imports are a build error anyway.
|
||||
*
|
||||
* This file is the browser-safe contract face (zero node imports): the
|
||||
* `__DSH_BOOT__` wire types, the boot-manifest parser, and the seams around
|
||||
* `__DSH_BOOT__` wire types, the boot-manifest parser, and the boundaries around
|
||||
* {@link ClientModuleSystem}. The package root is the host-side service that
|
||||
* composes the wire.
|
||||
*/
|
||||
@@ -170,7 +170,7 @@ export interface DshWindow {
|
||||
__DSH_MODULES__?: ClientModuleSystem
|
||||
}
|
||||
|
||||
/** Per-module bookkeeping in {@link ClientModuleLoader.loadCache} (module-graph seam, flat today). */
|
||||
/** Per-module bookkeeping in {@link ClientModuleLoader.loadCache} (module-graph boundary, flat today). */
|
||||
export interface ClientModuleRecord {
|
||||
/** Module id (entry name / package name). */
|
||||
id: string
|
||||
@@ -178,12 +178,12 @@ export interface ClientModuleRecord {
|
||||
surface: unknown
|
||||
/** Owned `<style data-plugin>` tag ids (`data-plugin-css` values) injected during materialization. */
|
||||
styles: string[]
|
||||
/** Observed `require()` edges (module-graph seam; only table words can appear today). */
|
||||
/** Observed `require()` edges (module-graph boundary; only table words can appear today). */
|
||||
edges: Set<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal-seam subset the vendored Loader and the client HMR plugin
|
||||
* The internal-contract subset the vendored Loader and the client HMR plugin
|
||||
* consume. Mounted on `ctx.loader.internal` by the shell boot and provided
|
||||
* as `ctx.modules` (contract C5).
|
||||
*/
|
||||
@@ -193,12 +193,12 @@ export interface ClientModuleLoader {
|
||||
/** Materialized-module registry: id → record. The governance-side read face for entry export surfaces. */
|
||||
loadCache: Map<string, ClientModuleRecord>
|
||||
/**
|
||||
* Internal seam consumed by the vendored Loader's `tree.import`. Resolves
|
||||
* Internal contract consumed by the vendored Loader's `tree.import`. Resolves
|
||||
* `specifier` through the branch order documented on the module, fetching
|
||||
* and executing a bundle when needed.
|
||||
* @param specifier - module specifier (entry name or table word).
|
||||
* @param parentURL - importer URL (unused — the client module graph is flat).
|
||||
* @param attrs - import attributes (unused; interface parity with Node's seam).
|
||||
* @param attrs - Import attributes (unused; interface parity with Node's loader contract).
|
||||
* @returns the module's export surface.
|
||||
*/
|
||||
import(specifier: string, parentURL: string, attrs: Record<string, unknown>): Promise<unknown>
|
||||
@@ -232,6 +232,6 @@ export interface ClientModuleSystemOptions {
|
||||
modules: BootModuleRow[]
|
||||
/** Module-table seed: platform-singleton specifier → shell instance. */
|
||||
staticModules: Record<string, unknown>
|
||||
/** Bundle-load seam. Defaults to a same-origin classic `<script src>` element. */
|
||||
/** Bundle-load hook. Defaults to a same-origin classic `<script src>` element. */
|
||||
loadBundle?: (url: string) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* ClientModuleSystem — the implementation behind the {@link ClientModuleLoader}
|
||||
* seam. The conceptual contract (lazy CJS model, resolution branch order) is
|
||||
* contract. The conceptual contract (lazy CJS model, resolution branch order) is
|
||||
* documented on the public interfaces in `./manifest.ts`; this file owns the
|
||||
* state tables and the load/materialize machinery.
|
||||
*/
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
ClientModuleSystemOptions, ClientPluginHandoff, DshWindow,
|
||||
} from './manifest.ts'
|
||||
|
||||
/** Default bundle-load seam: same-origin external classic script. */
|
||||
/** Default bundle-load hook: same-origin external classic script. */
|
||||
const defaultLoadBundle = (url: string): Promise<void> => new Promise((resolve, reject) => {
|
||||
const el = document.createElement('script')
|
||||
el.async = true
|
||||
@@ -53,7 +53,7 @@ const claimStyles = (id: string): string[] => {
|
||||
/**
|
||||
* The client module system: state tables plus the arrival/materialization
|
||||
* machinery implementing {@link ClientModuleLoader} (whose members carry the
|
||||
* seam contract docs). Construction indexes the boot rows and installs the
|
||||
* contract documentation). Construction indexes the boot rows and installs the
|
||||
* `window.__ModuleLoader__` registration sink (contract C6) — once per page.
|
||||
*/
|
||||
export class ClientModuleSystem implements ClientModuleLoader {
|
||||
@@ -72,7 +72,7 @@ export class ClientModuleSystem implements ClientModuleLoader {
|
||||
|
||||
/**
|
||||
* Build the module system over the parsed boot rows.
|
||||
* @param options - module rows, module-table staticModules, and bundle-load seam.
|
||||
* @param options - Module rows, module-table staticModules, and bundle-load hook.
|
||||
*/
|
||||
constructor(options: ClientModuleSystemOptions) {
|
||||
this.seed = new Map(Object.entries(options.staticModules))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* registers the factory), materialization on first import/require with
|
||||
* memoization and recursive self-sequencing, the resolution branch order,
|
||||
* shared in-flight arrival, invalidate-refetch (HMR), style claiming, the
|
||||
* default transport seam, and the loud failure modes (duplicate
|
||||
* default transport hook, and the loud failure modes (duplicate
|
||||
* registration, cycles, table misses, double boot).
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/runtime/README.md
|
||||
README.md: 08a45adc441cc2f672e84dea38d0991537187a1f
|
||||
README.zh.md: 7abf86aee015a7108547a0025ca8ba5beceecfb2
|
||||
README.md: bd8528e97b04d5b4b28922266306969e8f19295a
|
||||
README.zh.md: 9aea486fb17c5a170ee8c1195435d220b495b615
|
||||
|
||||
@@ -36,7 +36,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and
|
||||
|
||||
## The human transcript
|
||||
|
||||
`ConversationSnapshot.nodes` is the human transcript, not the model surface. `TranscriptAdapter` projects the raw window in log order — every append-origin surface event (`isAppendSurfaceEvent`) at its own log position, plus one `CompactionSummaryNode` marker per landed compaction checkpoint — and never consults surface order. `SteeringHistory` replays the durable `agent/inbox/spliced` records in that window: a user-origin message claimed from `next-step` becomes a `SteeringMessageNode` when its matching `user/message` lands, a `next-turn` claim stays a user node, and non-user next-step input stays context. `ConversationSnapshot.turnEnds` maps each completed turn in that window to its `turn/end` seq, retaining turn completion independently from the transcript so presentation can require a real boundary before enabling an action. A landed compaction therefore keeps the conversation it shadowed on the model side: the marker reports where the model stopped seeing that history instead of erasing it. Model-only replacement copies stay out: a pruned `tool/result` and a regenerated `assistant/message` rewrite one node for the model and mark no boundary. A checkpoint is a `user/message` carrying the compaction seam's plugin source that **replaced** a surface range; an appending plugin-sourced `user/message` is injected context, not a compaction. Each context node also carries a `provenance` view with the producer role and name: `contextProvenance()` reads the durable source alone to decide whether the row is an `inject` or a cross-session `recall`, and to name its producer from the instruction paths, referenced session titles, or plugin id that source already records. The client holds no table of plugin ids, so a renamed or newly mounted producer stays identifiable without a client release and a resumed or foreign log projects exactly like a live one; a source with no readable kind degrades to an unnamed injection. Beside it, `contextForm()` reads the producer-declared `ContextForm` — the second, independent axis: `kind` says who produced the context, `form` says what shape of information it is, so several producers may share one form. A form this UI version does not present projects as null and renders opaque. The adapter's plugin literal is pinned to the seam's own declaration by a type-only import of the cordis-free [`dsh-compact/checkpoint`](../../compact/compact/README.md) leaf, so renaming it there fails `tsc` here; a **value** import of the package would fail the client purity gate, and the package **root** is unreachable even as a type (it reaches `dsh-session`'s root, whose `Context` merge collides the host `sessions` with this program's).
|
||||
`ConversationSnapshot.nodes` is the human transcript, not the model surface. `TranscriptAdapter` projects the raw window in log order — every append-origin surface event (`isAppendSurfaceEvent`) at its own log position, plus one `CompactionSummaryNode` marker per landed compaction checkpoint — and never consults surface order. `SteeringHistory` replays the durable `agent/inbox/spliced` records in that window: a user-origin message claimed from `next-step` becomes a `SteeringMessageNode` when its matching `user/message` lands, a `next-turn` claim stays a user node, and non-user next-step input stays context. `ConversationSnapshot.turnEnds` maps each completed turn in that window to its `turn/end` seq, retaining turn completion independently from the transcript so presentation can require a real boundary before enabling an action. A landed compaction therefore keeps the conversation it shadowed on the model side: the marker reports where the model stopped seeing that history instead of erasing it. Model-only replacement copies stay out: a pruned `tool/result` and a regenerated `assistant/message` rewrite one node for the model and mark no boundary. A checkpoint is a `user/message` carrying the compaction seam's plugin source that **replaced** a surface range; an appending plugin-sourced `user/message` is injected context, not a compaction. Each context node also carries a `provenance` view with the producer role and name: `contextProvenance()` reads the durable source alone to decide whether the row is an `inject` or a cross-session `recall`, and to name its producer from the instruction paths, referenced session titles, or plugin id that source already records. The client holds no table of plugin ids, so a renamed or newly mounted producer stays identifiable without a client release and a resumed or foreign log projects exactly like a live one; a source with no readable kind degrades to an unnamed injection. Beside it, `contextForm()` reads the producer-declared `ContextForm` — the second, independent axis: `kind` says who produced the context, `form` says what shape of information it is, so several producers may share one form. A form this UI version does not present projects as null and renders opaque. The adapter's plugin literal is pinned to the Service Definition's declaration by a type-only import of the cordis-free [`dsh-compact/checkpoint`](../../compact/compact/README.md) leaf, so renaming it there fails `tsc` here; a **value** import of the package would fail the client purity gate, and the package **root** is unreachable even as a type (it reaches `dsh-session`'s root, whose `Context` merge collides the host `sessions` with this program's).
|
||||
|
||||
Because the projection is log-ordered, the node array is seq-monotonic by construction: log-only `command/run` / `command/done` nodes splice in by seq, `Session` merges interrupted frozen nodes by their fractional seqs, and a window whose checkpoint cites a shadowed range outside it renders the marker with nothing logged. The marker's summary text, replaced-item count, and estimated shadowed-token count come from the checkpoint's cited `compact/summary` event; a window cut that left that event outside makes those fields unavailable, and a later page that supplies it resolves them. `CommandNode.outcome.sourceEventSeq` preserves a successful command's explicit reference to that summary event, allowing the presentation layer to pair `/compact` with its checkpoint without parsing settlement copy or assuming the two rows are adjacent. Performance contract: one append materializes at most one node and copies the projection only when it adds that node; an event that changes no node keeps the previous array reference (a chunk storm costs nothing), and unchanged nodes keep their object identity.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
`ctx.slots.inject(name, callback)` 将完整的 `SlotMap` key 作为贡献项的依赖,适用于贡献方插件可独立于声明条目激活的情形。声明存在时,它会同步运行 `callback`,否则等待;声明折叠会 dispose(资源释放)回调 effect,重新声明则会再次运行回调。控制器归调用方的插件 fiber 所有,因此卸载贡献方会取消等待或移除其活跃注册项。直接调用 `slots.register()` 向未声明 slot 注册仍会抛出异常。
|
||||
|
||||
回调返回一个同步 disposer 或由多个 disposer 构成的 iterable。因此,generator 可以 yield 多个 `slots.register()` 调用,并将它们组成一项事务:setup 失败会回滚先前 yield 的注册项,teardown 则按逆序运行它们。声明生命周期使用专用的单调 declaration epoch(声明代次),因此,即使折叠与重新声明合并在同一次 renderer 通知中,回调仍会重启,而普通条目变更不会重启它。声明绑定的 teardown 与账本变更同步运行,在同一 tick 内的后续注册之前释放运行时资源。详见 [slot 声明注入决策](../../../.agents/notes/implemented/architecture/2026-08-05-slot-declaration-injection.md)。
|
||||
回调返回一个同步 disposer 或由多个 disposer 构成的 iterable。因此,generator 可以 yield 多个 `slots.register()` 调用,并将它们组成一项事务:setup 失败会回滚先前 yield 的 effect,teardown 则按逆序运行它们。声明生命周期使用专用的单调 declaration epoch(声明代次),因此,即使折叠与重新声明合并在同一次 renderer 通知中,回调仍会重启,而普通条目变更不会重启它。声明绑定的 teardown 与账本变更同步运行,在同一 tick 内的后续注册之前释放运行时资源。详见 [slot 声明注入决策](../../../.agents/notes/implemented/architecture/2026-08-05-slot-declaration-injection.md)。
|
||||
|
||||
## Workspace 与 Session 列表
|
||||
|
||||
@@ -18,7 +18,7 @@ Workspace 和 Session 列表各自具有单调的 `pending` → `ready` 基线
|
||||
|
||||
`WorkspacesService.delete(workspaceId)` 在一元响应成功后从客户端投影中移除注册记录;对应的 `host/workspace-removed` 帧具有幂等性,并负责同步其他标签页。Session 状态与当前 Session selection 相互独立,因此 Workspace 消失后,其已纳入客户端投影的 Session 会立即投影到 Ungrouped 下。
|
||||
|
||||
`WorkspaceListState.archivedSessionIds` 镜像 Host 的注册表级全局归档集合(一个按 Host 顺序的 `readonly SessionId[]`,仅在成员变化时才替换;需要 O(1) 查询的消费方自建临时 Set)。它是全快照状态:`workspace.list` 基线、`archiveSession` 一元回声和 `host/archived-sessions-changed` 帧各自安装完整集合。`WorkspacesService.archiveSession(sessionId)` 通过 wire 归档;投影扫描在当前 selection 落入归档集合时将其清空为 New Session 视图状态——一条规则同时覆盖本地回声、其他标签页的帧、以及重连基线恢复出一个离线期间被归档的 selection。在 `workspace.list` 请求进行中安装的集合还会取代该过期基线携带的集合。各分组视图在所有位置隐藏集合成员,而会话行本身仍留在列表 store 中。
|
||||
`WorkspaceListState.archivedSessionIds` 镜像 Host 的注册表级全局归档集合(一个按 Host 顺序的 `readonly SessionId[]`,仅在成员变化时才替换;需要 O(1) 查询的消费方自建临时 Set)。它是全快照状态:`workspace.list` 基线、`archiveSession` 一元回声和 `host/archived-sessions-changed` 帧各自安装完整集合。`WorkspacesService.archiveSession(sessionId)` 通过 wire 归档;投影层在当前 selection 落入归档集合时统一清空为 New Session 视图状态——一条规则同时覆盖本地回声、其他标签页的帧、以及重连基线恢复出一个离线期间被归档的 selection。在 `workspace.list` 请求进行中安装的集合还会取代该过期基线携带的集合。各分组视图在所有位置隐藏集合成员,而会话行本身仍留在列表 store 中。
|
||||
|
||||
SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 observable;web-react 创建钩子。Workspace 业务状态不会进入 `SessionListState` 或配置项 store。
|
||||
|
||||
@@ -36,7 +36,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸
|
||||
|
||||
## 面向人的 transcript(文本记录)
|
||||
|
||||
`ConversationSnapshot.nodes` 是面向人的 transcript,不是模型 surface。`TranscriptAdapter` 按日志顺序投影原始窗口。每个 append 来源的 surface 事件(`isAppendSurfaceEvent`)落在它自己的日志位置上,每次落地的压缩(compaction)检查点还会贡献一个 `CompactionSummaryNode` 标记;适配器从不查询 surface 顺序。`SteeringHistory` 会重放该窗口中的持久 `agent/inbox/spliced` 记录:用户来源的消息从 `next-step` 被领取,并在与之匹配的 `user/message` 落地时,会投影为 `SteeringMessageNode`;从 `next-turn` 领取的消息仍是用户节点,非用户来源的 next-step 输入仍是上下文。`ConversationSnapshot.turnEnds` 把该窗口中的每个已完成轮次映射到其 `turn/end` seq;它独立于 transcript 保留轮次完成状态,使呈现层能够在启用操作前要求存在真实边界。于是一次落地的压缩会保留它在模型侧遮蔽掉的对话:标记报告模型从哪里开始看不见那段历史,而不是把它抹掉。仅模型可见的 replacement 副本不进入记录:被裁剪的 `tool/result` 和重新生成的 `assistant/message` 只为模型重写一个节点,不标记任何边界。检查点是携带压缩 seam 插件来源、且**替换**了一段 surface 范围的 `user/message`;一条 append 的插件来源 `user/message` 是注入上下文,不是压缩。每个上下文节点还携带一份包含生产者角色和名称的 `provenance` 视图:`contextProvenance()` 只读取持久来源,据此判定该行是 `inject`(注入)还是跨会话的 `recall`(召回),并用该来源已经记录的指令文件路径、被引用会话标题或插件 id 命名其生产者。客户端不保存任何插件 id 表,因此重命名或新挂载的生产者无需客户端发版即可保持可辨识,恢复的会话日志与外部日志的投影结果和实时会话完全一致;没有可读 kind 的来源则降级为无名注入。与之并列的 `contextForm()` 读取生产方声明的 `ContextForm`,这是相互独立的第二根轴:`kind` 说明上下文由谁产生,`form` 说明它是何种形态的信息,因此多个生产方可以共用一种形态。本 UI 版本不呈现的形态投影为 null,按 opaque 渲染。适配器的插件字面量通过对无 cordis 的 [`dsh-compact/checkpoint`](../../compact/compact/README.md) 叶子做仅类型导入,钉在压缩 seam 自己的声明上:在那里改名会让此处 `tsc` 失败;而对该包(package)做**值**导入会被客户端纯度门禁拒绝,包的**根**即便作为类型也无法到达(它会到达 `dsh-session` 的根,其 `Context` 合并会让 host 的 `sessions` 与本程序的冲突)。
|
||||
`ConversationSnapshot.nodes` 是面向人的 transcript,不是模型 surface。`TranscriptAdapter` 按日志顺序投影原始窗口。每个 append 来源的 surface 事件(`isAppendSurfaceEvent`)落在它自己的日志位置上,每次落地的压缩(compaction)检查点还会贡献一个 `CompactionSummaryNode` 标记;适配器从不查询 surface 顺序。`SteeringHistory` 会重放该窗口中的持久 `agent/inbox/spliced` 记录:用户来源的消息从 `next-step` 被领取,并在与之匹配的 `user/message` 落地时,会投影为 `SteeringMessageNode`;从 `next-turn` 领取的消息仍是用户节点,非用户来源的 next-step 输入仍是上下文。`ConversationSnapshot.turnEnds` 把该窗口中的每个已完成轮次映射到其 `turn/end` seq;它独立于 transcript 保留轮次完成状态,使呈现层能够在启用操作前要求存在真实边界。于是一次落地的压缩会保留它在模型侧遮蔽掉的对话:标记报告模型从哪里开始看不见那段历史,而不是把它抹掉。仅模型可见的 replacement 副本不进入记录:被裁剪的 `tool/result` 和重新生成的 `assistant/message` 只为模型重写一个节点,不标记任何边界。检查点是携带压缩 seam 插件来源、且**替换**了一段 surface 范围的 `user/message`;一条 append 的插件来源 `user/message` 是注入上下文,不是压缩。每个上下文节点还携带一份包含生产者角色和名称的 `provenance` 视图:`contextProvenance()` 只读取持久来源,据此判定该行是 `inject`(注入)还是跨会话的 `recall`(召回),并用该来源已经记录的指令文件路径、被引用会话标题或插件 id 命名其生产者。客户端不保存任何插件 id 表,因此重命名或新挂载的生产者无需客户端发版即可保持可辨识,恢复的会话日志与外部日志的投影结果和实时会话完全一致;没有可读 kind 的来源则降级为无名注入。与之并列的 `contextForm()` 读取生产方声明的 `ContextForm`,这是相互独立的第二根轴:`kind` 说明上下文由谁产生,`form` 说明它是何种形态的信息,因此多个生产方可以共用一种形态。本 UI 版本不呈现的形态投影为 null,按 opaque 渲染。适配器的插件字面量通过对无 cordis 的 [`dsh-compact/checkpoint`](../../compact/compact/README.md) 叶子做仅类型导入,钉在 Service Definition 的声明上:在那里改名会让此处 `tsc` 失败;而对该包(package)做**值**导入会被客户端纯度门禁拒绝,包的**根**即便作为类型也无法到达(它会到达 `dsh-session` 的根,其 `Context` 合并会让 host 的 `sessions` 与本程序的冲突)。
|
||||
|
||||
由于投影按日志顺序,节点数组天然按 seq 单调:仅日志的 `command/run` / `command/done` 节点按 seq 插入,`Session` 按分数 seq 归并被打断的冻结节点,而检查点所引范围落在窗口之外的窗口会渲染出标记且不打印任何日志。标记的摘要文本、被替换条目数量和估算的被遮蔽 token 数量都来自检查点引用的 `compact/summary` 事件;窗口切分把该事件留在窗口外时这些字段不可用,后续包含该事件的分页会解析出它们。`CommandNode.outcome.sourceEventSeq` 保留成功命令对该摘要事件的显式引用,使呈现层能够配对 `/compact` 与其检查点,而无须解析结算文案或假定两行相邻。性能约定:一次追加最多物化一个节点,并且仅在加入该节点时复制投影;不改变任何节点的事件保持上一次的数组引用(分片风暴零成本),未变化的节点保持其对象标识。
|
||||
|
||||
@@ -54,7 +54,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸
|
||||
|
||||
## 模型重试投影
|
||||
|
||||
Session 对象会在事件 wire 边界依据生产方的完整字段约定,验证由插件负责、按提供方路由的 `llm/retry` 载荷,包括对计时器、整数、状态、提供方延迟和非空诊断字段的约束。有效事件会移除对应失败步骤的流式输出片段,并在该事件的序列位置插入一条持久的重试提示。该提示在后续重试轮次开始前为 `scheduled`;源轮次中止或被 dispose 时,会将该提示标记为 `cancelled`,重试轮次则会将其标记为 `started`。normal mode 提示携带其有限上限;always mode 提示则保持显式无界。没有重试的终态 `turn/end` 错误会从持久消息与可选错误码投影出一个 `turn-error` 节点;AUTH 投影会把可能回显凭据片段的提供方文案替换为 `API key is invalid`,原始诊断仍保留在会话日志中。进入重试的失败则只保留该次尝试的重试提示。窗口重建与历史回放应用相同的投影,因此刷新既不会让已丢弃的分片重新出现,也不会丢失终态失败反馈。可见但尚未定稿的输出会在终态错误旁冻结为中断的 assistant 节点。
|
||||
Session 对象会在事件 wire 边界依据生产方的完整字段约定,验证由插件负责、按提供方路由的 `llm/retry` 载荷,包括计时器、整数、状态、提供方延迟和非空诊断字段的边界。有效事件会移除对应失败步骤的流式输出片段,并在该事件的序列位置插入一条持久的重试提示。该提示在后续重试轮次开始前为 `scheduled`;源轮次中止或被 dispose 时,会将该提示标记为 `cancelled`,重试轮次则会将其标记为 `started`。normal mode 提示携带其有限上限;always mode 提示则保持显式无界。没有重试的终态 `turn/end` 错误会从持久消息与可选错误码投影出一个 `turn-error` 节点;AUTH 投影会把可能回显凭据片段的提供方文案替换为 `API key is invalid`,原始诊断仍保留在会话日志中。进入重试的失败则只保留该次尝试的重试提示。窗口重建与历史回放应用相同的投影,因此刷新既不会让已丢弃的分片重新出现,也不会丢失终态失败反馈。可见但尚未定稿的输出会在终态错误旁冻结为中断的 assistant 节点。
|
||||
|
||||
## 会话 fork
|
||||
|
||||
@@ -62,7 +62,7 @@ Session 对象会在事件 wire 边界依据生产方的完整字段约定,验
|
||||
|
||||
## 会话模型选择
|
||||
|
||||
每个常驻 `Session` 都拥有一个 `modelSelection` 快照,其中包含当前 `ModelSelection`、按提供方分组的目录、逐提供方失败记录,以及 `idle`/`loading`/`ready`/`selecting`/`error` 状态。历史记录会建立或刷新当前选择,打开选择器会刷新目录;选择失败会保留上一个选择和可用分组。目录与选择操作共用单调递增的代次,因此较旧响应无法覆盖较新的选择。重连重建会恢复 Host 报告的选择,同时不替换未变化的选择子结构。
|
||||
每个常驻 `Session` 都拥有一个 `modelSelection` 快照,其中包含当前提供方/模型目标、按提供方分组的目录、逐提供方失败记录,以及 `idle`/`loading`/`ready`/`selecting`/`error` 状态。历史记录会建立或刷新当前目标,打开选择器会刷新目录;选择失败会保留上一个目标和可用分组。目录与选择操作共用单调递增的代次,因此较旧响应无法覆盖较新的选择。重连重建会恢复 Host 报告的目标,同时不替换未变化的选择子结构。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -70,7 +70,7 @@ Session 对象会在事件 wire 边界依据生产方的完整字段约定,验
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
更改模型选择可能改变提供方侧的缓存复用,或使其失效;该包本身不会改变提示词前缀。
|
||||
更改目标可能改变提供方侧的缓存复用,或使其失效;该包本身不会改变提示词前缀。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export interface ISessions {
|
||||
*/
|
||||
scope(id: SessionId): AgentContext | undefined
|
||||
/**
|
||||
* Read the Agent scope tag off a context (service-method seam: fetch
|
||||
* Read the Agent scope tag off a context (service-method boundary: fetch
|
||||
* bundles must reach scope resolution through ctx.sessions).
|
||||
* @param ctx - any client context.
|
||||
* @returns the session id, or undefined on root contexts.
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ObservableSnapshot } from '../contract/store.ts'
|
||||
import { Notifier } from './notifier.ts'
|
||||
|
||||
// The single projection type table, typed end to end (host unit, wire block,
|
||||
// client store, React hook) — the interface package's pure-type outlet
|
||||
// client store, React hook) — the Service Definition package's pure-type outlet
|
||||
// (`/types`, zero imports), never the package root: the root's dsh-agent →
|
||||
// dsh-session chain would drag the host `Context.sessions` merge into the
|
||||
// client program (one program must not hold both sides). No second
|
||||
|
||||
@@ -488,7 +488,7 @@ export class SessionsService implements ISessions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the Agent scope tag off a context. Service-method seam: fetch
|
||||
* Read the Agent scope tag off a context. Service-method boundary: fetch
|
||||
* bundles must reach scope resolution through ctx.sessions — a cross-bundle
|
||||
* value import of the standalone helper would inline a second module
|
||||
* instance whose private tag Symbol never matches.
|
||||
@@ -503,7 +503,7 @@ export class SessionsService implements ISessions {
|
||||
* Resolve the business Session behind an Agent-scoped context — the one
|
||||
* hop every scoped consumer (event listeners, per-session controllers)
|
||||
* takes from ctx-space into object-space (the client mirror of host
|
||||
* `agent.session`). Same service-method seam as
|
||||
* `agent.session`). Same service-method boundary as
|
||||
* {@link SessionsService.scopeOf}.
|
||||
* @param ctx - an Agent-scoped context.
|
||||
* @returns the session face, or undefined when the ctx is untagged or its scope was pruned.
|
||||
@@ -590,7 +590,7 @@ export class SessionsService implements ISessions {
|
||||
ctx,
|
||||
binding,
|
||||
session,
|
||||
// Sources are bare observables; React binds selector hooks at its own seam.
|
||||
// Sources are bare observables; React binds selector hooks at its own boundary.
|
||||
provideInfo: this.provideChannel.materializeInfo(binding),
|
||||
}
|
||||
this.scopes.set(id, record)
|
||||
|
||||
@@ -198,7 +198,7 @@ export class Session implements SessionFace {
|
||||
/**
|
||||
* Bind the Agent-scoped context minted by SessionsService (single write;
|
||||
* a second bind is a wiring error and throws). Direction stays one-way at
|
||||
* the seam: consumers still reach the Session via `sessions.sessionOf`,
|
||||
* this binding boundary: consumers still reach the Session via `sessions.sessionOf`,
|
||||
* while the Session holds its own dispatch point (host Agent.loopCtx
|
||||
* mirror).
|
||||
* @param actx - the agent's scoped context.
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
// browser bundle cannot resolve; surface.ts has no Node dependencies.
|
||||
import { isAppendSurfaceEvent, isReplacementSurfaceEvent } from '@deepseek-ai/dsh-session/surface'
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
// Cordis-free leaf subpath (the dsh-commands/brand shape): the seam's own
|
||||
// Cordis-free leaf subpath (the dsh-commands/brand shape): the Service Definition's
|
||||
// declaration of the checkpoint source, reachable as a TYPE from this program.
|
||||
// The package ROOT is not — it reaches dsh-session's root, whose Context merge
|
||||
// declares the HOST `sessions: SessionStore` against this program's
|
||||
@@ -28,7 +28,7 @@ import type { AssistantStepMetadata } from './assistant-timing.ts'
|
||||
import { indexAssistantStepTiming, settledAssistantTiming } from './assistant-timing.ts'
|
||||
|
||||
/**
|
||||
* The compaction seam's checkpoint plugin, pinned to the seam's own declaration
|
||||
* The compaction capability's checkpoint plugin, pinned to the Service Definition's declaration
|
||||
* at COMPILE time: renaming it there fails this annotation (`TS2322`). The
|
||||
* import stays type-only because a value import would fail the client purity
|
||||
* gate (`packages/client/tsdown.client.ts`) — cross-plugin value imports are
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* the load-time validations, and the unload cascade). This layer owns what
|
||||
* needs the runtime: the 'slots/changed' event bridge, register and
|
||||
* declaration injection through the caller's ctx.effect (fiber unload
|
||||
* collects both), the renderer install seam (install()/renderSlot('root') +
|
||||
* collects both), the renderer installation contract (install()/renderSlot('root') +
|
||||
* the SlotRendererHost face), and the store INSTANCE axis — handle x scope
|
||||
* key -> create/cache, dropped with the last holding entry, session instances
|
||||
* cleared (with persisted state) on scope death.
|
||||
|
||||
@@ -59,7 +59,7 @@ export class Workspace implements ObservableSnapshot<WorkspaceSnapshot> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize this local Workspace through the Host create seam.
|
||||
* Materialize this local Workspace through the Host create API.
|
||||
* Re-entry shares the in-flight completion; a materialized instance returns undefined.
|
||||
* @returns the Host result, or undefined when this Workspace is already materialized.
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Behavioral half of the compaction-checkpoint drift trap.
|
||||
*
|
||||
* `TranscriptAdapter` pins its plugin literal to the seam's own declaration at
|
||||
* `TranscriptAdapter` pins its plugin literal to the Service Definition's declaration at
|
||||
* compile time through a type-only import of `dsh-compact/checkpoint`, so
|
||||
* renaming the seam's plugin already fails `tsc`. This spec covers the same
|
||||
* renaming the Service Definition's plugin already fails `tsc`. This spec covers the same
|
||||
* drift from the other side — end to end through the adapter, driving it with a
|
||||
* checkpoint built from the canonical `COMPACT_CHECKPOINT_SOURCE` value and
|
||||
* checking the seam's own predicate agrees. Both values come from the
|
||||
* checking the Service Definition's predicate agrees. Both values come from the
|
||||
* cordis-free checkpoint leaf, so the client test program never loads the host
|
||||
* package root or its `Context` merges.
|
||||
*/
|
||||
@@ -17,7 +17,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import { TranscriptAdapter } from '../src/client/sessions/transcript-adapter.ts'
|
||||
|
||||
/** A replacement user message stamped with the seam's own canonical source. */
|
||||
/** A replacement user message stamped with the Service Definition's canonical source. */
|
||||
function canonicalCheckpoint(seq: number): SessionEvent {
|
||||
return {
|
||||
type: 'user/message',
|
||||
@@ -43,7 +43,7 @@ describe('compaction checkpoint recognition', () => {
|
||||
})
|
||||
|
||||
it("agrees with the seam's own predicate on the source it recognizes", () => {
|
||||
// Both sides answer the same question about the same value: if the seam
|
||||
// Both sides answer the same question about the same value: if the Service Definition
|
||||
// renames its plugin, this equality is what breaks.
|
||||
const checkpoint = canonicalCheckpoint(1)
|
||||
expect(checkpoint.type === 'user/message' && isCompactCheckpointSource(checkpoint.data.source)).toBe(true)
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SessionManager } from '../src/client/sessions/manager.ts'
|
||||
import { FakeApiClient, ok } from './fake-api.ts'
|
||||
import { entries, plainTurn } from './event-script.ts'
|
||||
|
||||
// Test-domain keys merged into the projection map (the interface package's
|
||||
// Test-domain keys merged into the projection map (the Service Definition package's
|
||||
// pure-type outlet), the same way domain host plugins merge theirs.
|
||||
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||
interface SessionProjectionMap {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* SlotsService terminal-design account (design.md §11-3 main landing):
|
||||
* built-in 'root', the three load-time throws (duplicate declaration /
|
||||
* undeclared contribution / cross-scope store handle), the renderer install
|
||||
* seam (double install / not installed / non-root key), store instance
|
||||
* undeclared contribution / cross-scope store handle), the renderer installation
|
||||
* contract (double install / not installed / non-root key), store instance
|
||||
* resolution and lifecycle on the ledger axis, and the entry-unload cascade.
|
||||
*/
|
||||
import { Context } from 'cordis'
|
||||
@@ -92,13 +92,13 @@ function captureHost(bench: Bench, children?: object): SlotRendererHost {
|
||||
return host
|
||||
}
|
||||
|
||||
/** Minimal independent Workspace list source for the renderer host seam. */
|
||||
/** Minimal independent Workspace list source for the renderer host contract. */
|
||||
function fakeWorkspaces() {
|
||||
const state = { items: [], phase: 'ready' as const }
|
||||
return { list: { getSnapshot: () => state, subscribe: () => () => undefined } }
|
||||
}
|
||||
|
||||
/** Minimal sessions face for the host seam (list observable + current provide projection). */
|
||||
/** Minimal sessions face for the host contract (list observable + current provide projection). */
|
||||
function fakeSessions() {
|
||||
const state = { ids: [], byId: {}, current: undefined as string | undefined }
|
||||
const absentInfo = { sessionId: undefined, hooks: { session: undefined }, props: {} }
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/schema-form/README.md
|
||||
README.md: 716cc7ba3c24f3a4de081e2d26f803b905235fac
|
||||
README.zh.md: 9473620da83e73f7a095694b2321f1ebc5f9c53d
|
||||
README.md: ef1d2f9d8ce936fe60d38849f975dc8c0a08ded4
|
||||
README.zh.md: aff77bc4c31a9aaa13977480d32551536ddceadd
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Schema/draft model layer for settings editors. The wire's `settings.describe` carries each namespace's serialized schemastery schema (`schema.toJSON()` ref envelope); `rehydrateSchema` turns it back into a live validator with `new Schema(json)` — the same schema object that validates a section on the host validates drafts in the browser, so client-side validation never drifts from the seam's. Editors render their own controls (the Models page hand-writes its card around the fields it probes here); this package owns no React and no rendering.
|
||||
Schema/draft model layer for settings editors. The wire's `settings.describe` carries each namespace's serialized schemastery schema (`schema.toJSON()` ref envelope); `rehydrateSchema` turns it back into a live validator with `new Schema(json)` — the same schema object that validates a section on the host validates drafts in the browser, so client-side validation never drifts from the Service Definition's. Editors render their own controls (the Models page hand-writes its card around the fields it probes here); this package owns no React and no rendering.
|
||||
|
||||
## Contract
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
面向 settings 编辑器的 schema/草稿模型层。wire 侧的 `settings.describe` 携带每个 namespace 的序列化 schemastery schema(`schema.toJSON()` 的 ref 信封);`rehydrateSchema` 用 `new Schema(json)` 将其还原(rehydrate)为活的校验器——在宿主上校验分节的那份 schema 对象,就是在浏览器里校验草稿的那份对象,因此客户端校验绝不会偏离 seam 侧的校验。编辑器各自渲染自己的控件(Models 页围绕它在此探测到的字段手写自己的卡片);该包不含任何 React,也不做任何渲染。
|
||||
面向 settings 编辑器的 schema/草稿模型层。wire 侧的 `settings.describe` 携带每个 namespace 的序列化 schemastery schema(`schema.toJSON()` 的 ref 信封);`rehydrateSchema` 用 `new Schema(json)` 将其还原(rehydrate)为活的校验器——在宿主上校验分节的那份 schema 对象,就是在浏览器里校验草稿的那份对象,因此客户端校验绝不会偏离 Service Definition 的校验。编辑器各自渲染自己的控件(Models 页围绕它在此探测到的字段手写自己的卡片);该包不含任何 React,也不做任何渲染。
|
||||
|
||||
## 约定
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export type { SessionBehaviorOverrides, SessionFixture, Stabilizer } from './fix
|
||||
export { makeTranslate } from './translate.ts'
|
||||
export { usePinnedBrowserLanguages } from './locale-env.ts'
|
||||
|
||||
/** Erased register face for the internal root call (the public declare seam holds the typing). */
|
||||
/** Erased register face for the internal root call (the public declaration contract holds the typing). */
|
||||
type ErasedRegister = (options: object, component: unknown) => () => void
|
||||
|
||||
/**
|
||||
@@ -80,7 +80,7 @@ export interface FeatureHandle {
|
||||
/**
|
||||
* Owner-props cell behind the auto frame: one external store the frame
|
||||
* subscribes to, so {@link SlotTestRuntime.renderSlot} and
|
||||
* {@link SlotView.update} drive React through the standard uSES seam.
|
||||
* {@link SlotView.update} drive React through the standard uSES boundary.
|
||||
*/
|
||||
class OwnerPropsCell {
|
||||
private readonly owners = new Map<string, object>()
|
||||
@@ -146,7 +146,7 @@ export class TestRoot {
|
||||
): Promise<void> {
|
||||
await this.stabilize(() => {
|
||||
// Erased hop (same pattern as SlotsService's own implementation arm);
|
||||
// the declare signature above is the typed seam.
|
||||
// the declaration signature above is the typed contract.
|
||||
this.disposeEntry = (this.slots.register as unknown as ErasedRegister)({ name: 'root', children }, frame)
|
||||
})
|
||||
}
|
||||
@@ -267,7 +267,7 @@ export class SlotTestRuntime {
|
||||
|
||||
/**
|
||||
* Render the root slot tree through the ctx-level entry (the shell's own
|
||||
* seam): `ctx.slots.renderSlot('root', {})` under Testing Library.
|
||||
* entry point): `ctx.slots.renderSlot('root', {})` under Testing Library.
|
||||
* @returns the Testing Library view.
|
||||
*/
|
||||
renderRoot(): RenderResult {
|
||||
|
||||
@@ -368,7 +368,7 @@ export class TestSessions implements ISessions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the session scope tag off a context (service-method seam mirror).
|
||||
* Read the session scope tag off a context (service-method boundary mirror).
|
||||
* @param ctx - any client context.
|
||||
* @returns the session id, or undefined on root contexts.
|
||||
*/
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
|
||||
README.md: 58c8dc68758ddae11966195c42cf324e388cfc47
|
||||
README.zh.md: f60b89c0ac378f5a87bb49194465fd8bc65d7c42
|
||||
README.md: 911bca28dcfb31b1d8ef9ea5458a0d2017d8b31d
|
||||
README.zh.md: 25574421bbcc3992188e378a49a69acf25744af7
|
||||
|
||||
@@ -40,7 +40,7 @@ The chat stats line takes its token accounting from the generic token-meter `tok
|
||||
|
||||
`src/client/` is organized by domain. `contract/` is the shared face for slot declarations, composed props, and cross-domain types; `skeleton/`, `chat/`, `input/`, `queue/`, and `settings/` keep their implementations internal, while `apply.ts` is their assembly point. The `/client` export surface contains only loader entries, service classes, and contract types; components and store factories reach the page through slot registrations.
|
||||
|
||||
A finished turn ends with a turn-tail hole: the chat view renders the `conversation.chat.turnTail` list slot between the closing assistant's body and its IconActions, once per turn at the seq `assistantActionsSeqs` elects, dispatching `TurnTailOwnerProps` (the snapshot nodes, the closing seq, and the tool rows' `openFile`). This package owns only the hole; the produced-files row that fills it — derivation from the mutation tools' `locations`, the chip cap, the copy — lives in `@deepseek-ai/dsh-client-ui-deliverables`, so composing that plugin out of cordis.yml turns the surface off while the hole renders empty at zero cost. The closing prose participates through the same off switch: the chat view asks the optional `chatFileMentions` service (ctx.get; provided by the same plugin) for a closing message's inline-code vocabulary and threads the result into MarkdownText's `fileMentions` seam — an absent service leaves the prose inert.
|
||||
A finished turn ends with a turn-tail hole: the chat view renders the `conversation.chat.turnTail` list slot between the closing assistant's body and its IconActions, once per turn at the seq `assistantActionsSeqs` elects, dispatching `TurnTailOwnerProps` (the snapshot nodes, the closing seq, and the tool rows' `openFile`). This package owns only the hole; the produced-files row that fills it — derivation from the mutation tools' `locations`, the chip cap, the copy — lives in `@deepseek-ai/dsh-client-ui-deliverables`, so composing that plugin out of cordis.yml turns the surface off while the hole renders empty at zero cost. The closing prose participates through the same off switch: the chat view asks the optional `chatFileMentions` service (ctx.get; provided by the same plugin) for a closing message's inline-code vocabulary and threads the result into MarkdownText's `fileMentions` contract — an absent service leaves the prose inert.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Host 带 placement 的 `session/queue` 快照也会携带待处理 steering。Qu
|
||||
|
||||
`src/client/` 按领域组织。`contract/` 是 slot 声明、组合 props 与跨领域类型的共享表层;`skeleton/`、`chat/`、`input/`、`queue/` 和 `settings/` 保持内部实现,`apply.ts` 是它们的组装点。`/client` 导出表层只包含 loader entry、service class 和 contract 类型;组件与 store factory 经 slot 注册抵达页面。
|
||||
|
||||
完成的一轮以一个 turn-tail 空位收尾:chat 视图在收尾 assistant 正文与其 IconActions 之间渲染 `conversation.chat.turnTail` list slot,每轮一次、位于 `assistantActionsSeqs` 选出的 seq,派发 `TurnTailOwnerProps`(快照节点、收尾 seq,以及工具行的 `openFile`)。本包只拥有空位;填充它的产物行——从改写工具 `locations` 的派生、chip 上限、文案——都在 `@deepseek-ai/dsh-client-ui-deliverables` 里,因此把那个插件从 cordis.yml 中组合掉即可关闭该交互面,空位以零成本渲染为空。收尾正文经由同一个开关参与其中:chat 视图向可选的 `chatFileMentions` service(ctx.get;由同一插件提供)索取收尾消息的行内代码词表,并把结果接进 MarkdownText 的 `fileMentions` seam——service 缺席时正文保持死文本。
|
||||
完成的一轮以一个 turn-tail 空位收尾:chat 视图在收尾 assistant 正文与其 IconActions 之间渲染 `conversation.chat.turnTail` list slot,每轮一次、位于 `assistantActionsSeqs` 选出的 seq,派发 `TurnTailOwnerProps`(快照节点、收尾 seq,以及工具行的 `openFile`)。本包只拥有空位;填充它的产物行——从改写工具 `locations` 的派生、chip 上限、文案——都在 `@deepseek-ai/dsh-client-ui-deliverables` 里,因此把那个插件从 cordis.yml 中组合掉即可关闭该交互面,空位以零成本渲染为空。收尾正文经由同一个开关参与其中:chat 视图向可选的 `chatFileMentions` service(ctx.get;由同一插件提供)索取收尾消息的行内代码词表,并把结果接进 MarkdownText 的 `fileMentions` 约定——service 缺席时正文保持死文本。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -363,7 +363,7 @@ export function apply(ctx: Context): void {
|
||||
ctx.plugin(todoDockEntry)
|
||||
|
||||
// The read-only queue dock entry (T9 file territory) rides the same
|
||||
// registration seam into the input dock declared above.
|
||||
// registration path into the input dock declared above.
|
||||
ctx.plugin(queueDockEntry)
|
||||
|
||||
slots.register({
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface PopupDismissFace {
|
||||
}
|
||||
|
||||
/**
|
||||
* Construction seams of one facade. The slash/popup faces are THUNKS: the
|
||||
* Construction dependencies of one facade. The slash/popup faces are THUNKS: the
|
||||
* shell is created inside the sessions provide materialization (before the
|
||||
* scope record is queryable), where `slash.sessionOf`/`command.popupFor`
|
||||
* cannot resolve yet — resolution defers to first interactive use.
|
||||
|
||||
@@ -214,7 +214,7 @@ export function QueueDock({ useSession, updateQueue, notify, t }: QueueDockProps
|
||||
|
||||
/**
|
||||
* The dock entry as a plain registrant plugin. The conversation service is
|
||||
* the action seam; the slot declaration is its independent lifecycle seam.
|
||||
* the action contract; the slot declaration has an independent lifecycle boundary.
|
||||
*/
|
||||
export const queueDockEntry = {
|
||||
name: 'conversation-queue-dock',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* table's relevant cells + the real SessionInput machine (scoped-event
|
||||
* listeners wired the way the hub does) + the real InputBar. ui-command
|
||||
* itself is not a dependency of this package; the source below is the
|
||||
* decision-table contract at the SlashSource seam.
|
||||
* decision-table contract at the `SlashSource` boundary.
|
||||
*/
|
||||
import { Context } from 'cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('selection survives on the store seat', () => {
|
||||
doomed.actions.select({ turnSeq: 1 })
|
||||
expect(localStorage.getItem('dsh.conversation.chat.s1')).not.toBeNull()
|
||||
|
||||
// TestSessions.remove drives the same public slot lifecycle seam the
|
||||
// TestSessions.remove drives the same public slot lifecycle contract the
|
||||
// production SessionsService calls when the scope dies (pruneStoreScope).
|
||||
await b.runtime.sessions.remove('s1')
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* the runtime's built-in 'root' slot and, in the same breath, declares the
|
||||
* four child slots (declaration = exclusive render authority), seats the
|
||||
* layout store (panel geometry), and wires the panel-action service face.
|
||||
* ctx.layout is the cross-plugin panel-action seam; navigation state lives
|
||||
* ctx.layout is the cross-plugin panel-action contract; navigation state lives
|
||||
* with the runtime sessions service. A second effect seats the theme
|
||||
* presenter, which projects ctx.theme snapshots onto document.body.
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Panel geometry itself lives in the root entry's layout store (stores.ts);
|
||||
* the current-session selection lives with the runtime sessions service, and
|
||||
* the per-session active view dissolved into ui-conversation's session store
|
||||
* (its only consumer). What remains here is the seam other plugins'
|
||||
* (its only consumer). What remains here is the contract other plugins'
|
||||
* apply worlds reach for panel transitions (sidebar toggle from ui-sidebar,
|
||||
* details open/close from ui-conversation) — writes stay inside the store's
|
||||
* declared action set, delivered as the registration's bound actions.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* LayoutService behavior: the cross-plugin panel-action face. Geometry
|
||||
* lives in the entry store (layout-store.spec.ts) — here we assert the
|
||||
* delegation seam: attachPanels wiring, the three actions forwarding, the
|
||||
* delegation contract: attachPanels wiring, the three actions forwarding, the
|
||||
* unwired fail-loud, and re-attach overwriting a stale action set.
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-model/README.md
|
||||
README.md: b37f807944850fb34331cd8d78b4a2ccf157f21c
|
||||
README.zh.md: 906096bed06af26517e215b2d1e1da4b62e58095
|
||||
README.md: 519429834f214fcb82eeb692378fb79770fa30be
|
||||
README.zh.md: 116e151d1afeaaa22618c408eed2c7542d1357e7
|
||||
|
||||
@@ -8,7 +8,7 @@ The Host-reported provider/model/reasoning `ModelSelection` is the single select
|
||||
|
||||
When the Host reports that no adapter serves the session's route (`session.models.routable`), this plugin raises a composer block through `ctx.conversation.blocks` and the input goes inert with this plugin's own copy; recovering clears it without a reload. It follows `routable` and nothing else: a `null` — before the first load, or after one failed — never blocks, or a slow Host would lock a working composer, and catalog membership never blocks either, because a route serving a model it stopped advertising is missing from the groups yet perfectly usable. The trigger's own `Select model` fallback still covers that case, which is display, not a gate.
|
||||
|
||||
Directories are per-session, resolved lazily through `ctx.models.directoryFor(sessionId)`, and disposed with the session scope. Addressed subagent sessions expose neither entry, and their directory rejects loads, selections, and reconnect refreshes, because ordinary Agent-bound model RPCs would activate persisted child history outside the direct-parent continuation seam.
|
||||
Directories are per-session, resolved lazily through `ctx.models.directoryFor(sessionId)`, and disposed with the session scope. Addressed subagent sessions expose neither entry, and their directory rejects loads, selections, and reconnect refreshes, because ordinary Agent-bound model RPCs would activate persisted child history outside the direct-parent continuation path.
|
||||
|
||||
The `/client` export surface is the plugin body (`apply`/`inject`), `ModelService`, `ModelDirectory` with its state shape, and the seat's injected face type.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Host 报告的 `ModelSelection` 是唯一的选择事实,其中包含提供方
|
||||
|
||||
当宿主报告没有适配器服务该会话的路由(`session.models.routable`)时,本插件经 `ctx.conversation.blocks` 注册一个 composer 阻塞块,输入框随之停用并显示本插件自己的文案;恢复后无需重新加载即自动清除。它只跟随 `routable`:`null`(首次加载之前,或加载失败之后)绝不阻断,否则一个慢的宿主就会锁死一个本来可用的 composer;目录成员关系同样不阻断,因为一条仍在服务、只是不再公布该模型的路由不在分组里,却完全可用。触发器自己的 `Select model` 回退仍然覆盖那种情形——那是显示,不是闸门。
|
||||
|
||||
目录按会话惰性解析(`ctx.models.directoryFor(sessionId)`),随会话作用域一并释放。已寻址 subagent 会话不公开任一入口,其目录会拒绝加载、选择与重新连接刷新,因为绑定到 agent(智能体)的普通模型 RPC 会在直接 parent 继续执行 seam 之外激活持久化 child 历史。
|
||||
目录按会话惰性解析(`ctx.models.directoryFor(sessionId)`),随会话作用域一并释放。已寻址 subagent 会话不公开任一入口,其目录会拒绝加载、选择与重新连接刷新,因为绑定到 agent(智能体)的普通模型 RPC 会在直接 parent 继续执行路径之外激活持久化 child 历史。
|
||||
|
||||
`/client` 导出面为插件本体(`apply`/`inject`)、`ModelService`、`ModelDirectory` 及其状态形状、slot 注入面类型。
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* ride each entry's own retry surface (popup shell error/retry; seat menu
|
||||
* inline error) without forking the state. Addressed subagent sessions expose
|
||||
* neither entry because those Agent-bound RPCs would activate persisted
|
||||
* history outside the direct-parent continuation seam.
|
||||
* history outside the direct-parent continuation path.
|
||||
*/
|
||||
import type { ModelSelection, SessionModels } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Host clipboard write shared by Web UI copy controls. Success feedback stays
|
||||
// with each control; this seam only reports whether the host accepted a write.
|
||||
// with each control; this helper only reports whether the host accepted a write.
|
||||
|
||||
/**
|
||||
* Write text to the host clipboard, preferring the async Clipboard API and
|
||||
|
||||
@@ -95,7 +95,7 @@ export class IncrementalMarkdownParser {
|
||||
// to verify the whole retained prefix, and startsWith compares bytes two
|
||||
// orders of magnitude faster than parsing them — the cost this class
|
||||
// exists to remove. Passing append/reset deltas instead would push
|
||||
// append bookkeeping across the session-projection seam for a check
|
||||
// append bookkeeping across the session-projection update boundary for a check
|
||||
// that stays sub-millisecond at realistic reply sizes.
|
||||
if (!text.startsWith(this.prevText)) {
|
||||
this.prevText = ''
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// its singular/plural, the head/tail height cap and its expand control, the
|
||||
// empty-diffs null render, and the copy control writing the prefixed diff text
|
||||
// on both the accepted and the refused clipboard paths. writeClipboard's own
|
||||
// return contract is pinned in terminal-block.spec.tsx (the shared seam), so
|
||||
// return contract is pinned in terminal-block.spec.tsx (the shared return contract), so
|
||||
// only its DOM consequence is asserted here.
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// arms, the prompt line's run-state dot, the exit-status pill, the head/tail height cap and its expand control,
|
||||
// and the copy control writing the raw output on both the accepted and the
|
||||
// refused clipboard paths. writeClipboard's own return contract is pinned here
|
||||
// too, since it is the seam both copy controls in this package share; the
|
||||
// too, since it is the return contract both copy controls in this package share; the
|
||||
// resolution of ANSI runs into styles is pinned in ansi.spec.ts, so only its
|
||||
// DOM consequence (which runs get a span wrapper) is asserted here.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
|
||||
/**
|
||||
* Owner share of the browser hole — the only facts crossing the shell/region
|
||||
* seam. Business data and actions arrive through the region's own inject.
|
||||
* boundary. Business data and actions arrive through the region's own inject.
|
||||
*/
|
||||
export interface SidebarSectionOwnerProps {
|
||||
/** Shell fold-state output: wide renders the full browser, rail the icon column. */
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface SourceRoster {
|
||||
all(): readonly SlashSource[]
|
||||
}
|
||||
|
||||
/** Construction seams of one controller. */
|
||||
/** Construction hooks for one controller. */
|
||||
export interface SlashControllerDeps {
|
||||
/** The owning session scope (event dispatch + teardown registration site). */
|
||||
actx: ClientContext
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-slots/README.md
|
||||
README.md: bb489dea0c3848cf3d501dcf095a65fe1cef9ef6
|
||||
README.zh.md: 8b10ddf2d974c3054b5e086a302cd8f84dca9667
|
||||
README.md: 7ee2a3d22c2a41b0e5c356c52d383a047cf99029
|
||||
README.zh.md: 793bb2d1f77e02e34a41fab16b2c2c64b78744dc
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Slot registry pure core, slot terminal design: SlotMap declaration merging, the single `register` composition API on SlotCore, the four-share component-props type family, the store-seat type family, and the renderer install-seam contract. React types only at runtime — the package is React-free and cordis-free.
|
||||
Slot registry pure core, slot terminal design: SlotMap declaration merging, the single `register` composition API on SlotCore, the four-share component-props type family, the store-seat type family, and the renderer installation contract. React types only at runtime — the package is React-free and cordis-free.
|
||||
|
||||
One `register({ name, children?, store?, inject?, ...kind }, Component)` call contributes a component into a declared slot and, in the same breath, declares child slots (declaration = render authorization = runtime spec, one table), a store seat, and the registrant's business face. The component is checked at the call site against `ComposedProps` — the intersection of four shares, each derived from its single source of truth:
|
||||
|
||||
@@ -17,9 +17,9 @@ Chain-kind slots invert keyed routing — entries self-nominate instead of the d
|
||||
|
||||
The standard-kit interfaces (`SessionStandardProps`, `GlobalStandardProps`) are declared empty here and merged by the runtime package (same declare-merge pattern as SlotMap keys). The renderer binds the runtime's session and workspace observable sources into selector hooks. Inject factory parameters derive from the declaration (`InjectParams`): session slots get `sessionId`, a declared store appends baked `actions`, nothing else — data access lives in the apply closure's ctx.
|
||||
|
||||
The store family (`defineStore` spec in / `StoreHandle<T, A>` out) types the store seat: `init` infers the state schema, `actions` is the complete draft-transform write set, `BakedActions` strips the draft parameter into the callbacks components and inject factories receive. The `defineStore` value implementation lives in the runtime package (the engine's home) and satisfies the `DefineStore` contract exported here. Engine products and the renderer host contract carry bare snapshot sources (`getSnapshot`/`subscribe`), never React hooks — hook binding is the render machinery's side of the seam; only the props-contract hook type (`SnapshotSelectorHook`) lives here.
|
||||
The store family (`defineStore` spec in / `StoreHandle<T, A>` out) types the store seat: `init` infers the state schema, `actions` is the complete draft-transform write set, `BakedActions` strips the draft parameter into the callbacks components and inject factories receive. The `defineStore` value implementation lives in the runtime package (the engine's home) and satisfies the `DefineStore` contract exported here. Engine products and the renderer host contract carry bare snapshot sources (`getSnapshot`/`subscribe`), never React hooks — hook binding belongs to the render machinery; only the props-contract hook type (`SnapshotSelectorHook`) lives here.
|
||||
|
||||
`SlotCore` seeds the a-priori `'root'` slot at construction and enforces load-time validation (undeclared-slot registration, duplicate child declaration, one shared handle under two scopes, a chain registration without `select` — all throw at register). An entry's disposer collapses its declared child slots recursively: ledger rows, contributions, and store mounts die on one lifecycle axis. Each key also carries a declaration epoch that advances only on declaration and collapse; the runtime uses it for [`ctx.slots.inject`](../runtime/README.md#slot-declaration-injection), independently from ordinary entry versions. `renderer.ts` carries the install seam (`SlotRenderer`, `SlotRendererHost`) plus `StaleAuthorizationError`/`SlotOwnershipError`; the implementation lives in web-react, the installation in the shell boot.
|
||||
`SlotCore` seeds the a-priori `'root'` slot at construction and enforces load-time validation (undeclared-slot registration, duplicate child declaration, one shared handle under two scopes, a chain registration without `select` — all throw at register). An entry's disposer collapses its declared child slots recursively: ledger rows, contributions, and store mounts die on one lifecycle axis. Each key also carries a declaration epoch that advances only on declaration and collapse; the runtime uses it for [`ctx.slots.inject`](../runtime/README.md#slot-declaration-injection), independently from ordinary entry versions. `renderer.ts` carries the installation contract (`SlotRenderer`, `SlotRendererHost`) plus `StaleAuthorizationError`/`SlotOwnershipError`; the implementation lives in web-react, the installation in the shell boot.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
Slot 注册表纯核心、slot 终端设计:SlotMap 声明合并、SlotCore 上唯一的 `register` 组合 API、四 share 组件 props 类型家族、store seat 类型家族,以及 renderer 安装 seam 约定。只使用 React 类型;该包不依赖 React,也不依赖 Cordis。
|
||||
Slot 注册表纯核心、slot 终端设计:SlotMap 声明合并、SlotCore 上唯一的 `register` 组合 API、四 share 组件 props 类型家族、store seat 类型家族,以及 renderer 安装约定。只使用 React 类型;该包不依赖 React,也不依赖 Cordis。
|
||||
|
||||
一次 `register({ name, children?, store?, inject?, ...kind }, Component)` 调用会向已声明 slot 贡献一个组件,同时声明子 slot(声明 = 渲染授权 = 运行时规范,三者共用一张表)、store seat 以及注册方的业务表层。组件会在调用点依据 `ComposedProps` 接受检查;该类型是四个 share 的交集,每个 share 都从各自的唯一真源派生:
|
||||
|
||||
@@ -17,9 +17,9 @@ chain-kind slot 会反转键控路由:条目自行提名,而不是由分发
|
||||
|
||||
标准工具包接口(`SessionStandardProps`、`GlobalStandardProps`)在这里声明为空,由 runtime 包合并(与 SlotMap key 相同的 declare-merge 模式)。renderer 会把运行时 Session 和 Workspace observable source 绑定为 selector hook。Inject factory 参数从声明派生(`InjectParams`):Session slot 获得 `sessionId`;声明 store 时追加 baked `actions`;没有其他参数,数据访问位于 apply 闭包的 ctx 中。
|
||||
|
||||
store 家族(输入 `defineStore` 规范/输出 `StoreHandle<T, A>`)为 store seat 建模:`init` 推断状态 schema;`actions` 是完整的 draft-transform 写入集合;`BakedActions` 移除 draft 参数,成为组件和 inject factory 收到的回调。`defineStore` 值实现位于 runtime 包(引擎所属位置),并满足这里导出的 `DefineStore` 约定。引擎产物与 renderer host 约定携带裸快照 source(`getSnapshot`/`subscribe`),绝不携带 React hook;hook 绑定属于渲染机制这一侧的 seam,只有 props 约定 hook 类型(`SnapshotSelectorHook`)位于这里。
|
||||
store 家族(输入 `defineStore` 规范/输出 `StoreHandle<T, A>`)为 store seat 建模:`init` 推断状态 schema;`actions` 是完整的 draft-transform 写入集合;`BakedActions` 移除 draft 参数,成为组件和 inject factory 收到的回调。`defineStore` 值实现位于 runtime 包(引擎所属位置),并满足这里导出的 `DefineStore` 约定。引擎产物与 renderer host 约定携带裸快照 source(`getSnapshot`/`subscribe`),绝不携带 React hook;hook 绑定属于渲染机制,只有 props 约定 hook 类型(`SnapshotSelectorHook`)位于这里。
|
||||
|
||||
`SlotCore` 在构造时预置 `'root'` slot,并强制执行加载时验证(注册未声明 slot、重复声明子项、在两个 scope 下使用同一个共享 handle、chain 注册缺少 `select`,这些情况都在 register 时抛出)。条目的 disposer 会递归移除其声明的子 slot:账本行、贡献和 store 挂载都会随同一生命周期结束而移除。每个 key 还携带一个 declaration epoch(声明代次),它只在声明与移除时递增;运行时将其用于 [`ctx.slots.inject`](../runtime/README.md#slot-declaration-injection),且与普通条目版本相互独立。`renderer.ts` 携带安装 seam(`SlotRenderer`、`SlotRendererHost`)以及 `StaleAuthorizationError`/`SlotOwnershipError`;实现在 web-react 中,安装则在外壳启动中完成。
|
||||
`SlotCore` 在构造时预置 `'root'` slot,并强制执行加载时验证(注册未声明 slot、重复声明子项、在两个 scope 下使用同一个共享 handle、chain 注册缺少 `select`,这些情况都在 register 时抛出)。条目的 disposer 会递归移除其声明的子 slot:账本行、贡献和 store 挂载都会随同一生命周期结束而移除。每个 key 还携带一个 declaration epoch(声明代次),它只在声明与折叠时递增;运行时将其用于 [`ctx.slots.inject`](../runtime/README.md#slot-declaration-injection),且与普通条目版本相互独立。`renderer.ts` 携带安装约定(`SlotRenderer`、`SlotRendererHost`)以及 `StaleAuthorizationError`/`SlotOwnershipError`;实现在 web-react 中,安装则在外壳启动中完成。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ export type MatchedShare<E extends SlotEntryDef, M> =
|
||||
/**
|
||||
* Conversation-session selector hook alias for props contracts. Wide by
|
||||
* default at this dependency-inverted layer; the runtime narrows at its
|
||||
* export seam (`UseSession<ConversationSnapshot>`).
|
||||
* export outlet (`UseSession<ConversationSnapshot>`).
|
||||
*/
|
||||
export type UseSession<Snap extends object = object> = SnapshotSelectorHook<Snap>
|
||||
|
||||
@@ -392,7 +392,7 @@ type BaseOptions<K extends keyof SlotMap & string, D extends ChildrenDecl, H, M
|
||||
|
||||
/**
|
||||
* One stored registration, as recorded by the core and read by the render
|
||||
* machinery (type-erased at this boundary; the register seam already proved
|
||||
* machinery (type-erased at this boundary; the registration contract already proved
|
||||
* the shares against the component).
|
||||
*/
|
||||
export interface StoredEntry {
|
||||
@@ -467,7 +467,7 @@ interface SlotRecord {
|
||||
const NO_ENTRIES: readonly StoredEntry[] = Object.freeze([])
|
||||
|
||||
/**
|
||||
* Pure slot registry (no cordis; event emission and the renderer install seam
|
||||
* Pure slot registry (no cordis; event emission and the renderer installation contract
|
||||
* live in the runtime Service wrapper).
|
||||
*
|
||||
* The 'root' slot is the one a-priori declaration, seeded at construction
|
||||
|
||||
@@ -34,11 +34,11 @@ export interface HostObservable<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-erased store instance face at the render seam (the typed twin is
|
||||
* Type-erased store instance face at the render boundary (the typed twin is
|
||||
* {@link StoreInstance}): a bare snapshot source plus the draft-stripped
|
||||
* action callbacks. No React hook crosses this seam — the render machinery
|
||||
* action callbacks. No React hook crosses this boundary — the render machinery
|
||||
* binds `useStore` from the source at its own side (cached per instance);
|
||||
* typing lands at the component seam via {@link PropsStore}.
|
||||
* typing lands at the component boundary via {@link PropsStore}.
|
||||
*/
|
||||
export interface StoreInstanceLike {
|
||||
getSnapshot(): unknown
|
||||
@@ -54,7 +54,7 @@ export interface StoreInstanceLike {
|
||||
/**
|
||||
* Per-session standard props resolved per session id (identity-stable per
|
||||
* session scope; a recreated scope yields a new info). Plugins contribute
|
||||
* members through the runtime `sessions.provide` seam; the render side binds
|
||||
* members through the runtime `sessions.provide` contract; the render side binds
|
||||
* every `hooks` source into a `use<Name>` selector hook (hooks never appear
|
||||
* on the host contract) and spreads `props` verbatim. The runtime itself
|
||||
* contributes the first entry (`'session'` → `useSession`).
|
||||
@@ -161,7 +161,7 @@ export interface SlotRendererHost {
|
||||
locale?: LocaleFace | undefined
|
||||
}
|
||||
|
||||
/** The install seam: runtime owns install()/renderSlot(); web-react implements rendering. */
|
||||
/** The installation contract: runtime owns install()/renderSlot(); web-react implements rendering. */
|
||||
export interface SlotRenderer {
|
||||
/**
|
||||
* Render the root slot tree over the host surface (the only ctx-level entry).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Re-export seam for the `settings.general.item` slot type consumed by this
|
||||
* Re-export outlet for the `settings.general.item` slot type consumed by this
|
||||
* package's Appearance row. The canonical home is the locale package (the
|
||||
* common dependency of every item registrant); this file exists so row
|
||||
* modules import the type from within their own package.
|
||||
|
||||
@@ -89,7 +89,10 @@ function AppRoot({ renderSlot }: AppRootProps) {
|
||||
return <>{renderSlot('conversation', {})}</>
|
||||
}
|
||||
|
||||
/** Same real-stack bench as the toolview-slot spec: SlotsService + renderer + both owning package applies; fakes only at service seams. */
|
||||
/**
|
||||
* Same real-stack bench as the toolview-slot spec: SlotsService + renderer +
|
||||
* both owning package applies; fakes only at service boundaries.
|
||||
*/
|
||||
async function bench(snapshot: ConversationSnapshot) {
|
||||
const ctx = new Context()
|
||||
const slotsFiber = ctx.plugin(SlotsService)
|
||||
|
||||
@@ -58,7 +58,7 @@ const LAYOUT_CHILDREN = {
|
||||
|
||||
/**
|
||||
* Real-stack bench: SlotTestRuntime with the session/layout doubles at the
|
||||
* service seams only (external boundaries), the package apply on its own
|
||||
* service boundaries only, the package apply on its own
|
||||
* fiber, and the test AppFrame occupying 'root'.
|
||||
*/
|
||||
async function bench(nodes: ToolResultNode[]) {
|
||||
|
||||
@@ -350,7 +350,7 @@ function standardKit(
|
||||
/**
|
||||
* One rendered entry: standard kit + cached inject + owner props (owner
|
||||
* wins). The kit and injected shares are erased at the render boundary — the
|
||||
* register seam already proved the composed contract — so each Entry renders
|
||||
* registration contract already proved the composed type — so each Entry renders
|
||||
* through a props-widened view of the component (the design-budgeted
|
||||
* composition point, one per scope branch).
|
||||
*/
|
||||
@@ -584,7 +584,7 @@ function RootOutlet({ ownerProps }: { ownerProps: object }) {
|
||||
/**
|
||||
* Build the renderer the shell installs into the runtime SlotsService
|
||||
* (ctx.slots.install(createSlotRenderer()) at boot; the service owns the
|
||||
* install/renderSlot seam and the double-install/not-installed throws).
|
||||
* install/renderSlot contract and the double-install/not-installed throws).
|
||||
* @returns the renderer.
|
||||
*/
|
||||
export function createSlotRenderer(): SlotRenderer {
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/web/README.md
|
||||
README.md: 74c481d573fb716e624e639c74b35c82e6894f63
|
||||
README.zh.md: 0e36107e50b2e22d65c126a4fd8873a89b0bb687
|
||||
README.md: 48355a046910bd5b78249af7b7781dab6a7c3a60
|
||||
README.zh.md: f0b567569849c698e8a1a29323e7088b49996522
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Web shell kernel: `new AppWebEntry(el, seams?).run()` mounts the whole client through the two-stage boot (web2). Stage one (module face): build the client module system (`@deepseek-ai/dsh-client-modules`) over the host-pushed entry graph (`window.__DSH_BOOT__`) and prefetch the `immediately` tier in parallel — bundle execution registers factories only. Stage two (plugin face): mount the vendored cordis Loader with the module system injected as its `internal` seam, create one loader entry per graph row plus the shell-own app-shell assembly entry (tree.import materializes each module), and gate AppRoot on the settle (loader quiesced + every entry fiber ACTIVE → full UI in one switch). Composition is entirely the host graph's: the roster and the immediately tier live in the composing app; the shell makes zero composition decisions.
|
||||
Web shell kernel: `new AppWebEntry(el, seams?).run()` mounts the whole client through the two-stage boot (web2). Stage one (module face): build the client module system (`@deepseek-ai/dsh-client-modules`) over the host-pushed entry graph (`window.__DSH_BOOT__`) and prefetch the `immediately` tier in parallel — bundle execution registers factories only. Stage two (plugin face): mount the vendored cordis Loader with the module system injected through its `internal` contract, create one loader entry per graph row plus the shell-own app-shell assembly entry (tree.import materializes each module), and gate AppRoot on the settle (loader quiesced + every entry fiber ACTIVE → full UI in one switch). Composition is entirely the host graph's: the roster and the immediately tier live in the composing app; the shell makes zero composition decisions.
|
||||
|
||||
Shell self-sufficiency (web2 hard rule): the kernel value-imports no plugin package — the boot status store and signals are hand-rolled here (`loader-status.ts`), so the loading page works while (and especially when) plugins fail. The app-shell assembly (`@deepseek-ai/dsh-client-app-shell`, a shell-owned pseudo entry with no npm package behind it) is the only module registered through `registerStatic`; it inject-waits on slots/sessions/layout like any plugin.
|
||||
|
||||
`PLATFORM_MODULES` (src/platform.ts) is the single source of truth for the shared module surface: seed-table keys, tsdown client externals, and the vite alias set are its projections.
|
||||
|
||||
The optional `seams` parameter forwards the module system's `loadBundle` transport override (`BootSeams`) for environments where external `<script>` execution cannot reach the page context; ordinary browser callers omit it.
|
||||
The optional override parameter `seams` forwards the module system's `loadBundle` transport override (`BootSeams`) for environments where external `<script>` execution cannot reach the page context; ordinary browser callers omit it.
|
||||
|
||||
The shell owns browser-title projection. With a selected session carrying a durable title, it renders `<session title> — <existing HTML title>` and reacts to later title revisions; no selection or a selected untitled session preserves the existing title, and shell unmount restores it. The existing HTML title remains the configurable product suffix.
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
Web 外壳内核:`new AppWebEntry(el, seams?).run()` 通过两阶段启动(web2)挂载整个客户端。第一阶段(模块侧):构建客户端模块系统(`@deepseek-ai/dsh-client-modules`),以主机推送的配置项图(`window.__DSH_BOOT__`)为基础,并行预取 `immediately` 层级;执行组合包只会注册 factory。第二阶段(插件侧):挂载仓库内置的 Cordis Loader,并把模块系统作为其 `internal` seam 注入;为每一行图数据创建一个 loader 配置项,另创建外壳自身的 app-shell 组装配置项(tree.import 会物化各模块);以 settle 作为 AppRoot 的门禁(loader 完全停稳 + 每个配置项 fiber 都为 ACTIVE → 一次切换显示完整 UI)。组合完全由主机图决定:花名册和 immediately 层级都位于负责组合的应用中;外壳不作任何组合决策。
|
||||
Web 外壳内核:`new AppWebEntry(el, seams?).run()` 通过两阶段启动(web2)挂载整个客户端。第一阶段(模块侧):构建客户端模块系统(`@deepseek-ai/dsh-client-modules`),以主机推送的配置项图(`window.__DSH_BOOT__`)为基础,并行预取 `immediately` 层级;执行组合包只会注册 factory。第二阶段(插件侧):挂载仓库内置的 Cordis Loader,并通过其 `internal` 约定注入模块系统;为每一行图数据创建一个 loader 配置项,另创建外壳自身的 app-shell 组装配置项(tree.import 会物化各模块);以 settle 作为 AppRoot 的门禁(loader 完全停稳 + 每个配置项 fiber 都为 ACTIVE → 一次切换显示完整 UI)。组合完全由主机图决定:花名册和 immediately 层级都位于负责组合的应用中;外壳不作任何组合决策。
|
||||
|
||||
外壳自给自足(web2 硬性规则):内核不对任何插件包(package)执行值导入;启动状态 store 与信号在这里手写(`loader-status.ts`),因此即使插件失败,加载页面仍能工作,而此时这一点尤其重要。app-shell 组装(`@deepseek-ai/dsh-client-app-shell`,由外壳拥有、背后没有 npm 包的伪配置项)是唯一通过 `registerStatic` 注册的模块;它与任何插件一样,通过 inject 等待 slots/sessions/layout。
|
||||
|
||||
`PLATFORM_MODULES`(src/platform.ts)是共享模块表层的唯一真源:种子表 key、tsdown 客户端 external 和 vite alias 集都是它的投影。
|
||||
|
||||
可选 `seams` 参数会为外部 `<script>` 执行无法到达页面上下文的环境转发模块系统的 `loadBundle` 传输覆盖(`BootSeams`);普通浏览器调用方省略此参数。
|
||||
可选的覆盖参数 `seams` 会为外部 `<script>` 执行无法到达页面上下文的环境转发模块系统的 `loadBundle` 传输覆盖(`BootSeams`);普通浏览器调用方省略此参数。
|
||||
|
||||
外壳拥有浏览器标题投影。选中带有持久标题的会话时,它会渲染 `<session title> — <existing HTML title>` 并响应后续标题修订;未选择会话或选中无标题会话时,会保留现有标题;外壳卸载时恢复标题。现有 HTML 标题仍是可配置的产品后缀。
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* `window.__DSH_BOOT__` into the two-view BootManifest (wire boundary, D16)
|
||||
* → build the module system over the module-view rows → render the loading
|
||||
* page → prefetch every `immediately` row in parallel with mounting the
|
||||
* vendored cordis Loader (internal-seam injection BEFORE any entry exists —
|
||||
* vendored cordis Loader (`internal` contract injection BEFORE any entry exists —
|
||||
* the bare-import fallback in tree.import must never run in a browser) →
|
||||
* await the prefetch tier, THEN adopt the modules entry and create one
|
||||
* loader entry per plugin-view row plus the shell-own app-shell assembly
|
||||
@@ -47,7 +47,7 @@ import { getStaticModules } from './seed.ts'
|
||||
import { STATE_LABELS, createLoaderStatusStore, createSignal } from './loader-status.ts'
|
||||
import './base.css'
|
||||
|
||||
/** Module transport seam the shell passes through (jsdom tests replace the <script> path). */
|
||||
/** Module transport hook the shell passes through (jsdom tests replace the <script> path). */
|
||||
export type BootSeams = Pick<ClientModuleSystemOptions, 'loadBundle'>
|
||||
|
||||
/**
|
||||
@@ -80,7 +80,7 @@ export class AppWebEntry {
|
||||
/**
|
||||
* Hold the mount point; all work happens in {@link run}.
|
||||
* @param el - mount point (the app's #root).
|
||||
* @param seams - optional module transport overrides (test environments).
|
||||
* @param seams - Optional module transport overrides for test environments.
|
||||
*/
|
||||
constructor(el: HTMLElement, seams?: BootSeams) {
|
||||
this.el = el
|
||||
@@ -157,7 +157,7 @@ export class AppWebEntry {
|
||||
})))
|
||||
}
|
||||
|
||||
/** Plugin face: mount the Loader, inject the internal seam, adopt modules, create the graph entries, settle, sweep. */
|
||||
/** Plugin face: mount the Loader, inject the `internal` contract, adopt modules, create the graph entries, settle, sweep. */
|
||||
private async runPluginBoot(prefetching: Promise<void>): Promise<void> {
|
||||
const ctx = this.ctx
|
||||
await ctx.plugin(Loader)
|
||||
|
||||
Reference in New Issue
Block a user