refactor: replace overloaded surface terminology

This commit is contained in:
Turtle
2026-07-24 19:54:25 +08:00
parent c172faed37
commit 0c708cb10d
626 changed files with 1396 additions and 1397 deletions

View File

@@ -13,7 +13,7 @@ The [slot system standard](../../.agents/notes/implemented/architecture/2026-07-
3. **Component props are the four shares, all derived**: `PropsRuntime<K>` (SlotMap: owner params + `useSession`/`sessionId` on session scope + global `useSessions`/`useWorkspaces`) & `PropsRenderSlots<S>` (children keys) & `PropsStore<H>` (store factory) & the inject face. Never hand-write a member a share already derives; never re-type a share locally.
4. **Hooks are framework-made only**: `useSession`, `useSessions`, `useWorkspaces`, `useStore`, `renderSlot` are the five standing seats, plus the `use<Name>` hooks the renderer binds from provide contributions and inject `hooks` compartments. Business code never creates a hook or selector as a prop value — pass plain data and callbacks. (Component-internal behavioral hooks that subscribe to nothing external are fine.)
5. **Live data has exactly three channels**: parent knows it → owner props at the renderSlot site; only the component knows it → local state; shared across entries or survives remounts → a store declared at register. Derived data is a pure function over framework-hook data (`useMemo`), never its own subscription.
6. **Stores: read `props.useStore`, write `props.actions.*`** — the declared actions are the complete mutation surface. Write the store as an exported `createXXXStore()` factory (module-level handles are forbidden — de-facto singletons); share by passing one handle to several registers inside `apply`. Production code never calls the factory or `.create()` outside `apply`; tests do (that is the sanctioned zero-machinery path).
6. **Stores: read `props.useStore`, write `props.actions.*`** — the declared actions are the complete mutation API. Write the store as an exported `createXXXStore()` factory (module-level handles are forbidden — de-facto singletons); share by passing one handle to several registers inside `apply`. Production code never calls the factory or `.create()` outside `apply`; tests do (that is the sanctioned zero-machinery path).
7. **inject returns plain data and callbacks** from the apply closure's own ctx — no hand-made hooks, no ReactNode producers, no whole-service objects. A registrant-private reactive fact uses the reserved `hooks` compartment (bare observables the renderer binds to `use<Name>`; components never see the sources). The plugin may use only the dependencies named by its `inject` declaration; there is no wider ctx to reach for.
## Reactive read and contract-currency discipline
@@ -32,7 +32,7 @@ How live data reaches render code, and what UI domains may share:
The `/client` entrypoint of a UI plugin package is its public browser API, not a convenience barrel. Three rules apply package-wide (do not restate them as per-file comments):
1. **A UI plugin exports no values beyond what cordis loading needs**`apply` / `inject` (and `Config` where present), plus store factories consumed type-only by components (`ReturnType<typeof createXXXStore>`). Shared types (owner data, injected values, composed prop aliases) may also be exported. Implementation components, pure helpers, constants, and store handles stay internal. Adding any new value export requires user sign-off, not a matching consumer.
2. **Same-package tests import internals directly** — relative `../src/client/xxx.ts` from package tests, or the `./src/*` subpath where a spec lives outside the package. Never widen the public surface to make a test compile.
2. **Same-package tests import internals directly** — relative `../src/client/xxx.ts` from package tests, or the `./src/*` subpath where a spec lives outside the package. Never widen the public API to make a test compile.
3. **Cross-package imports of another plugin's symbols are in principle forbidden.** The sanctioned routes are the slot system (register/renderSlot) and ctx services. If neither fits, stop and escalate — do not add an export to unblock yourself.
## ctx discipline (components never see ctx)

View File

@@ -71,7 +71,7 @@ const MARKDOWN_FIXTURE = [
'- first item',
' - nested item',
'',
'| Surface | State |',
'| Area | State |',
'| --- | --- |',
'| history | rendered |',
'| streaming | stable |',

View File

@@ -46,7 +46,7 @@ export type { ClientConnectionRpc } from '../rpc.ts'
export const inject: string[] = []
/**
* The ctx.connection service surface: the api client plus a one-shot
* The ctx.connection service API: the API client plus a one-shot
* controller starter (the runtime plugin supplies sinks when its object layer
* is ready — connection stays consumer-agnostic).
*/

View File

@@ -87,7 +87,7 @@ describe('connection client apply', () => {
it('start() hands out one loop, rejects a second consumer, and stop() aborts the streams', async () => {
;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' }
const handle = await mount()
// config omitted: the `config ?? {}` default arm is part of the surface.
// config omitted: the `config ?? {}` default arm is part of the API.
const loop = handle.start({})
expect(() => handle.start({})).toThrow(/already owned by another consumer/)
loop.stop() // teardown must not throw; the fixture streams abort quietly

View File

@@ -16,7 +16,7 @@ export const inject = ['invariants']
/**
* No runtime invariant: ns-by-locale dictionary registry with a stable
* bind(ns) surface — it emits no cordis events and owns no cross-plugin
* bind(ns) API — it emits no cordis events and owns no cross-plugin
* mutable relation; fallback-chain resolution and locale-store behavior are
* asserted directly by this package's behavior specs.
*/

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/modules/README.md
README.md: a1d578850c2518a85dc32f048768b78caf5ffec4
README.md: efaff699839b977cc45f89f3c164402241b90dc2
README.zh.md: 772a4870f7ef6730d9d3d4db434ed771d97984f0

View File

@@ -4,7 +4,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` 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).
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)` → exports, 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 resolve to the same exports (a plugin bundle IS its package's client half).
Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → surface; shell-own static registry (`registerStatic`, app-shell) → module; registered factory → materialize; graph row (`window.__DSH_BOOT__`) → load its external classic script + materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the asynchronous load branch and records observed edges into the module record. `prefetch` is the stage-one arrival hook (script load and factory registration only; concurrent calls share one in-flight task); `invalidate` drops the factory and materialized record so the next prefetch/import reloads the script (the HMR hook).

View File

@@ -10,13 +10,13 @@
* factory (`window.__ModuleLoader__.load({id, factory})`); every module body
* side effect — including CSS injection — lives inside the factory closure
* and runs at materialization, not at script execution. Materialization
* (factory(require) → export surface) happens on first import/require and is
* (factory(require) → exports) happens on first import/require and is
* memoized in {@link ClientModuleLoader.loadCache}; a factory that requires
* another registered-but-unmaterialized module materializes it recursively,
* so load order needs no external sequencing.
*
* Resolution branch order (import): seed word → shell instance; memoized
* record → surface; static registry (shell-own modules, e.g. app-shell) →
* record → exports; static registry (shell-own modules, e.g. app-shell) →
* module; registered factory → materialize; graph row → load + materialize;
* anything else → throw (loud — the runtime mirror of the
* build-time bundle purity gate). The synchronous `require` handed to
@@ -149,13 +149,13 @@ export interface ClientPluginHandoff {
id: string
/**
* Closure factory holding the whole bundle body: receives the synchronous
* require bound to the module table and returns the bundle's export
* surface. Runs once, at materialization.
* require bound to the module table and returns the bundle's exports. Runs
* once, at materialization.
*/
factory: (require: (spec: string) => unknown) => Record<string, unknown>
}
/** Window surface of the web boot protocol: the host-injected graph, the registration sink, and the kernel handoff slot. */
/** Window API of the web boot protocol: the host-injected graph, registration sink, and kernel handoff slot. */
export interface DshWindow {
/** Host-composed entry graph, injected before the shell bundle runs; wire-boundary raw until {@link parseBootManifest}. */
__DSH_BOOT__?: unknown
@@ -174,8 +174,8 @@ export interface DshWindow {
export interface ClientModuleRecord {
/** Module id (entry name / package name). */
id: string
/** The materialized export surface (factory `module.exports`, or the shell module for static registrations). */
surface: unknown
/** Materialized exports (`module.exports` from a factory, or a statically registered shell module). */
exports: unknown
/** Owned `<style data-plugin>` tag ids (`data-plugin-css` values) injected during materialization. */
styles: string[]
/** Observed `require()` edges (module-graph boundary; only table words can appear today). */
@@ -190,7 +190,7 @@ export interface ClientModuleRecord {
export interface ClientModuleLoader {
/** Discriminant against Node's internal loader shapes ('v1'/'v2'). */
version: 'client'
/** Materialized-module registry: id → record. The governance-side read face for entry export surfaces. */
/** Materialized-module registry: id → record. The governance-side read API for entry exports. */
loadCache: Map<string, ClientModuleRecord>
/**
* Internal contract consumed by the vendored Loader's `tree.import`. Resolves
@@ -199,7 +199,7 @@ export interface ClientModuleLoader {
* @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 loader contract).
* @returns the module's export surface.
* @returns the module's exports.
*/
import(specifier: string, parentURL: string, attrs: Record<string, unknown>): Promise<unknown>
/**

View File

@@ -28,7 +28,7 @@ const defaultLoadBundle = (url: string): Promise<void> => new Promise((resolve,
/**
* A plugin bundle IS its package's client half: `<id>/client` (the exports
* subpath external bundles emit) and the bare graph id name the same
* surface, so table lookups normalize the suffix away.
* exports, so table lookups normalize the suffix away.
*/
const stripClientSuffix = (spec: string): string =>
spec.endsWith('/client') ? spec.slice(0, -'/client'.length) : spec
@@ -123,8 +123,8 @@ export class ClientModuleSystem implements ClientModuleLoader {
this.materializing.add(id)
try {
const edges = new Set<string>()
const surface = registered(this.makeRequire(edges))
const record: ClientModuleRecord = { id, surface, styles: claimStyles(id), edges }
const exports = registered(this.makeRequire(edges))
const record: ClientModuleRecord = { id, exports, styles: claimStyles(id), edges }
this.loadCache.set(id, record)
return record
} finally {
@@ -146,8 +146,8 @@ export class ClientModuleSystem implements ClientModuleLoader {
if (this.statics.has(spec)) return this.statics.get(spec)
const id = stripClientSuffix(spec)
const record = this.loadCache.get(id)
if (record !== undefined) return record.surface
if (this.factories.has(id)) return this.materialize(id).surface
if (record !== undefined) return record.exports
if (this.factories.has(id)) return this.materialize(id).exports
throw new Error(
`client-modules: require("${spec}") missed the module table — not a platform seed word, not a shell-own module, `
+ 'and no registered factory (a build-time externals drift, or a forbidden cross-plugin value import)',
@@ -158,11 +158,11 @@ export class ClientModuleSystem implements ClientModuleLoader {
async import(specifier: string): Promise<unknown> {
if (this.seed.has(specifier)) return this.seed.get(specifier)
const existing = this.loadCache.get(specifier)
if (existing !== undefined) return existing.surface
if (existing !== undefined) return existing.exports
if (this.statics.has(specifier)) {
const surface = this.statics.get(specifier)
this.loadCache.set(specifier, { id: specifier, surface, styles: [], edges: new Set() })
return surface
const exports = this.statics.get(specifier)
this.loadCache.set(specifier, { id: specifier, exports, styles: [], edges: new Set() })
return exports
}
if (!this.factories.has(specifier)) {
const row = this.graphRows.get(specifier)
@@ -174,7 +174,7 @@ export class ClientModuleSystem implements ClientModuleLoader {
}
await this.arrive(row)
}
return this.materialize(specifier).surface
return this.materialize(specifier).exports
}
registerStatic(id: string, module: unknown): void {

View File

@@ -70,7 +70,7 @@ describe('lazy CJS arrival', () => {
expect(b.loader.loadCache.size).toBe(0)
})
it('import materializes once and memoizes the export surface', async () => {
it('import materializes once and memoizes the exports', async () => {
const ran: string[] = []
const b = bench([row('a')], { a: () => { ran.push('a'); return { marker: 'a' } } })
const first = await b.loader.import('a', '', {})
@@ -83,8 +83,8 @@ describe('lazy CJS arrival', () => {
it('import without prefetch loads, registers, and materializes in one call', async () => {
const b = bench([row('a')], { a: () => ({ marker: 'direct' }) })
const surface = await b.loader.import('a', '', {})
expect((surface as { marker: string }).marker).toBe('direct')
const exports = await b.loader.import('a', '', {})
expect((exports as { marker: string }).marker).toBe('direct')
expect(b.fetched).toHaveLength(1)
})
@@ -123,8 +123,8 @@ describe('require resolution', () => {
})
await b.loader.prefetch('a')
await b.loader.prefetch('b')
const surface = await b.loader.import('a', '', {})
expect((surface as { got: string }).got).toBe('from-b')
const exports = await b.loader.import('a', '', {})
expect((exports as { got: string }).got).toBe('from-b')
expect(order).toEqual(['a', 'b'])
expect(b.loader.loadCache.get('a')?.edges.has('b/client')).toBe(true)
expect(b.loader.loadCache.has('b')).toBe(true)
@@ -135,8 +135,8 @@ describe('require resolution', () => {
const b = bench([row('a')], {
a: req => ({ dep: req('react') }),
}, { seed: { react } })
const surface = await b.loader.import('a', '', {})
expect((surface as { dep: unknown }).dep).toBe(react)
const exports = await b.loader.import('a', '', {})
expect((exports as { dep: unknown }).dep).toBe(react)
expect(await b.loader.import('react', '', {})).toBe(react)
expect(b.loader.loadCache.has('react')).toBe(false)
})
@@ -284,8 +284,8 @@ describe('default transport seam', () => {
})
})
const loader: ClientModuleLoader = new ClientModuleSystem({ modules: [row('dee')], staticModules: {} })
const surface = await loader.import('dee', '', {})
expect((surface as { marker: string }).marker).toBe('via-script')
const exports = await loader.import('dee', '', {})
expect((exports as { marker: string }).marker).toBe('via-script')
expect(append).toHaveBeenCalledOnce()
expect([...document.querySelectorAll('script')]).toEqual([])
})

View File

@@ -18,7 +18,7 @@ import type {
} from '@deepseek-ai/dsh-client-ui-slots'
// Store contract types are ui-slots authority; re-exported beside the engine
// so store consumers get one import surface.
// so store consumers get one import path.
export type {
ActionsDecl, BakedActions, BoundActions, StoreFactory, StoreHandle, StoreInstance, StoreSpec,
} from '@deepseek-ai/dsh-client-ui-slots'
@@ -165,7 +165,7 @@ function deepFreeze(value: unknown): void {
/** A live engine instance: the contract instance plus the raw engine store. */
export interface EngineStoreInstance<T, A extends ActionsDecl<T>> extends StoreInstance<T, A> {
/** The underlying engine store (framework/test surface; components never see it). */
/** The underlying engine store (framework/test API; components never see it). */
readonly store: SnapshotStore<T>
}

View File

@@ -243,7 +243,7 @@ export function apply(ctx: Context): void {
workspaces.handleHostEnvelope(envelope)
// Typed-event bridge: the session layer ignores registry frames (no
// session routing); consumers (command directory caches, the settings
// and model surfaces) subscribe on ctx.
// and model services) subscribe on ctx.
const frame = envelope.payload
if (frame.type === 'host/commands-changed') ctx.emit('commands/changed')
else if (frame.type === 'host/session-preset-changed') {

View File

@@ -430,7 +430,7 @@ export class SessionManager {
}
}
// ---- List surface ----
// ---- List API ----
/** Full refresh via session.list (single-flight: an in-flight call is reused). */
refreshList(): Promise<void> {
@@ -629,7 +629,7 @@ export class SessionManager {
this.notifier.markDirty()
}
// ---- Subscription surface (for useSessionList) ----
// ---- Subscription API (for useSessionList) ----
/**
* uSES subscription entry for useSessionList.

View File

@@ -60,7 +60,7 @@ export interface SessionOptions {
* remaining public members are manager/runtime entry points.
*/
export class Session implements SessionFace {
// ---- Window and derived state (all private; the snapshot is the only read surface) ----
// ---- Window and derived state (all private; the snapshot is the only read API) ----
private events: SessionEvent[] = []
/** Wire views aligned with `events` by index (envelope-level annotations; undefined = no view).
* Kept parallel rather than merged so `events` stays the raw log slice (model-visible ⟺ logged). */
@@ -428,7 +428,7 @@ export class Session implements SessionFace {
await this.open()
}
// ---- Subscription surface (useSyncExternalStore direct wiring) ----
// ---- Subscription API (useSyncExternalStore direct wiring) ----
/**
* uSES subscription entry.

View File

@@ -25,7 +25,7 @@ const CSS_VIRTUAL_PREFIX = '\0dsh-css:'
const CSS_VIRTUAL_SUFFIX = '.mjs'
/**
* Wire/type layers a client bundle may inline: browser-safe contract surfaces
* Wire/type layers a client bundle may inline: browser-safe contracts
* with no runtime identity to share (no Symbol/instanceof/singleton state).
* Everything else under @deepseek-ai/* is either a module-table entry
* (external) or a leak the purity gate rejects.

View File

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

View File

@@ -40,7 +40,7 @@ The composer bar declares session-scoped single seats for `'conversation.input.p
The chat stats line takes its token accounting from the generic token-meter `tokenUsage` projection read through the standard-kit `useProjection`: billed input is uncached input plus cache reads and writes; cache hit divides cache reads by that total. Visible nodes supply only the turn and step counts plus the LLM and tool wall times, which are window-scoped facts about what is on screen rather than accounting; durable token and context groups remain visible when compaction leaves no assistant node in the loaded window. The same window fold averages each recorded step's TTFT and divides sampled output tokens by their summed decode spans into a latency/throughput group localized through the `conversation` locale namespace (`TTFT avg … · … tok/s` in English); a step missing a timing boundary or a usage sample drops out of those figures instead of skewing them. The turn-count, step-count, duration, cache, and token labels use the same namespace. Each settled turn additionally appends hover-revealed `TTFT {s}s · {tps} tok/s` labels to its assistant footer after the `Ran for` duration — the turn's first-step TTFT and its turn-aggregate decode throughput — gated on the turn's timing being in the loaded window (a contiguous log suffix, so an in-window turn carries every one of its steps) and omitting whichever figure is unrecorded. A deployment without token-meter drops the token groups; when the line overflows, it elides with an ellipsis and a delayed hover tooltip carries the full text only while actually clipped. Context occupancy renders as the composer's trailing ContextMeter: a 14px occupancy ring after the model seat, fed by `contextPressure` and rendered only once both a numerator and a route capacity are known, that click-opens a panel pairing the `percent used` header and `~used / capacity` figures with a color-segmented bar and `~`-prefixed heuristic composition rows (system prompt, tools, messages) from the `contextBreakdown` projection. The ring and header read `projectedTokens` — the provider sample carried forward over the surface's movement since — so a compaction registers immediately instead of after a further turn; the composition rows stay wholly heuristic and therefore still do not sum to the header ([rationale](../../llm/token-meter/README.md)). Occupancy is deliberately an approximation: numerator and capacity are independent last-wins projection fields, not one atomic request observation.
`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.
`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` exports contain only loader entries, service classes, and contract types; components and store factories reach the page through slot registrations.
A finished turn materializes one ordered `turn-tail` Conversation Node. Its engine-owned `TurnLocation` supplies the closing Assistant and Turn data; the renderer places the `conversation.chat.turnTail` chain before that node's IconActions and dispatches `TurnTailOwnerProps` containing the Turn, closing seq, and `openFile`. This package owns only the hole; `@deepseek-ai/dsh-client-ui-deliverables` accumulates mutation-tool `locations` into Turn data and owns the produced-files row, chip cap, and copy, 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.

View File

@@ -122,7 +122,7 @@ export class ConversationService extends Service implements IConversation {
/**
* Send a prompt into the scoped session. Business failures also land in the
* session snapshot's promptError (object-layer surface); the rejection here
* session snapshot's promptError (object-layer state); the rejection here
* exists for caller choreography (the composer restores the draft on it).
* @param text - prompt text, sent verbatim as one text block.
*/

View File

@@ -1,15 +1,15 @@
// @vitest-environment jsdom
// apply inject factories exercised end to end against the terminal thin
// shape: the strict session surface (views triple, draft mirror), the
// API: the strict session API (views triple, draft mirror), the
// provide-channel input face (machine-sink submit choreography incl.
// optimistic clear + failure restore), the resident surface (selectWorkspace
// optimistic clear + failure restore), the resident API (selectWorkspace
// draft carrying), the composer-bar stop face, openDetails = select action +
// layout orchestration, and the closeDetails details surface. Complements
// layout orchestration, and the closeDetails details API. Complements
// chat-apply.spec.tsx (registration) and selection-survival.spec.tsx (store
// axis). History opening is NOT an inject concern — the runtime sessions
// service opens on watch (sessions-service.spec.ts owns that behavior).
//
// The inject surfaces are read off the ledger entries deliberately (typed at
// The inject APIs are read off the ledger entries deliberately (typed at
// this spec's own contract): these cases pin factory choreography the UI
// guards would mask. Rendering-path acceptance lives in
// chat-toolview-slot.spec.tsx.
@@ -75,30 +75,30 @@ async function bench() {
const entryOf = (key: 'conversation' | 'conversation.session' | 'conversation.session.header' | 'conversation.composer.bar' | 'conversation.view' | 'details') =>
runtime.slots.entries(key)[0]!
/** Resolve store instance + call the inject the way the outlet would. */
const conversationSurface = (id: SessionId) => {
const conversationApi = (id: SessionId) => {
const entry = entryOf('conversation.session')
const instance = runtime.storeOf('conversation.session', id) as ChatInstance
const injected = (entry.inject as unknown as (sessionId: SessionId, actions: ChatActions) => ConversationSessionInjected)(
id, instance.actions)
return { instance, injected }
}
const conversationHeaderSurface = (id: SessionId) => {
const conversationHeaderApi = (id: SessionId) => {
const entry = entryOf('conversation.session.header')
const instance = runtime.storeOf('conversation.session.header', id) as ChatInstance
const injected = (entry.inject as unknown as (sessionId: SessionId, actions: ChatActions) => ConversationSessionHeaderInjected)(
id, instance.actions)
return { instance, injected }
}
const residentSurface = (id: SessionId | undefined) => {
const residentApi = (id: SessionId | undefined) => {
const entry = entryOf('conversation')
return (entry.inject as unknown as (sessionId: SessionId | undefined) => ConversationInjected)(id)
}
const composerSurface = (id: SessionId | undefined) => {
const composerApi = (id: SessionId | undefined) => {
const entry = entryOf('conversation.composer.bar')
return (entry.inject as unknown as (sessionId: SessionId | undefined) => ComposerBarInjected)(id)
}
/** Same resolution for the chat entry riding the view ring. */
const chatViewSurface = (id: SessionId) => {
const chatViewApi = (id: SessionId) => {
const entry = entryOf('conversation.view')
const instance = runtime.storeOf('conversation.view', id) as ChatInstance
const injected = (entry.inject as unknown as (sessionId: SessionId, actions: ChatActions) => ChatViewInjected)(
@@ -106,7 +106,7 @@ async function bench() {
return { instance, injected }
}
/** Materialize the input provide contribution the way the runtime does. */
const inputSurface = (id: SessionId) => {
const inputApi = (id: SessionId) => {
const info = runtime.sessions.provideInfo(id)!
const state = info.hooks['input'] as {
getSnapshot: () => { draft: string }
@@ -120,21 +120,21 @@ async function bench() {
}
return {
runtime, feature, slots: runtime.slots, entryOf,
conversationSurface, conversationHeaderSurface, residentSurface, composerSurface, chatViewSurface, inputSurface,
conversationApi, conversationHeaderApi, residentApi, composerApi, chatViewApi, inputApi,
sessionFake, layoutFake,
}
}
describe('conversation slot inject surface', () => {
it('assembles the thin surface side-effect-free', async () => {
describe('conversation slot inject API', () => {
it('assembles the thin API side-effect-free', async () => {
const b = await bench()
const { injected } = b.conversationSurface(ROOT)
const { injected } = b.conversationApi(ROOT)
// Assembly has no session side effects: opening the event window belongs
// to the runtime watch path, not the inject factory.
expect(b.sessionFake.open).not.toHaveBeenCalled()
expect(injected.views.list().map(v => v.id)).toEqual(['chat'])
const chatView = b.chatViewSurface(ROOT)
const chatView = b.chatViewApi(ROOT)
chatView.injected.loadOlder()
expect(b.sessionFake.loadOlder).toHaveBeenCalledTimes(1)
chatView.injected.forkAt(17)
@@ -149,8 +149,8 @@ describe('conversation slot inject surface', () => {
it('the provide-channel input face submits through the machine sink: trim, optimistic clear, failure restore without clobber', async () => {
const b = await bench()
const { injected } = b.conversationSurface(ROOT)
const { state, actions } = b.inputSurface(ROOT)
const { injected } = b.conversationApi(ROOT)
const { state, actions } = b.inputApi(ROOT)
// Whitespace-only: the machine treats it as empty — no prompt, draft kept.
actions.setDraft(' ')
actions.submit()
@@ -176,16 +176,16 @@ describe('conversation slot inject surface', () => {
await new Promise(r => setTimeout(r, 0))
expect(state.getSnapshot().draft).toBe('typed during flight')
// The provide contribution is idempotent per session: one shell identity.
expect(b.inputSurface(ROOT).state).toBe(state)
expect(b.inputApi(ROOT).state).toBe(state)
// The draft mirror rides the conversation inject face.
const mirrored: string[] = []
const unbind = injected.bindDraftMirror(text => mirrored.push(text))
actions.setDraft('mirrored text')
expect(mirrored).toEqual(['mirrored text'])
unbind()
// Stop failure is swallowed (promptError owns the surface).
// Stop failure is swallowed (promptError owns the display).
b.sessionFake.cancel.mockResolvedValueOnce({ ok: false, error: { code: 'internal', message: 'x', details: {} } })
b.composerSurface(ROOT).stop!()
b.composerApi(ROOT).stop!()
await new Promise(r => setTimeout(r, 0))
expect(b.sessionFake.cancel).toHaveBeenCalledTimes(1)
await b.runtime.dispose()
@@ -216,20 +216,20 @@ describe('conversation slot inject surface', () => {
it('openDetails (chat view face) writes the selection through the store actions and opens the panel', async () => {
const b = await bench()
const { instance, injected } = b.chatViewSurface(ROOT)
const { instance, injected } = b.chatViewApi(ROOT)
injected.openDetails({ turnSeq: 2, callId: 'c1' })
expect(instance.store.getSnapshot().selection).toEqual({ turnSeq: 2, callId: 'c1' })
expect(b.layoutFake.openDetails).toHaveBeenCalledTimes(1)
// The chat view shares the conversation entry's store instance: selection
// writes land where the skeleton and details read.
const conv = b.conversationSurface(ROOT)
const conv = b.conversationApi(ROOT)
expect(conv.instance).toBe(instance)
await b.runtime.dispose()
})
it('openFile (chat view face) resolves against session cwd and calls workspaces.openPath', async () => {
const b = await bench()
const { injected } = b.chatViewSurface(ROOT)
const { injected } = b.chatViewApi(ROOT)
injected.openFile('src/a.ts')
await vi.waitFor(() => {
expect(b.runtime.workspaces.calls).toContainEqual({ method: 'openPath', args: ['/proj/src/a.ts'] })
@@ -239,11 +239,11 @@ describe('conversation slot inject surface', () => {
it('routes workspace switching through the runtime owner, carrying the draft', async () => {
const b = await bench()
const resident = b.residentSurface(ROOT)
const resident = b.residentApi(ROOT)
// Same-session connect (the picked workspace resolves to this session):
// no draft movement, plain re-open.
b.runtime.workspaces.stub('connectWorkspace', () => Promise.resolve(ROOT))
const { state, actions } = b.inputSurface(ROOT)
const { state, actions } = b.inputApi(ROOT)
actions.setDraft('carry me')
void resident.selectWorkspace('workspace-1' as never)
await vi.waitFor(() => {
@@ -261,7 +261,7 @@ describe('conversation slot inject surface', () => {
expect(b.runtime.sessions.calls).toContainEqual({ method: 'open', args: [OTHER] })
})
expect(state.getSnapshot().draft).toBe('')
expect(b.inputSurface(OTHER).state.getSnapshot().draft).toBe('carry me')
expect(b.inputApi(OTHER).state.getSnapshot().draft).toBe('carry me')
await b.runtime.dispose()
})
@@ -269,7 +269,7 @@ describe('conversation slot inject surface', () => {
const b = await bench()
// No-session resident (hero before any session): connect resolves and
// navigation proceeds without any draft choreography.
const noSession = b.residentSurface(undefined)
const noSession = b.residentApi(undefined)
b.runtime.workspaces.stub('connectWorkspace', () => Promise.resolve(ROOT))
void noSession.selectWorkspace('workspace-0' as never)
await vi.waitFor(() => {
@@ -279,15 +279,15 @@ describe('conversation slot inject surface', () => {
// Cross-session connect with an EMPTY draft: no move, no clearing.
const OTHER = 'b9-other' as SessionId
await b.runtime.sessions.add({ id: OTHER }, { current: false })
const resident = b.residentSurface(ROOT)
const { state } = b.inputSurface(ROOT)
const resident = b.residentApi(ROOT)
const { state } = b.inputApi(ROOT)
expect(state.getSnapshot().draft).toBe('')
b.runtime.workspaces.stub('connectWorkspace', () => Promise.resolve(OTHER))
void resident.selectWorkspace('workspace-3' as never)
await vi.waitFor(() => {
expect(b.runtime.sessions.calls).toContainEqual({ method: 'open', args: [OTHER] })
})
expect(b.inputSurface(OTHER).state.getSnapshot().draft).toBe('')
expect(b.inputApi(OTHER).state.getSnapshot().draft).toBe('')
// Connect failure: the rejection propagates to the caller (the view owns
// the rollback) and no further navigation happens.
@@ -310,7 +310,7 @@ describe('conversation slot inject surface', () => {
it('views read face projects the ring ledger (subscribe/version through ctx.slots)', async () => {
const b = await bench()
const { injected } = b.conversationSurface(ROOT)
const { injected } = b.conversationApi(ROOT)
const before = injected.views.version()
const listener = vi.fn()
const unsub = injected.views.subscribe(listener)
@@ -332,7 +332,7 @@ describe('conversation slot inject surface', () => {
})
})
describe('details inject surface', () => {
describe('details inject API', () => {
it('details injects the one layout callback; selection rides the shared store instead', async () => {
const b = await bench()
const entry = b.entryOf('details')

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-goal/README.md
README.md: a53fb3a89eaee364cb025ca728ca42ce934887b0
README.md: f0446aa0637bc181f7fdc22e5d0d3192e0ac20cf
README.zh.md: 1ad9f50aee5b103f6455e4d4b7d29fa9eb29a108

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Goal surface plugin, browser half: the `GoalBar` strip is the second standalone card in the `conversation.input.dock` composer-context stack (order 10, after Todo and before Queue). The live goal arrives through `useProjection('goal')` — the host-computed whole value seeded by the history tail page and updated by `session/projection` frames — so the plugin owns no domain store, refresh chain, or event listener. The slot inject face carries only the four mutation verbs (edit / pause / resume / clear through `ctx.remote.goals` — an active goal offers the pause action, a paused one resume); each reads the CAS ref from the session's current projected value at call time and surfaces the rejected Remote error inline. The strip single-flights mutations synchronously because React's pending render cannot fence same-frame clicks; after a successful clear it immediately suppresses that exact goal id while the authoritative null projection catches up. Goal creation stays on the `/goal` host command; loading, absent, completed, and successfully cleared goals render nothing.
The `/client` export surface is the plugin body (`apply`/`inject`), the `GoalBar`/`GoalDock` components, and the injected verb face types.
The `/client` exports are the plugin body (`apply`/`inject`), the `GoalBar`/`GoalDock` components, and the injected verb face types.
## Model Experience

View File

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

View File

@@ -6,7 +6,7 @@ Shell plugin: three-column AppFrame (drag handles and concession chain) plus the
AppFrame always mounts the conversation and details columns; a connected Session renders through `SessionProvider`. The transient layout store starts the sidebar at its default width and details closed, and it never reads or writes `localStorage`. Hero and other unselected states also derive a zero rendered details width without changing that stored preference. AppFrame retains the last non-blank Session id across those states: the first Session remains closed, an explicit details action opens the contract default width, returning to the same Session restores its unchanged width, and selecting a different Session closes details before paint. The conversation owner share is empty, while the sidebar owner share contains only `collapsed` and `width`; registrants obtain business data from standard hooks and actions from their own inject faces.
The `/client` export surface is the plugin body (`apply`/`inject`), `LayoutService`, and the four owner-share interfaces. AppFrame, the panel store, and the concession solver remain package-internal.
The `/client` exports are the plugin body (`apply`/`inject`), `LayoutService`, and the four owner-share interfaces. AppFrame, the panel store, and the concession solver remain package-internal.
## Model Experience

View File

@@ -15,7 +15,7 @@ import { createLayoutStore } from './stores.ts'
import { LayoutService } from './service.ts'
import { ThemePresenter } from './theme-presenter.ts'
// Contract surface only (export-convergence rule: cross-package consumers
// Contract exports only (export-convergence rule: cross-package consumers
// keep a symbol exported; test-only/package-internal symbols live off /src).
// ILayout: the ctx.layout face consumers and test fakes type against.
// OwnerShare contracts below are the render-side halves registrants compose
@@ -64,7 +64,7 @@ export interface ConvOwnerProps {}
/** Details owner share: empty — sessionId arrives as a framework-standard prop. */
export interface DetailsOwnerProps {}
/** Required services (cordis fiber inject — the loader passes the whole export surface as an object plugin). */
/** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
export const inject = ['slots', 'theme']
/**

View File

@@ -3,7 +3,7 @@
// ONE register() call declares the three child slots + seats the store factory
// + wires the panel actions through the inject hook; teardown cascades
// (service unprovided + declarations gone + registration cleared). Node half
// and the invariant companion ride along — one-line surfaces the aggregate
// and the invariant companion ride along — one line exposes the aggregate
// coverage gate still requires exercised.
import { Context } from '@deepseek-ai/cordis'
@@ -111,7 +111,7 @@ describe('node half + invariant companion', () => {
const register = vi.fn().mockReturnValue(() => {})
const ctx = { invariants: { register } } as never
// The /invariant subpath types live in lib/types (build product); assert
// the surface so the call stays typed where lint runs without a build.
// the API so the call stays typed where lint runs without a build.
const dispose = await (invariant as { apply: (ctx: never) => Promise<() => void> }).apply(ctx)
expect(register).toHaveBeenCalledWith('@deepseek-ai/dsh-client-ui-layout', expect.any(Function))
// The installer is the declared no-op — calling it must not throw.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-model/README.md
README.md: 519429834f214fcb82eeb692378fb79770fa30be
README.md: fdc3258eb37b45f4773648d2d796c275f8657d47
README.zh.md: 116e151d1afeaaa22618c408eed2c7542d1357e7

View File

@@ -10,7 +10,7 @@ When the Host reports that no adapter serves the session's route (`session.model
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.
The `/client` exports are the plugin body (`apply`/`inject`), `ModelService`, `ModelDirectory` with its state fields, and the seat's injected face type.
## Model Experience

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-permission/README.md
README.md: 742e82d767152073ab963dc74c0565d6e8f8e5c4
README.md: cf3981d66745bd5a65c83e09daeb8463023f447b
README.zh.md: 70bbbb2d14358cbe52a6fc27deb7ce01d5f3679b

View File

@@ -6,7 +6,7 @@ Permission browser surfaces for two different lifetimes. The General-settings ro
The current-session surface remains a popupSelect DECORATION hung on the host `/permission` command (`ctx.command.decorate`). A decoration is not a second command — the host command keeps its slash-menu row, the argued path (`/permission <preset>` switches directly), and the durable lifecycle logging; the decoration replaces only the bare invocation with the picker: one flat preset list with the current value marked active and kebab-case preset names rendered as title-case labels (`workspace-write``Workspace Write`, the composer chip's display transform twin), where a pick submits the `/permission <preset>` command line. Options and the active mark read the session's `permissions` projection (the same host-computed select the composer chip renders), so both current-session surfaces share one read source and one write path, and the pushed projection frame is the single confirmation both follow. The decoration is available exactly while the projection key is present; a permission-less composition shows neither picker nor Settings row.
The `/client` export surface is the plugin body (`apply`/`inject`).
The `/client` exports are the plugin body (`apply`/`inject`).
## Model Experience

View File

@@ -15,7 +15,7 @@ export const name = 'client-ui-primitives-invariant'
export const inject = ['invariants']
/**
* No runtime invariant: pure props-in React atoms with zero cordis surface
* No runtime invariant: pure props-in React atoms with no Cordis API
* no events, no services, no mutable cross-plugin state; rendering contracts
* are asserted directly by this package's component specs.
*/

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-sidebar/README.md
README.md: 45ae267d98b17bbc612cf932f5b95b42ba6ff4bf
README.md: 4eb9eeb73f1f8398eb9d16434996840182ba79a9
README.zh.md: a9fb927305d0bab5fb4d27adbfdbec90dfa1dd6d

View File

@@ -12,7 +12,7 @@ Scrollbars in the column are a pointer affordance: the shell rebinds ui-theme's
The foot is the `sidebar.settings` seat: the sidebar renders only the bottom-pinned layout slot and shares its column state (`wide`); ui-settings registers the trigger row and settings panel there.
The `/client` export surface is the plugin body (`apply`/`inject`) plus the contract types only; SidebarRoot, the row components, and the tree derivation remain package-internal behind the slot registration.
The `/client` exports are the plugin body (`apply`/`inject`) plus the contract types only; SidebarRoot, the row components, and the tree derivation remain package-internal behind the slot registration.
## Model Experience

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-skill/README.md
README.md: 36b4cf4181d74ca1ea05fd8ed2db5e42fa36c7f2
README.md: 0456db4de9453e5060e39b5f061422486e44dfc9
README.zh.md: 336f43117e7bc4de41a31e636ee0966e5d1a2cd6

View File

@@ -8,7 +8,7 @@ A pick lands the literal `/name ` text and the prompt ships the same literal ([s
A failed `skill.list` throws from `candidates`, which the slash shell logs and folds into a silent menu-group drop — the menu shows only pending/ready states.
The `/client` export surface is the plugin body (`apply`/`inject`) only; the source object is internal to the registration effect.
The `/client` exports are the plugin body (`apply`/`inject`) only; the source object is internal to the registration effect.
## Skill tool row

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-slash/README.md
README.md: 3f97351bea04dbc268e6acc105be10e1e4e0a4b5
README.md: e6c9c3240d03a0cde57eda0bce2a995f91786f4e
README.zh.md: cf9e42d419cb472509a74ba227990b1502b38a75

View File

@@ -8,7 +8,7 @@ Layering: `src/core/` is the pure core — `detectTrigger`, `menuReduce`/`seedGr
MenuView renders the menu store into the `conversation.input.overlay` slot (list kind, session scope) and renders null while closed. Typed triggers seed every source registered for that trigger; a programmatic launcher seeds only its requested source and publishes the source name through the controller's `launcher` snapshot store until the menu closes or typed tracking resumes. Groups sort by the optional `SlashSource.order` (lower first, default 0, ties keep registration order) under title rows localized through the `slash.menu` locale namespace (an unknown source shows its raw name); the list height clamps to the space above the composer, and a pointer down outside both the menu and the surrounding composer card dismisses it. The slot is owned by ui-conversation's composer entry (anchor, children declaration, lifecycle); its SlotMap type merge lives in this package's `src/client/slots.ts` because the dependency direction (ui-conversation → ui-slash) admits no reverse type import. Combobox pattern: focus stays in the textarea, rows pick on mousedown, the highlight rides `aria-activedescendant`.
The `/client` export surface is the plugin body (`apply`/`inject`), `SlashService`, `MenuViewInjected`, and the contract types. MenuView itself is internal — the slot registration closes over it.
The `/client` exports are the plugin body (`apply`/`inject`), `SlashService`, `MenuViewInjected`, and the contract types. MenuView itself is internal — the slot registration closes over it.
## Model Experience

View File

@@ -27,7 +27,7 @@ export interface LocaleFace extends HostObservable<{ revision: number }> {
bind(ns: string): Translate
}
/** Minimal observable surface for host-provided standard-kit data sources. */
/** Minimal observable API for host-provided standard-kit data sources. */
export interface HostObservable<T> {
getSnapshot(): T
subscribe(fn: () => void): () => void
@@ -97,7 +97,7 @@ export interface RenderOpts {
hookContext?: unknown
}
/** Host surface the runtime SlotsService presents to the installed renderer. */
/** Host API the runtime SlotsService presents to the installed renderer. */
export interface SlotRendererHost {
/**
* Subscribe to a key's registration changes (microtask-batched).
@@ -167,8 +167,8 @@ export interface SlotRendererHost {
/** 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).
* @param host - the installing service's host surface.
* Render the root slot tree over the host API (the only ctx-level entry).
* @param host - the installing service's host API.
* @param ownerProps - owner props from the shell's renderSlot('root', ...) call.
* @returns the rendered tree.
*/

View File

@@ -1,6 +1,6 @@
// SlotCore terminal-design behavior: the single register composition API —
// a-priori 'root', children declaration/authorization, load-time validation,
// one-axis lifecycle cascade, store scope pinning, subscription surface.
// one-axis lifecycle cascade, store scope pinning, subscription API.
import { describe, expect, it, vi } from 'vitest'
import type { SlotComponent, StoreHandle } from '@deepseek-ai/dsh-client-ui-slots'
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
@@ -230,7 +230,7 @@ describe('store scope pinning', () => {
})
})
describe('subscription surface', () => {
describe('subscription API', () => {
it('tracks declaration epochs separately from ordinary entry mutations', () => {
const core = new SlotCore()
expect(core.declarationEpoch('root')).toBe(1)

View File

@@ -5,8 +5,8 @@ import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
'surface.a': { kind: 'single'; scope: 'root' }
'surface.b': { kind: 'single'; scope: 'root' }
'dynamic.a': { kind: 'single'; scope: 'root' }
'dynamic.b': { kind: 'single'; scope: 'root' }
'surface.injected': { kind: 'single'; scope: 'root'; inject: { token: string } }
}
}
@@ -16,17 +16,17 @@ const Comp: SlotComponent<object> = () => null
describe('dynamic-key escape hatch', () => {
it('specDynamic reads wide-typed specs for string keys; undefined while undeclared', () => {
const core = new SlotCore()
expect(core.specDynamic('surface.a')).toBeUndefined()
core.register({ name: 'root', children: { 'surface.a': { kind: 'single', scope: 'root' } } }, Comp as never)
expect(core.specDynamic('surface.a')).toEqual({ kind: 'single', scope: 'root' })
expect(core.specDynamic('dynamic.a')).toBeUndefined()
core.register({ name: 'root', children: { 'dynamic.a': { kind: 'single', scope: 'root' } } }, Comp as never)
expect(core.specDynamic('dynamic.a')).toEqual({ kind: 'single', scope: 'root' })
expect(core.specDynamic('never.declared')).toBeUndefined()
})
it('spec() narrows by SlotMap key', () => {
const core = new SlotCore()
core.register({ name: 'root', children: { 'surface.a': { kind: 'single', scope: 'root' } } }, Comp as never)
expect(core.spec('surface.a')).toEqual({ kind: 'single', scope: 'root' })
expect(core.spec('surface.b')).toBeUndefined()
core.register({ name: 'root', children: { 'dynamic.a': { kind: 'single', scope: 'root' } } }, Comp as never)
expect(core.spec('dynamic.a')).toEqual({ kind: 'single', scope: 'root' })
expect(core.spec('dynamic.b')).toBeUndefined()
})
it('records the parent-declared Slot inject on the runtime spec', () => {
@@ -41,9 +41,9 @@ describe('dynamic-key escape hatch', () => {
it('entries/getVersion on an untouched key return the frozen empty array and 0', () => {
const core = new SlotCore()
expect(core.entries('surface.b')).toHaveLength(0)
expect(core.entries('surface.b')).toBe(core.entries('surface.b'))
expect(core.getVersion('surface.b')).toBe(0)
expect(core.entries('dynamic.b')).toHaveLength(0)
expect(core.entries('dynamic.b')).toBe(core.entries('dynamic.b'))
expect(core.getVersion('dynamic.b')).toBe(0)
})
it('isLive is false for entries the core never held', () => {

View File

@@ -103,7 +103,7 @@ declare function NarrowTakeover(props: PropsRuntime<'chain.takeover'> & { matche
describe('terminal-design type chain', () => {
it('holds the positive chain and the compile-time negatives', () => {
// Everything below is compile-surface only.
// Everything below is compile-time only.
const samples = (core: SlotCore, chat: ChatHandle, fp: FrameProps, cp: ConvProps, acts: BoundActions<ChatHandle>) => {
// ── positive chain ─────────────────────────────────────────────
// Frame: children + factory store + inject; actions arrive baked.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-theme/README.md
README.md: 7b81728b02c0dadcbd134a4bca20c49721a3dd52
README.md: cab9961a6d703d600a856e71339cda7062d20d62
README.zh.md: 81b64c356749b6ffe12694b218e92eaa483ff739

View File

@@ -20,5 +20,5 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Third-party themes are a surface, not a product** — registering one means overriding same-named alias variables; no validation exists that an override set is complete.
- **Third-party themes are an extension point, not a product** — registering one means overriding same-named alias variables; no validation exists that an override set is complete.
- **The token sheets are the sole color authority** — values absent from cssdesign (for example the design's #4176E6 tab blue) are deliberately not appended; the nearest semantic token wins. Design-owner-approved additions are the exception and enter as a static step plus a semantic alias in the same change (`--dsw-static-blue-900` / `--dsw-alias-label-primary-bluish`).

View File

@@ -2,7 +2,7 @@
/**
* Real tsdown artifact shape: lib/client.js hands off through
* window.__ModuleLoader__.load, resolves externals through the injected
* require, returns the export surface (apply + inject), and a mounted apply
* require, returns the exports (apply + inject), and a mounted apply
* registers the view tab into a real SlotsService ring. Skips when dist/ is
* not built (`pnpm --filter @deepseek-ai/dsh-client-ui-trajectory bundle`).
*/
@@ -52,24 +52,24 @@ describe('tsdown client artifact', () => {
['@deepseek-ai/dsh-client-runtime/client', await import('@deepseek-ai/dsh-client-runtime/client')],
['@deepseek-ai/dsh-client-ui-primitives', await import('@deepseek-ai/dsh-client-ui-primitives')],
])
const surface = handoff!.factory((spec) => {
const exports = handoff!.factory((spec) => {
if (!modules.has(spec)) throw new Error(`unexpected require: ${spec}`)
return modules.get(spec)
})
return { handoff: handoff!, surface }
return { handoff: handoff!, exports }
}
it.skipIf(code === undefined)('hands off with the manifest id and a DI-require factory', async () => {
const { handoff, surface } = await loadArtifact()
const { handoff, exports } = await loadArtifact()
expect(handoff.id).toBe(PLUGIN_ID)
expect(surface.apply).toBeTypeOf('function')
expect(surface.inject).toEqual([
expect(exports.apply).toBeTypeOf('function')
expect(exports.inject).toEqual([
'slots', 'conversationEvents', 'conversationViews', 'sessions', 'locale',
])
})
it.skipIf(code === undefined)('mounted as an object plugin, apply registers the view tab on the real ring', async () => {
const { surface } = await loadArtifact()
const { exports } = await loadArtifact()
const ctx = new Context()
const slots = new SlotsService(ctx)
await ctx.plugin(ConversationEventRegistry).await()
@@ -87,7 +87,7 @@ describe('tsdown client artifact', () => {
ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never)
const locale = await import('@deepseek-ai/dsh-client-locale/client')
ctx.plugin({ inject: [...locale.inject], apply: locale.apply })
const fiber = ctx.plugin(surface as { apply: (ctx: Context) => void })
const fiber = ctx.plugin(exports as { apply: (ctx: Context) => void })
await fiber.await()
const events = ctx.get('conversationEvents') as ConversationEventRegistry
const views = ctx.get('conversationViews') as ConversationViewRegistry

View File

@@ -6,7 +6,7 @@ export { bindSnapshotSelector } from './bind.ts'
/**
* Selector hook over a session's conversation snapshot. Wide (`object`) by
* default inside this dependency-inverted package; runtime narrows it once at
* its export surface (`UseSession<ConversationSnapshot>`) — the snapshot type
* its exports (`UseSession<ConversationSnapshot>`) — the snapshot type
* never flows back into web-react.
*/
export type UseSession<Snap extends object = object> = SnapshotSelectorHook<Snap>

View File

@@ -20,7 +20,7 @@ export const HostContext = createContext<SlotRendererHost | null>(null)
/**
* Read the installed renderer host; throws outside the rendered root tree
* (framework components must not render detached from the renderer).
* @returns the host surface.
* @returns the host API.
*/
export function useHost(): SlotRendererHost {
const host = useContext(HostContext)
@@ -132,7 +132,7 @@ export function SessionMaybeProvider({ children }: { children: ReactNode }) {
)
}
/** SessionProvider surface: render-prop body plus the no-session branch. */
/** SessionProvider API: render-prop body plus the no-session branch. */
export interface SessionProviderProps {
/** No-session body (also covers a current id whose session cannot be resolved). */
empty?: (() => ReactNode) | undefined

View File

@@ -3,7 +3,7 @@
* Integration against the real ui-slots SlotCore through a passthrough host:
* registrations go through the real register() (options form, children
* declaration), and the outlets ride the real subscribe/getVersion/entries/
* isLive surfaces — microtask-batched notifications, mutation-stable entry
* isLive APIs — microtask-batched notifications, mutation-stable entry
* references (the cache axis), and ledger-fed stale bindings are the
* real-core semantics the fake-host suite cannot vouch for.
*/

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/web/README.md
README.md: 48355a046910bd5b78249af7b7781dab6a7c3a60
README.md: 4b44e0e2c2e192363ce32ac2decc218f5ea17bd2
README.zh.md: f0b567569849c698e8a1a29323e7088b49996522

View File

@@ -6,7 +6,7 @@ Web shell kernel: `new AppWebEntry(el, seams?).run()` mounts the whole client th
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.
`PLATFORM_MODULES` (src/platform.ts) is the single source of truth for shared modules: seed-table keys, tsdown client externals, and the Vite alias set are its projections.
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.

View File

@@ -19,7 +19,7 @@ import type { PlatformModule } from './platform.ts'
/**
* Build the static table handed to the module loader at boot.
* @returns module specifier → export-surface entity (one entry per platform word).
* @returns module specifier → exported entity (one entry per platform word).
*/
export function getStaticModules(): Record<string, unknown> {
// The satisfies pin is the projection contract: a word added to