fix(desktop): let subagent header collapse roster and details

This commit is contained in:
unsupportedpastels
2026-09-08 18:03:51 +00:00
committed by Teknium
parent ed6671db83
commit 7568fb6727
2 changed files with 61 additions and 25 deletions
@@ -6,11 +6,14 @@ import { $subagentsBySession, upsertSubagent } from '@/store/subagents'
import { ComposerStatusStack } from './index'
vi.mock('@/lib/use-enter-animation', () => ({ useEnterAnimation: () => undefined }))
vi.stubGlobal(
'ResizeObserver',
class {
disconnect() {}
observe() {}
unobserve() {}
}
)
@@ -19,7 +22,7 @@ afterEach(() => {
$subagentsBySession.set({})
})
it('automatically previews bounded live work only from the composer session, including queued children', () => {
it('shows live work only from the composer session and keeps it hidden after collapse and progress', () => {
for (let i = 0; i < 5; i++) {
upsertSubagent('owner', { subagent_id: `child-${i}`, goal: `Task ${i}`, status: i ? 'queued' : 'running' })
}
@@ -35,11 +38,18 @@ it('automatically previews bounded live work only from the composer session, inc
expect(screen.getByText('Task 0')).toBeTruthy()
expect(screen.getByText('Reading actual source')).toBeTruthy()
expect(screen.queryByText('Task 4')).toBeNull()
expect(screen.queryByText('Private foreign task')).toBeNull()
expect(screen.getByRole('button', { name: /5 Subagents/ })).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: /5 Subagents/ }))
const header = screen.getByRole('button', { name: /5 Subagents/ })
fireEvent.click(header)
expect(screen.queryByText('Task 0')).toBeNull()
expect(screen.queryByText('Task 4')).toBeNull()
expect(header.getAttribute('aria-expanded')).toBe('false')
act(() => upsertSubagent('owner', { subagent_id: 'child-0', text: 'More progress' }, false, 'subagent.progress'))
expect(screen.queryByText('Task 0')).toBeNull()
fireEvent.click(header)
expect(screen.getByText('Task 0')).toBeTruthy()
expect(screen.getByText('Task 4')).toBeTruthy()
expect(screen.getByText('More progress')).toBeTruthy()
view.rerender(
<MemoryRouter>
<ComposerStatusStack queue={null} sessionId="empty" />
@@ -48,6 +58,29 @@ it('automatically previews bounded live work only from the composer session, inc
expect(screen.queryByText('Task 0')).toBeNull()
})
it('collapses a single worker and its selected detail using the caret, preserving the steering draft', () => {
upsertSubagent('owner', { subagent_id: 'child', goal: 'Single task' })
const view = render(
<MemoryRouter>
<ComposerStatusStack queue={null} sessionId="owner" />
</MemoryRouter>
)
fireEvent.click(screen.getByRole('button', { name: /Single task/ }))
expect(view.container.querySelector('[data-slot="composer-subagent-detail"]')).toBeTruthy()
const draft = screen.getByRole('textbox')
fireEvent.change(draft, { target: { value: 'Keep this draft' } })
const header = screen.getByRole('button', { name: /1 Subagent/ })
fireEvent.click(header.firstElementChild!)
expect(screen.queryByText('Single task')).toBeNull()
expect(view.container.querySelector('[data-slot="composer-subagent-detail"]')).toBeNull()
expect(header.getAttribute('aria-expanded')).toBe('false')
fireEvent.click(header)
expect(view.container.querySelector('[data-slot="composer-subagent-detail"]')).toBeTruthy()
expect((screen.getByRole('textbox') as HTMLInputElement).value).toBe('Keep this draft')
})
it('retires the live frame only after every child settles, without depending on the parent busy state', () => {
upsertSubagent('owner', { subagent_id: 'child', goal: 'Live task' })
render(
@@ -64,32 +64,35 @@ export function SubagentSection({ sessionId }: SubagentSectionProps) {
return (
<div className="composer-no-drag min-w-0" data-slot="composer-subagents">
<StatusSection
collapsedIndicator={
<GlyphSpinner
ariaLabel={live.some(item => item.status === 'running') ? t.agents.running : t.agents.queued}
className="text-(--ui-purple)"
spinner="braille"
/>
}
defaultCollapsed={false}
icon={<Codicon className="text-(--ui-purple)" name="agent" size="0.8rem" />}
label={t.statusStack.subagents(live.length)}
preview={
<>
{live.slice(0, 3).map(row)}
{live.length > 3 && (
<p className="px-2 text-[0.68rem] text-(--ui-text-tertiary)">{t.agents.moreAgents(live.length - 3)}</p>
)}
</>
}
>
<div className="max-h-[25vh] overflow-y-auto overscroll-contain">{live.map(row)}</div>
{detail && (
<div
className="max-h-[25vh] overflow-y-auto overscroll-contain px-3 py-2"
data-slot="composer-subagent-detail"
>
<SubagentControls
key={`${sessionId}:${detail.id}`}
sessionId={sessionId}
setText={text => setDrafts(previous => ({ ...previous, [detail.id]: text }))}
subagentId={detail.id}
text={drafts[detail.id] ?? ''}
/>
<SubagentRow node={{ ...detail, children: [] }} nowMs={nowMs} />
<SubagentTranscript key={`tail:${sessionId}:${detail.id}`} sessionId={sessionId} subagentId={detail.id} />
</div>
)}
</StatusSection>
{detail && (
<div className="max-h-[25vh] overflow-y-auto overscroll-contain px-3 py-2" data-slot="composer-subagent-detail">
<SubagentControls
key={`${sessionId}:${detail.id}`}
sessionId={sessionId}
setText={text => setDrafts(previous => ({ ...previous, [detail.id]: text }))}
subagentId={detail.id}
text={drafts[detail.id] ?? ''}
/>
<SubagentRow node={{ ...detail, children: [] }} nowMs={nowMs} />
<SubagentTranscript key={`tail:${sessionId}:${detail.id}`} sessionId={sessionId} subagentId={detail.id} />
</div>
)}
</div>
)
}