refactor(gui): dissolve the tool ring into per-view keyed slots
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.
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
/**
|
||||
* ConversationService orchestration half after the store-seat slimming:
|
||||
* scope-addressed send/cancel (result folding, root throw), the startSession
|
||||
* chain (create → sessions.open → scoped send), views ordering, and the
|
||||
* service-unavailable loud failures. Selection/draft state left this service
|
||||
* for the declared chat store (chat-store.spec.ts / selection-survival.spec.ts).
|
||||
* chain (create → sessions.open → scoped send), and the service-unavailable
|
||||
* loud failures. Selection/draft state left this service for the declared
|
||||
* chat store (chat-store.spec.ts / selection-survival.spec.ts); the view
|
||||
* registry left for the 'conversation.view' slot (views-type-chain.spec.tsx).
|
||||
*/
|
||||
import { Context } from 'cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
@@ -68,7 +69,8 @@ async function bench(opts?: { sessions?: boolean }) {
|
||||
scope: (id: SessionId) => (id === sid('new-1') ? mint(id) : scopes.get(id)),
|
||||
} as unknown as SessionsService
|
||||
if (opts?.sessions !== false) ctx.provide('sessions', sessionsFake)
|
||||
const fiber = ctx.plugin((pluginCtx) => { void new ConversationService(pluginCtx) })
|
||||
// Class-plugin mount — the same form apply.ts uses in production.
|
||||
const fiber = ctx.plugin(ConversationService)
|
||||
await fiber.await()
|
||||
const svc = ctx.get('conversation') as ConversationService
|
||||
const scopedSvc = (id: SessionId) => mint(id).get('conversation') as ConversationService
|
||||
@@ -149,17 +151,3 @@ describe('service-unavailable loud failures', () => {
|
||||
.rejects.toThrow(/conversation service unavailable through the new scope/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('views ordering', () => {
|
||||
it('orders by explicit order with undefined treated as zero (both comparator arms)', async () => {
|
||||
const b = await bench()
|
||||
const entry = (id: string, order?: number) => ({
|
||||
id, label: id, component: () => null,
|
||||
...(order !== undefined ? { order } : {}),
|
||||
})
|
||||
b.svc.registerView(entry('z-late', 5) as never)
|
||||
b.svc.registerView(entry('default-zero') as never)
|
||||
b.svc.registerView(entry('first', -1) as never)
|
||||
expect(b.svc.views().map(v => v.id)).toEqual(['first', 'default-zero', 'z-late'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user