fix(desktop): keep composer status groups collapsed except todos

This commit is contained in:
brooklyn!
2026-09-09 19:06:20 -05:00
parent 8e85b276f1
commit 5b181e511a
11 changed files with 131 additions and 14 deletions
@@ -46,10 +46,6 @@ export function QueuePanel({
}
return (
// Keyed on the park flag: StatusSection owns its collapse state from
// defaultCollapsed, so remount on park/unpark. A Stop must EXPAND the
// panel — the halted prompts' only presence is here, and leaving them
// behind a collapsed "N queued" pill is how they read as vanished.
<StatusSection
accessory={
parked ? (
@@ -66,9 +62,7 @@ export function QueuePanel({
</Tip>
) : undefined
}
defaultCollapsed={!parked}
icon={<Codicon className="text-muted-foreground/70" name={parked ? 'debug-pause' : 'layers'} size="0.8rem" />}
key={parked ? 'parked' : 'flowing'}
label={parked ? c.queuedPaused(entries.length) : c.queued(entries.length)}
>
{entries.map(entry => {
@@ -0,0 +1,109 @@
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router'
import { afterEach, expect, it } from 'vitest'
import { $backgroundStatusBySession } from '@/store/composer-status'
import { $goalsBySession } from '@/store/goals'
import { $sessionControlBySession } from '@/store/session-control'
import { $subagentsBySession, upsertSubagent } from '@/store/subagents'
import { $todosBySession } from '@/store/todos'
import { QueuePanel } from '../queue-panel'
import { ComposerStatusStack } from './index'
const noop = () => {}
const queue = (parked: boolean) => (
<QueuePanel
busy
editingId={null}
entries={[{ attachments: [], id: 'queued', queuedAt: 1, text: 'Queued request' }]}
onDelete={noop}
onEdit={noop}
onResume={noop}
onSendNow={noop}
parked={parked}
/>
)
const stack = (parked = false) => (
<MemoryRouter>
<ComposerStatusStack queue={queue(parked)} sessionId="owner" />
</MemoryRouter>
)
afterEach(() => {
cleanup()
$backgroundStatusBySession.set({})
$goalsBySession.set({})
$sessionControlBySession.set({})
$subagentsBySession.set({})
$todosBySession.set({})
})
it('auto-expands only todos and keeps other groups closed as activity arrives', () => {
$todosBySession.set({ owner: [{ id: 'todo', content: 'Visible todo', status: 'in_progress' }] })
$goalsBySession.set({ owner: { status: 'active', title: 'Hidden legacy goal', updatedAt: 1 } })
$backgroundStatusBySession.set({
owner: [{ id: 'process', type: 'background', state: 'running', title: 'Background process' }]
})
upsertSubagent('owner', { subagent_id: 'worker', goal: 'Worker task' })
const view = render(stack())
expect(screen.getByText('Visible todo')).toBeTruthy()
for (const text of ['Hidden legacy goal', 'Background process', 'Worker task', 'Queued request']) {
expect(screen.queryByText(text)).toBeNull()
}
act(() => upsertSubagent('owner', { subagent_id: 'worker', text: 'Progress arrived' }, false, 'subagent.progress'))
view.rerender(stack(true))
expect(screen.queryByText('Worker task')).toBeNull()
expect(screen.queryByText('Queued request')).toBeNull()
const header = screen.getByRole('button', { name: /1 Subagent/ })
fireEvent.click(header)
expect(screen.getByText('Worker task')).toBeTruthy()
expect(screen.getByText('Progress arrived')).toBeTruthy()
view.rerender(stack(false))
expect(screen.getByText('Worker task')).toBeTruthy()
})
it('starts structured goals collapsed and preserves manual queue expansion when parked', () => {
$sessionControlBySession.set({
owner: {
capability: 'supported',
error: null,
loading: false,
pendingAction: null,
snapshot: {
goal: {
title: 'Structured goal',
status: 'active',
max_turns: 20,
turns_used: 1,
subgoals: ['Criterion'],
gates: [],
contract: { outcome: '', verification: '', boundaries: '', constraints: '', stop_when: '' }
},
heartbeat: null,
loop: null,
revision: '1',
updated_at: 1
}
}
})
const view = render(stack())
const goal = screen.getByRole('button', { name: /Goal active/ })
expect(goal.getAttribute('aria-expanded')).toBe('false')
expect(screen.queryByText('Criterion')).toBeNull()
fireEvent.click(goal)
expect(screen.getByText('Criterion')).toBeTruthy()
const queued = screen.getByRole('button', { name: /1 queued/i })
fireEvent.click(queued)
expect(screen.getByText('Queued request')).toBeTruthy()
view.rerender(stack(true))
expect(screen.getByText('Queued request')).toBeTruthy()
})
@@ -1,4 +1,4 @@
import { cleanup, render, screen } from '@testing-library/react'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -57,6 +57,8 @@ describe('ComposerStatusStack goal indicator', () => {
renderStack()
expect(screen.getByText('Goal active')).toBeTruthy()
expect(screen.queryByText('ship the feature')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: /Goal (active|paused)/ }))
expect(screen.getByText('ship the feature')).toBeTruthy()
})
@@ -66,6 +68,8 @@ describe('ComposerStatusStack goal indicator', () => {
renderStack()
expect(screen.getByText('Goal paused')).toBeTruthy()
expect(screen.queryByText('ship the feature')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: /Goal (active|paused)/ }))
expect(screen.getByText('ship the feature')).toBeTruthy()
})
@@ -74,6 +78,7 @@ describe('ComposerStatusStack goal indicator', () => {
renderStack()
fireEvent.click(screen.getByRole('button', { name: /Goal active/ }))
expect(screen.getByText('Continuing toward goal (3/20)')).toBeTruthy()
})
@@ -241,7 +241,7 @@ export function ComposerStatusStack({ onSubmit, queue, sessionId }: ComposerStat
/>
) : undefined
}
defaultCollapsed={group.type !== 'todo' && group.type !== 'goal'}
defaultCollapsed={group.type !== 'todo'}
icon={<Codicon className="text-muted-foreground/70" name={GROUP_ICON[group.type]} size="0.8rem" />}
label={groupLabel(group, t.statusStack)}
>
@@ -313,7 +313,7 @@ export function ComposerStatusStack({ onSubmit, queue, sessionId }: ComposerStat
composerDockCard('top'),
// Inset (mx-2) so the stack reads slightly narrower than the composer
// surface below it — the original look.
'mx-2 overflow-hidden rounded-b-none border-b border-b-transparent pt-0.5'
'mx-2 overflow-hidden rounded-b-none border-b border-b-transparent'
)}
>
<div
@@ -324,7 +324,6 @@ export const SessionControlGoalSection = memo(function SessionControlGoalSection
</DropdownMenuContent>
</DropdownMenu>
}
defaultCollapsed={false}
icon={<Codicon className={iconClass} name="target" size="0.8rem" />}
label={headerLabel}
>
@@ -169,7 +169,8 @@ describe('ComposerStatusStack session-control UI', () => {
expect(mockRefreshSessionControl).toHaveBeenCalledWith(SID)
expect(mockRefreshSessionGoal).not.toHaveBeenCalled()
// Legacy goal is rendered while capability is unknown
// Legacy goal remains available while capability is unknown.
fireEvent.click(screen.getByRole('button', { name: /Goal active/ }))
expect(screen.getByText('Legacy Goal Title')).toBeTruthy()
})
@@ -187,6 +188,7 @@ describe('ComposerStatusStack session-control UI', () => {
renderStack()
fireEvent.click(screen.getByRole('button', { name: /Goal active/ }))
expect(screen.getByText('Structured Goal Title')).toBeTruthy()
expect(screen.queryByText('Legacy Goal Title')).toBeNull()
})
@@ -28,6 +28,7 @@ it('sends steer and stop to the child parent owner, never the active gateway or
setSessionOwnerHint('parent', { connectionId: 'remote-owner', profile: 'research' })
upsertSubagent('parent', { subagent_id: 'worker', child_session_id: 'child-transcript', goal: 'Owned work' })
render(<SubagentSection sessionId="parent" />)
fireEvent.click(screen.getByRole('button', { name: /1 Subagent/ }))
fireEvent.click(screen.getByRole('button', { name: /Owned work/ }))
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Check the negative control' } })
fireEvent.click(screen.getByRole('button', { name: 'Steer' }))
@@ -53,6 +54,7 @@ it('keeps rejected steer text and does not retarget when the owner is unknown',
const request = vi.spyOn(gateway, 'requestGatewayForAgent').mockResolvedValue({ status: 'rejected' })
upsertSubagent('unknown', { subagent_id: 'worker', goal: 'Unbound work' })
render(<SubagentSection sessionId="unknown" />)
fireEvent.click(screen.getByRole('button', { name: /1 Subagent/ }))
fireEvent.click(screen.getByRole('button', { name: /Unbound work/ }))
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Keep this instruction' } })
fireEvent.click(screen.getByRole('button', { name: 'Steer' }))
@@ -52,6 +52,7 @@ it('hydrates the empty owner composer and exposes an extended owner-routed trans
</MemoryRouter>
)
fireEvent.click(await screen.findByRole('button', { name: /1 Subagent/ }))
await screen.findByText('Recovered work')
expect(screen.getByText('Read File')).toBeTruthy()
expect(request).toHaveBeenCalledWith('remote-owner', 'research', 'subagent.list', { session_id: 'parent' })
@@ -24,6 +24,7 @@ it('keeps each worker draft while inspecting siblings and removes settled select
upsertSubagent('parent', { subagent_id: 'a', goal: 'Worker A' })
upsertSubagent('parent', { subagent_id: 'b', goal: 'Worker B' })
render(<SubagentSection sessionId="parent" />)
fireEvent.click(screen.getByRole('button', { name: /2 Subagents/ }))
fireEvent.click(screen.getByRole('button', { name: /Worker A/ }))
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Preserve my instruction' } })
fireEvent.click(screen.getByRole('button', { name: /Worker B/ }))
@@ -40,6 +41,7 @@ it('measures detail elapsed from worker start rather than first inspection', ()
upsertSubagent('parent', { subagent_id: 'timed', goal: 'Timed worker' })
now.mockReturnValue(117000)
const { container } = render(<SubagentSection sessionId="parent" />)
fireEvent.click(screen.getByRole('button', { name: /1 Subagent/ }))
fireEvent.click(screen.getByRole('button', { name: /Timed worker/ }))
expect(
container.querySelector(
@@ -36,11 +36,13 @@ it('shows live work only from the composer session and keeps it hidden after col
</MemoryRouter>
)
expect(screen.getByText('Task 0')).toBeTruthy()
expect(screen.getByText('Reading actual source')).toBeTruthy()
expect(screen.queryByText('Task 0')).toBeNull()
expect(screen.queryByText('Private foreign task')).toBeNull()
const header = screen.getByRole('button', { name: /5 Subagents/ })
fireEvent.click(header)
expect(screen.getByText('Task 0')).toBeTruthy()
expect(screen.getByText('Reading actual source')).toBeTruthy()
fireEvent.click(header)
expect(screen.queryByText('Task 0')).toBeNull()
expect(screen.queryByText('Task 4')).toBeNull()
expect(header.getAttribute('aria-expanded')).toBe('false')
@@ -67,6 +69,7 @@ it('collapses a single worker and its selected detail using the caret, preservin
</MemoryRouter>
)
fireEvent.click(screen.getByRole('button', { name: /1 Subagent/ }))
fireEvent.click(screen.getByRole('button', { name: /Single task/ }))
expect(view.container.querySelector('[data-slot="composer-subagent-detail"]')).toBeTruthy()
const draft = screen.getByRole('textbox')
@@ -88,6 +91,7 @@ it('retires the live frame only after every child settles, without depending on
<ComposerStatusStack queue={null} sessionId="owner" />
</MemoryRouter>
)
fireEvent.click(screen.getByRole('button', { name: /1 Subagent/ }))
expect(screen.getByText('Live task')).toBeTruthy()
act(() => upsertSubagent('owner', { subagent_id: 'child', status: 'completed' }, false, 'subagent.complete'))
expect(screen.queryByText('Live task')).toBeNull()
@@ -71,7 +71,6 @@ export function SubagentSection({ sessionId }: SubagentSectionProps) {
spinner="braille"
/>
}
defaultCollapsed={false}
icon={<Codicon className="text-(--ui-purple)" name="agent" size="0.8rem" />}
label={t.statusStack.subagents(live.length)}
>