refactor(gui): route the composer chain on PendingWait currency

This commit is contained in:
imccyu
2026-07-23 17:23:17 +08:00
parent de36006956
commit 07bdfbce9b
16 changed files with 314 additions and 275 deletions
@@ -79,11 +79,11 @@ export function apply(ctx: Context): void {
slots.register({
name: 'conversation',
// Declaring the keyed composer slot here both creates it and authorizes
// ConversationRoot (the takeover dispatch site) to render it; feature
// plugins (ui-question) register their replacement composers into it.
// Declaring the chain composer slot here both creates it and authorizes
// ConversationRoot (the takeover dispatch site) to render it; takeover
// plugins (ui-question) register selector-routed composer replacements.
children: {
'conversation.composer': { kind: 'keyed', scope: 'session' },
'conversation.composer': { kind: 'chain', scope: 'session' },
},
store: chat,
inject: (sessionId: SessionId, actions: BoundActions<typeof chat>): ConversationInjected => {
@@ -271,7 +271,7 @@ export function createChatView(deps: ChatViewDeps): FC<ConvViewProps> {
</div>
)}
{pending.map((item) => item.kind === 'approval'
? <PendingCard key={item.rpcId} item={item} />
? <PendingCard key={item.key} item={item} />
: null)}
</div>
</div>
@@ -1,18 +1,18 @@
// PendingCard: approval placeholder card. Questions take over the composer.
import { memo } from 'react'
import type { PendingInteraction } from '@deepseek-ai/dsh-client-runtime/client'
import type { PendingWait } from '@deepseek-ai/dsh-client-runtime/client'
import css from './PendingCard.module.css'
export interface PendingCardProps {
item: Extract<PendingInteraction, { kind: 'approval' }>
item: PendingWait<'approval'>
}
export const PendingCard = memo(function PendingCard({ item }: PendingCardProps) {
return (
<div className={css.card}>
<div className={css.title}><span className={css.mono}>{item.toolName}</span></div>
{item.reason !== undefined && <div className={css.reason}>{item.reason}</div>}
<div className={css.title}><span className={css.mono}>{item.payload.toolName}</span></div>
{item.payload.reason !== undefined && <div className={css.reason}>{item.payload.reason}</div>}
<div className={css.hint}>web </div>
</div>
)
@@ -4,7 +4,7 @@
* conversation.empty). Terminal slot design (§3): full component props are the
* automatic shares — PropsRuntime<K> (framework standard kit) & PropsStore<H>
* (declared store's read/write faces) & the injected business face declared
* here. The conversation entry alone declares a child slot (the keyed
* here. The conversation entry alone declares a child slot (the chain-kind
* conversation.composer takeover), so only ConversationSlotProps carries the
* renderSlot share.
*/
@@ -42,9 +42,16 @@ export interface ConversationInjected {
open(id: SessionId): void
}
/** Question-composer owner share supplied by ConversationRoot at its renderSlot site. */
export interface QuestionComposerOwnerProps {
interaction: Extract<PendingInteraction, { kind: 'question' }>
/**
* Composer chain currency: what ConversationRoot dispatches at its
* renderSlotChain site. The owner declares the currency only — never a
* per-entry contract; takeover packages narrow it in their own selectors
* (`interactions.find(i => i.kind === ...)`), so new takeover kinds register
* with zero owner changes.
*/
export interface ComposerChainProps {
/** The session's live pending waits, in arrival order (snapshot reference). */
interactions: readonly PendingInteraction[]
}
/** Full conversation-slot component props: runtime share & child-render share & store share & injected share. */
@@ -8,7 +8,7 @@
*/
import type { ConversationService } from './service.ts'
import type { ToolViewRegistry } from './toolviews/registry.ts'
import type { QuestionComposerOwnerProps } from './contract/slots.ts'
import type { ComposerChainProps } from './contract/slots.ts'
export { apply, inject } from './apply.ts'
export { ConversationService } from './service.ts'
@@ -22,8 +22,8 @@ export type {
ResolvedToolView, ToolCallBlock, ToolViewOptions, ToolViewProps, ToolViewResolver,
} from './contract/toolview.ts'
export type {
ChatStore, ConversationInjected, ConversationSlotProps, DetailsInjected, DetailsSlotProps,
EmptyStateInjected, EmptyStateSlotProps, QuestionComposerOwnerProps,
ChatStore, ComposerChainProps, ConversationInjected, ConversationSlotProps, DetailsInjected,
DetailsSlotProps, EmptyStateInjected, EmptyStateSlotProps,
} from './contract/slots.ts'
// Export discipline: packages/client/AGENTS.md.
@@ -37,9 +37,9 @@ declare module 'cordis' {
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
'conversation.composer': {
kind: 'keyed'
kind: 'chain'
scope: 'session'
owner: QuestionComposerOwnerProps
owner: ComposerChainProps
}
}
}
@@ -36,7 +36,7 @@ function deriveAncestry(list: SessionListState, id: SessionId): readonly Session
export function ConversationRoot({
sessionId, useSession, useSessions, useStore, actions,
views, send, stop, openDetails, loadOlder, open, renderSlot,
views, send, stop, openDetails, loadOlder, open, renderSlotChain,
}: ConversationRootProps) {
useSyncExternalStore(views.subscribe, views.version)
const list = views.list()
@@ -51,7 +51,7 @@ export function ConversationRoot({
const removed = useSession(s => s.removed)
const promptError = useSession(s => s.promptError)
const turns = useSession(s => countTurns(s))
const question = useSession(s => s.pending.find(item => item.kind === 'question'))
const pending = useSession(s => s.pending)
const error: InputBarError | null = promptError === null
? null
@@ -78,8 +78,8 @@ export function ConversationRoot({
)
}
// The default composer doubles as the keyed slot's fallback: a pending
// question with no registered takeover must still leave the input usable.
// The default composer doubles as the chain's all-decline fallback: a
// pending wait with no registered takeover must still leave the input usable.
const composerBar = (
<InputBar
draft={draft}
@@ -142,12 +142,7 @@ export function ConversationRoot({
{active !== undefined && renderView(active)}
</div>
{question !== undefined && question.kind === 'question'
? renderSlot('conversation.composer', { interaction: question }, {
entryKey: 'question',
fallback: composerBar,
})
: composerBar}
{renderSlotChain('conversation.composer', { interactions: pending }, { fallback: composerBar })}
</div>
)
}