= {
kind: 'assistant-step',
target: 'chat',
match: event => matchAssistant(event),
start: (_context, match) => initialState(match),
update: (context, match) => appendChunk(context.state, match.event), // increment, not the window
publication: () => 'animation-frame', // high-frequency deltas coalesce; never immediate
buildViewNode: context => assistantNode(context),
}
```
Rebuilding the whole node collection for every chunk and publishing it immediately:
```ts ignore-check
function onChunk(chunk: AssistantLiveChunk): void {
const nodes = buildNodesFromCompleteWindow(session) // full recompute per chunk
target.publish(nodes, { publication: 'immediate' }) // and an immediate publish
}
```
Each chunk costs a full-window rebuild inside the frame budget, so grouping and rendering stall behind the stream.
## The node build and grouping engine carries no business rule
Business rules never enter the grouping engine, and a business requirement never reshapes the built payload. `ProcessState`, the group product hierarchy, and the set of group semantics in `conversation-nodes/process-groups.ts` are infrastructure: the engine understands enum placement values such as grouped, standalone, and reply, and the caller computes which one applies. A special case inside the engine changes the produced structure for every consumer and puts a business condition on the group computation path.
- Judged condition: does the engine contain a business condition? A diff that adds a `ProcessState` field, adds or removes a group level, or changes group semantics for one business fails; the placement decision belongs to the caller that computes the enum value.
- Boundary: the layering, its cost, and existing business expectations are the reason a business does not rework the grouping design.
- Boundary: a new enum value carrying a new placement meaning is the supported extension, wherever the caller computes it.
- Exception and approver: none by default. A change to this infrastructure needs the corresponding domain expert's confirmation. [conversation.md](../subsystems/conversation.md#group-definitions) owns the group Definition types.
The caller decides the business meaning and passes the placement enum the engine places by:
```ts ignore-check
// Business judgment stays in the caller.
function placementOf(command: CommandView): NodePlacement {
return isHandoff(command) ? 'reply' : 'grouped'
}
// The engine consumes the enum as data; it reads no business condition.
function groupEntries(commands: readonly CommandView[]): readonly NodePlacement[] {
return commands.map(placementOf)
}
```
A business condition inside the engine's own computation:
```ts ignore-check
// The grouping engine asks what a node means, not where it goes.
function placeProcessNode(node: ChatConversationViewNode): GroupPlacement {
if (node.kind === 'command' && isRagMessage(node)) return 'rag' // business special case
return 'grouped'
}
```
The produced structure now carries one business's meaning, so every consumer and the group computation follow that feature.
## The node list and seat components carry no business branch
`ChatNodeList`, `ChatNodeSeat`, and `ChatGroupSeat` are infrastructure. They carry no business condition and no business-specific field, and a single business never changes the list traversal or the seat selection. A new node shape contributes its kind to `ChatNodeDataMap` and its renderer through the kind registration surface, so the list keeps dispatching on kind while business semantics stay in the business module. A branch added for one feature makes every node's rendering depend on that feature.
- Judged condition: a business branch in a list or seat component, a business-only field on its props, or a traversal or seat-selection change that serves one business.
- Correct outlet: the business package merges its own key into `ChatNodeDataMap` and registers a renderer for the node kind; the seat renders the registered component for the node's kind. The registration discipline is owned by [slots.md](../subsystems/slots.md#extension-rules).
- Boundary: a rendering change that applies to every business — ordering, virtualization, accessibility — is not a business modification.
- Exception and approver: none by default; the corresponding domain expert confirms any exception.
A business package that declares its kind and registers its renderer leaves the shared list untouched:
```tsx
// The business declares its kind and registers its renderer; the shared list is untouched.
declare module '@deepseek-ai/dsh-client-ui-chat/client' {
interface ChatNodeDataMap { handoff: HandoffData }
}
const HandoffNodeView = memo(function HandoffNodeView({ node, t }: ChatNodeOwnerProps<'handoff'>) {
return
})
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register(
{ name: 'conversation.chat.node', key: 'handoff', locale: NS }, HandoffNodeView))
```
A business branch and a business field inside the shared seat:
```tsx
export const ChatNodeSeat = memo(function ChatNodeSeat({ node, handoffFlag }: ChatNodeSeatProps) {
if (node.kind === 'user' && handoffFlag) return // business dispatch
return {renderByKind(node)}
})
```
One feature's condition now decides how every node renders, and the shared seat must change again for the next feature.
## The Markdown pipeline carries no business branch
`AssistantMarkdown` and its rendering pipeline are infrastructure: no business condition enters the Markdown render path, and no business rewrites the node mapping or the style injection rules. A presentation need registers a renderer on the existing chain or expresses itself in the data, and the official default content participates in that same registration surface rather than existing as an optional renderer with a host-side inline fallback. A pipeline that hosts business special cases can no longer be reviewed or optimized on its own.
- Judged condition: a business branch inside `AssistantMarkdown` or its block mapping — for example `if (isXxxBusiness) renderXxx()` — or a style injection rule keyed on one business.
- Correct outlet: a renderer registered on the existing chain, or a semantic field in the data that the generic renderer reads. The Component Factory registration is owned by the [ui-renderer README](../../packages/client/ui-renderer/README.md).
- Wrapping: a wrapper that needs the standard rendering calls the official entry and keeps its fallback; it never re-renders the slot and never imports the private React component across packages.
- Responsibility: the host owns position and invocation; the plugin owns its own state, cleanup, and controls, and the official host carries no business action for a contribution.
- `labels`: an optional complete `MarkdownLabels` object is used whole, with no field-level merge against the default; omitting it uses the official default. `variant="compact"` is fixed by the Factory.
- Boundary: generic Markdown capability — syntax support, performance — is not a business modification.
- Exception and approver: none by default; the corresponding domain expert confirms any exception.
A contribution that registers into the existing single seat, and reuses the published Factory instead of re-rendering the standard content:
```tsx
// The business contributes its own body into the seat the host already renders.
ctx.slots.inject('conversation.chat.reasoning.body', () => ctx.slots.register(
{ name: 'conversation.chat.reasoning.body', scope: 'session', locale: NS },
TranslatedReasoningBody,
))
// The contribution keeps the standard rendering by calling the published Factory.
function TranslatedReasoningBody({ text, running, renderFactorySlot, t }: ReasoningBodyProps) {
return (
<>
{t('reasoning.translated')}
{renderFactorySlot('conversation.chat.reasoning.content', { text, running })}
>
)
}
```
A business branch inside the shared Markdown pipeline:
```tsx
export const AssistantMarkdown = memo(function AssistantMarkdown({ blocks, ...props }: AssistantMarkdownProps) {
return <>{blocks.map(block => (
isHandoffBlock(block)
? // a business special case in the pipeline
: renderBlock(block, props)
))}>
})
```
Every business special case lands in one shared pipeline, and Markdown rendering can no longer change or be measured without the businesses it now carries.
## The user message model and rendering carry no business field
`UserMessage` and the UI's `user` kind rendering are infrastructure: no business-only field on the message model, and no business branch in the user message renderer. A business difference travels in the message payload — its own merge-extensible source kind or a content field — or through a registered renderer. A field added for one business is handled by every consumer of the model, and the next business adds another branch to the same shared renderer.
- Judged condition: a business-named field on `UserMessage`, or a business condition in the rendering of the `user` kind.
- Correct outlet: expression in the payload. `MessageSourceMap` is declaration-merge extensible, so a producer declares its own source kind instead of widening `UserMessage`; the message model is documented in [session.md](../subsystems/session.md).
- Boundary: a generic message capability — attachments, quotes, edit state — is not a business modification.
- Exception and approver: none by default; the corresponding domain expert confirms any exception.
A producer that declares its own source kind keeps the business difference in the payload:
```ts ignore-check
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
handoff: { kind: 'handoff'; sessionId: SessionId }
}
}
const message = createUserMessage({
role: 'user',
content: [{ type: 'text', text: prompt }],
source: { kind: 'handoff', sessionId: source.id },
})
```
A business field on the shared model, and the branch it forces into the shared renderer:
```ts ignore-check
interface UserMessage extends MessageBase {
readonly role: 'user'
readonly handoffKind?: 'handoff' | 'rag' // one business's field on the shared model
}
// ...and the renderer branches on it.
if (message.handoffKind === 'handoff') return
```
Every consumer of the message model handles one business's field, and the shared renderer grows a branch for each business that follows.