Four rounds of structural rework on the conversation surface, converging on one registration model for the whole client: - Review fixes: open() leaves the inject factory (SessionsService owns the semantic); ConversationService mounts via ctx.plugin(); the bespoke view registry retires into the 'conversation.view' list slot. - Ring alignment: createChatView factory retired (components get everything through checkable shares at the register call site); the hand-rolled t/i18n threading is deleted wholesale — a future framework-level i18n will supply t as a standard prop keyed by slot name, so no interim manual channel. - Toolview dissolution: ToolViewRegistry / ToolViewResolver / ToolViewOutlet / ctx.toolviews retire. Tool rows are entries of the 'conversation.chat.toolview' keyed slot (scope: session) declared by the chat entry; ToolRowOwnerProps is the unified owner payload; GenericToolCard becomes the call-site fallback; registrants are plain plugins (inject ['slots','conversation'] as the load-order seam); session-dimension dispatch moves into components (useSessions reads parentId); trajectory/waterfall gain same-shape slots the day they render tool rows (RendersCheck rejects empty declarations). Slot names mirror the composition path (<domain>.<entry>.<hole>). - Staging follows current: cell()/binding() are pure resolution (render-safe); the constructor subscribes to the list store and followCurrent opens the event window when the current session changes — staging IS the open signal, business verbs are the timing, React render/commit is decoupled from window lifecycle. A masked current (projection gap) keeps the stage untouched so deferred teardown semantics survive reconnects. Agent Note: .agents/notes/implemented/architecture/ 2026-07-23-toolview-dissolution.md (bilingual pair) records the decision, the four rejected alternatives, and the accepted semantic changes; the web client architecture note and packages/client/AGENTS.md carry the current-state narrative. Verified: typecheck 0, duplication 0 clones (478 files), full coverage run 6190 passed with zero threshold errors, knip 0, doc-sync 24/24, client aggregate tsc 0, render-count checks (one commit per chunk, zero row re-renders under streaming) green.
53 lines
2.6 KiB
TypeScript
53 lines
2.6 KiB
TypeScript
/**
|
|
* Chat store factory (slot terminal design §4): selection + draft + active
|
|
* view for one session, shared by the conversation and details registrations
|
|
* (apply constructs one handle and passes it to both). Session-scope
|
|
* derivation: both mount slots are scope=session, so the framework creates
|
|
* one instance per session; the persist key is scope-suffixed by the
|
|
* framework, aligning with the previous per-session draft persistence.
|
|
*
|
|
* Module exports the factory only — a module-level handle would pin identity
|
|
* in the module cache (a de-facto singleton surviving plugin reloads).
|
|
*/
|
|
import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import type { ChatStoreState, SelectionTarget } from './contract/views.ts'
|
|
|
|
/**
|
|
* Annotation twin of the actions literal below (the export needs a declared
|
|
* return type); drift fails assignability at the defineStore call.
|
|
*/
|
|
type ChatActions = {
|
|
select: (draft: ChatStoreState, target: SelectionTarget | null) => void
|
|
setDraft: (draft: ChatStoreState, text: string) => void
|
|
clearDraft: (draft: ChatStoreState) => void
|
|
restoreDraft: (draft: ChatStoreState, text: string) => void
|
|
setView: (draft: ChatStoreState, view: string) => void
|
|
}
|
|
|
|
/**
|
|
* Declare the per-session chat store. `selection` is the details-linkage
|
|
* channel (conversation writes, details reads); `draft` is the composer text
|
|
* (persisted so it survives session switches and reloads); `view` is the
|
|
* active conversation view id (a 'conversation.view' entry id — store seat is
|
|
* the cross-remount survival channel, null falls back to the first view).
|
|
* @returns the store handle (spec + identity + factory in one value).
|
|
*/
|
|
export function createChatStore(): EngineStoreHandle<ChatStoreState, ChatActions> {
|
|
return defineStore({
|
|
// Anchored to the contract shape: consumers read the store through
|
|
// PropsStore<ChatStore>'s SnapshotSelectorHook<ChatStoreState>, so init
|
|
// and the contract cannot drift.
|
|
init: (): ChatStoreState => ({ selection: null, draft: '', view: null }),
|
|
persist: 'dsh.conversation.chat',
|
|
actions: {
|
|
select: (d, target: SelectionTarget | null) => { d.selection = target },
|
|
setDraft: (d, text: string) => { d.draft = text },
|
|
clearDraft: (d) => { d.draft = '' },
|
|
// Optimistic-send failure restore: only when the user typed nothing new
|
|
// since the clear (send choreography lives in the inject factory).
|
|
restoreDraft: (d, text: string) => { if (d.draft === '') d.draft = text },
|
|
setView: (d, view: string) => { d.view = view },
|
|
},
|
|
})
|
|
}
|