Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Signed-off-by: suyao <sy20010504@gmail.com>
5.1 KiB
Chat Adapters
Status: target architecture (design). This describes the chat layer's intended contract layer, which lands incrementally across carve PRs. As of this PR, only
layout/,primitives/,tokens/,utils/are onmain; theadapters/module, the symbols, and the@renderer/components/chatpackage barrel referenced below are the design target and arrive in a later carve. The imports and code below illustrate the intended API, not code that compiles onmaintoday.
The chat adapters (src/renderer/components/chat/adapters/) are the planned contract
layer used by shared chat UI slices. They are intentionally thin: they will project
business entities into stable UI-facing shapes, but they do not fetch data, own cache,
read preferences, or replace existing UI components.
The target is to import from the chat package entry unless you are working inside that folder:
import { ComposerAdapter, ResourceListAdapter } from '@renderer/components/chat'
import { createMessageActionRegistry, createRightPaneRegistry } from '@renderer/components/chat'
Resource List
Use ResourceListAdapter before passing topic or session data into future ResourceList components. The output is ChatResourceItem, which only contains UI fields such as id, kind, title, subtitle, status, pinned, active, disabled, and optional meta.
const item = ResourceListAdapter.fromTopic(topic, {
active: topic.id === activeTopicId,
pinned: topic.pinned,
status: isStreaming ? 'streaming' : undefined
})
Callers still own active state, pin state, streaming state, and persistence. The adapter should not call DataApi, Cache, Preference, Redux, or service hooks.
Composer
Use ComposerAdapter to describe the minimum contract a future composer needs: target, draft, send, optional stop, streaming state, disabled state, and capability flags.
const composer = ComposerAdapter.createChat({
assistantId,
topicId,
draft: { text, attachments },
streaming: isPending,
capabilities: { attachments: true, stop: true },
send: ({ draft }) => sendMessage(draft.text),
stop: () => stopStreaming()
})
The adapter only delegates callbacks. The existing chat/session hooks keep ownership of send, stop, attachments, tool selection, and draft state.
Right Pane Registry
Use createRightPaneRegistry() when a feature needs to register pane descriptors by id. A descriptor contains id, title, and render(payload).
const registry = createRightPaneRegistry()
const dispose = registry.register({
id: 'references',
title: 'References',
render: (payload: { messageId: string }) => <ReferencePanel messageId={payload.messageId} />
})
const pane = registry.get<{ messageId: string }>('references')
dispose()
Registering the same id replaces the previous descriptor. Disposing an older registration does not remove a newer replacement.
Message Action Registry
Use createMessageActionRegistry() to register message action providers. The command/action model will be defined by actions/actionTypes.ts, including ActionDescriptor, CommandDescriptor, and ResolvedAction.
const registry = createMessageActionRegistry()
const dispose = registry.register({
id: 'copy-message',
resolve: ({ message }) => [{ id: `copy:${message.id}`, label: 'Copy' }]
})
const actions = registry.resolve({ message })
dispose()
Render Stability
Adapters are pure projection helpers, so they do not cause rerenders by themselves. Rerender risk comes from creating fresh arrays, objects, callbacks, or registries on every React render. When these contracts are wired into real UI, keep the projection boundary stable.
Use useMemo for list projections:
const items = useMemo(
() => topics.map((topic) => ResourceListAdapter.fromTopic(topic, { active: topic.id === activeTopicId })),
[topics, activeTopicId]
)
Do not map resources inline in JSX:
<ResourceList items={topics.map((topic) => ResourceListAdapter.fromTopic(topic))} />
For messages, the target is to use the MessageListItem contract from components/chat/messages (planned). Project once at the message-list data boundary; virtualized lists rely on stable item identity and measurement caches.
For composer contracts, wrap ComposerAdapter.createChat() and ComposerAdapter.createSession() in useMemo, and keep send / stop callbacks stable with the existing business hook output or useCallback.
Create registries at module scope, provider initialization, or in useRef. Register providers or pane descriptors from effects, not during render:
const registryRef = useRef(createMessageActionRegistry())
Keep adapter output small. Do not place raw topic, session, or message objects in meta; that would re-couple components to private business shapes and make downstream memoization depend on raw object identity.
Boundaries
- Do not import these adapters into data hooks to create a second source of truth.
- Do not add business reads or writes inside adapters.
- Do not replace
TopicItem,SessionItem,InputbarTools, or context menus. - Add tests alongside adapter changes in
__tests__/adapters.test.ts.