feat(web): add nested subagent conversations
This commit is contained in:
@@ -165,7 +165,10 @@ export function apply(ctx: Context): void {
|
||||
// the resident parent keeps Hero and composer layout identity stable.
|
||||
slots.register({
|
||||
name: 'conversation.session',
|
||||
children: { 'conversation.view': { kind: 'list', scope: 'session' } },
|
||||
children: {
|
||||
'conversation.view': { kind: 'list', scope: 'session' },
|
||||
'conversation.session.header.actions': { kind: 'list', scope: 'session' },
|
||||
},
|
||||
store: chatStore,
|
||||
inject: (sessionId: SessionId, _actions: BoundActions<typeof chatStore>): ConversationSessionInjected => ({
|
||||
views: {
|
||||
|
||||
@@ -17,6 +17,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
* remounted when the current session id changes.
|
||||
*/
|
||||
'conversation.session': { kind: 'single'; scope: 'session'; owner: ConversationSessionOwnerProps }
|
||||
/** Session-header actions contributed by feature plugins. */
|
||||
'conversation.session.header.actions': { kind: 'list'; scope: 'session'; owner: ConversationHeaderActionOwnerProps }
|
||||
/**
|
||||
* The conversation view ring: one list entry per view tab (chat here;
|
||||
* trajectory/waterfall from ui-trajectory), rendered one-at-a-time by
|
||||
@@ -135,6 +137,9 @@ export interface ConversationSessionOwnerProps {
|
||||
wrapActiveBody?: (view: ReactNode) => ReactNode
|
||||
}
|
||||
|
||||
/** Header actions derive their state from the standard session/global kit. */
|
||||
export interface ConversationHeaderActionOwnerProps {}
|
||||
|
||||
/**
|
||||
* The input-region slot currency (plan §1.4): dock/left/right entries read
|
||||
* the conversation snapshot and the live input state as owner props (both
|
||||
@@ -329,6 +334,8 @@ export type ComposerBarProps =
|
||||
*/
|
||||
export interface ComposerChainProps {
|
||||
interactions: readonly PendingInteraction[]
|
||||
/** A catalog-addressed child whose exact parent Agent is unavailable. */
|
||||
subagentReadOnly: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +357,7 @@ export type ConversationSlotProps =
|
||||
/** Full strict-session content props: per-session store, view ring, callbacks, and the locale seat. */
|
||||
export type ConversationSessionSlotProps =
|
||||
PropsRuntime<'conversation.session'>
|
||||
& PropsRenderSlots<'conversation.view'>
|
||||
& PropsRenderSlots<'conversation.view' | 'conversation.session.header.actions'>
|
||||
& PropsStore<ChatStore>
|
||||
& ConversationSessionInjected
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
.titleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
@@ -43,6 +45,13 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* figma Tab_Group 34:11441: 35px strip, gap 36, pad-left 8, tabs bottom-aligned. */
|
||||
.tabs {
|
||||
display: flex;
|
||||
|
||||
@@ -19,6 +19,7 @@ export function ConversationRoot({
|
||||
const openState = useSession(s => s.openState)
|
||||
const composerPhase = useSession(s => s.composerPhase)
|
||||
const pending = useSession(s => s.pending) ?? []
|
||||
const subagentReadOnly = useSession(s => s?.subagent?.parentAvailable === false) ?? false
|
||||
const session = useSession(s => s)
|
||||
const inputState = useInput(s => s)
|
||||
const cwd = useSessions(s => sessionId === undefined ? undefined : s.byId[sessionId]?.cwd)
|
||||
@@ -153,7 +154,7 @@ export function ConversationRoot({
|
||||
const phase = settling ? 'settling' : hero ? 'hero' : 'active'
|
||||
const composer = renderSlotChain(
|
||||
'conversation.composer',
|
||||
{ interactions: pending },
|
||||
{ interactions: pending, subagentReadOnly },
|
||||
{ fallback: composerBar, overlay: true },
|
||||
)
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@ export function ConversationSession({
|
||||
<>
|
||||
<div className={css.titleRow}>
|
||||
<h1 className={css.sessionTitle}>{title}</h1>
|
||||
<div className={css.headerActions}>
|
||||
{renderSlot('conversation.session.header.actions', {})}
|
||||
</div>
|
||||
</div>
|
||||
{tabs.length > 1 && (
|
||||
<div className={css.tabs} role="tablist">
|
||||
|
||||
@@ -44,6 +44,7 @@ export function InputBar({
|
||||
const commandMenuOpen = useMenuLauncher(source => source === 'command')
|
||||
const promptError = useSession(s => s.promptError) ?? null
|
||||
const running = useSession(s => s.running) ?? false
|
||||
const subagent = useSession(s => s.subagent) ?? null
|
||||
const removed = useSession(s => s.removed) ?? false
|
||||
// Plan mode swaps the textarea placeholder (the projection is the folded
|
||||
// host value; owner-prop placeholders — hero, session-unavailable — win).
|
||||
@@ -283,10 +284,11 @@ export function InputBar({
|
||||
if (el !== null) toggleCommandMenu?.(selectionOf(el))
|
||||
}
|
||||
|
||||
const primaryLabel = running ? t('input.stop') : t('input.send')
|
||||
const ordinary = subagent === null
|
||||
const primaryLabel = running && ordinary ? t('input.stop') : t('input.send')
|
||||
const onPrimary = (): void => {
|
||||
if (inputActions === undefined || stop === undefined) return // absent machine: the button is disabled
|
||||
if (running) {
|
||||
if (running && ordinary) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user