feat(client): surface subagent activity in sidebar
This commit is contained in:
@@ -64,6 +64,7 @@ describe('workspace browser rows', () => {
|
||||
title: 'Result title',
|
||||
workspace: 'Workspace context',
|
||||
running: true,
|
||||
runningSubagentCount: 0,
|
||||
completed: false,
|
||||
snippet: 'matching message excerpt',
|
||||
}
|
||||
@@ -86,7 +87,7 @@ describe('workspace browser rows', () => {
|
||||
] as const)('shows %s ahead of running in search results', (pendingInteraction, label) => {
|
||||
const result: SearchResultNode = {
|
||||
id: sid(pendingInteraction), title: 'Needs input', workspace: 'Project',
|
||||
pendingInteraction, running: true, completed: false,
|
||||
pendingInteraction, running: true, runningSubagentCount: 0, completed: false,
|
||||
}
|
||||
render(<SearchResultItem result={result} currentId={undefined} onOpen={vi.fn()} t={t} />)
|
||||
const row = screen.getByRole('treeitem')
|
||||
@@ -115,7 +116,8 @@ describe('workspace browser rows', () => {
|
||||
|
||||
it('renders and opens a selected running Session row', () => {
|
||||
const node: SessionNode = {
|
||||
id: sid('session'), title: 'Session', blank: false, running: true, completed: false, updatedAt: 0,
|
||||
id: sid('session'), title: 'Session', blank: false, running: true,
|
||||
runningSubagentCount: 0, completed: false, updatedAt: 0,
|
||||
}
|
||||
const onOpen = vi.fn()
|
||||
render(
|
||||
@@ -134,7 +136,10 @@ describe('workspace browser rows', () => {
|
||||
it('shows the green done dot only on a finished, unviewed session (running wins the slot)', () => {
|
||||
const renderRow = (over: Partial<SessionNode>) => render(
|
||||
<SessionNodeItem
|
||||
node={{ id: sid('s1'), title: 'One', blank: false, running: false, completed: false, updatedAt: 0, ...over }}
|
||||
node={{
|
||||
id: sid('s1'), title: 'One', blank: false, running: false,
|
||||
runningSubagentCount: 0, completed: false, updatedAt: 0, ...over,
|
||||
}}
|
||||
currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t}
|
||||
/>,
|
||||
@@ -155,9 +160,48 @@ describe('workspace browser rows', () => {
|
||||
expect(running.container.querySelector('[data-state="done"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows descendant activity without describing an idle parent as running', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const node: SessionNode = {
|
||||
id: sid('owner'), title: 'Delegating', blank: false, running: false,
|
||||
runningSubagentCount: 2, completed: false, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
const row = screen.getByRole('treeitem')
|
||||
expect(row.querySelector('[data-state="ongoing"]')).not.toBeNull()
|
||||
expect(screen.getByText('2 个子代理运行中')).toBeTruthy()
|
||||
expect(screen.queryByText('进行中')).toBeNull()
|
||||
|
||||
fireEvent.pointerEnter(row.parentElement as HTMLElement)
|
||||
act(() => { vi.advanceTimersByTime(500) })
|
||||
expect(screen.getAllByText('2 个子代理运行中')).toHaveLength(2)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps child activity as a secondary status while user attention is primary', () => {
|
||||
const node: SessionNode = {
|
||||
id: sid('owner'), title: 'Needs input', blank: false, pendingInteraction: 'question',
|
||||
running: false, runningSubagentCount: 1, completed: false, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
const row = screen.getByRole('treeitem')
|
||||
expect(row.querySelector('[data-state="warning"]')).not.toBeNull()
|
||||
expect(row.querySelector('[data-state="ongoing"]')).toBeNull()
|
||||
expect(screen.getByText('等待回答')).toBeTruthy()
|
||||
expect(screen.getByText('1 个子代理运行中')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the green done dot on a finished search result row', () => {
|
||||
render(<SearchResultItem
|
||||
result={{ id: sid('result'), title: 'Done', workspace: 'Workspace', running: false, completed: true }}
|
||||
result={{
|
||||
id: sid('result'), title: 'Done', workspace: 'Workspace', running: false,
|
||||
runningSubagentCount: 0, completed: true,
|
||||
}}
|
||||
currentId={undefined} onOpen={vi.fn()} t={t}
|
||||
/>)
|
||||
expect(screen.getByRole('treeitem').querySelector('[data-state="done"]')).not.toBeNull()
|
||||
@@ -231,7 +275,8 @@ describe('workspace browser rows', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const node: SessionNode = {
|
||||
id: sid('s-blank'), title: 'ignored', blank: true, running: false, completed: false, updatedAt: 0,
|
||||
id: sid('s-blank'), title: 'ignored', blank: true, running: false,
|
||||
runningSubagentCount: 0, completed: false, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={node.id} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
@@ -257,7 +302,8 @@ describe('workspace browser rows', () => {
|
||||
const onFork = vi.fn()
|
||||
const onArchive = vi.fn()
|
||||
const node: SessionNode = {
|
||||
id: sid('s1'), title: 'One', blank: false, running: false, completed: false, updatedAt: 0,
|
||||
id: sid('s1'), title: 'One', blank: false, running: false,
|
||||
runningSubagentCount: 0, completed: false, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={onOpen}
|
||||
onRename={onRename} onFork={onFork} onArchive={onArchive} t={t} />)
|
||||
@@ -290,7 +336,8 @@ describe('workspace browser rows', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const node: SessionNode = {
|
||||
id: sid('s1'), title: 'Hovered', blank: false, running: true, completed: false, updatedAt: 0,
|
||||
id: sid('s1'), title: 'Hovered', blank: false, running: true,
|
||||
runningSubagentCount: 0, completed: false, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={60_000} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
@@ -321,7 +368,7 @@ describe('workspace browser rows', () => {
|
||||
try {
|
||||
const node: SessionNode = {
|
||||
id: sid(pendingInteraction), title: 'Needs input', blank: false,
|
||||
pendingInteraction, running: true, completed: false, updatedAt: 0,
|
||||
pendingInteraction, running: true, runningSubagentCount: 0, completed: false, updatedAt: 0,
|
||||
}
|
||||
const view = render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
@@ -347,7 +394,8 @@ describe('workspace browser rows', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const node: SessionNode = {
|
||||
id: sid('s1'), title: 'Quiet', blank: false, running: false, completed: false, updatedAt: 0,
|
||||
id: sid('s1'), title: 'Quiet', blank: false, running: false,
|
||||
runningSubagentCount: 0, completed: false, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
@@ -364,7 +412,8 @@ describe('workspace browser rows', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const node: SessionNode = {
|
||||
id: sid('s1'), title: 'Done', blank: false, running: false, completed: true, updatedAt: 0,
|
||||
id: sid('s1'), title: 'Done', blank: false, running: false,
|
||||
runningSubagentCount: 0, completed: true, updatedAt: 0,
|
||||
}
|
||||
render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()}
|
||||
onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
|
||||
@@ -379,7 +428,8 @@ describe('workspace browser rows', () => {
|
||||
|
||||
it('draggable row wires start/end and gates hover/drop on an active same-group drag', () => {
|
||||
const node: SessionNode = {
|
||||
id: sid('s1'), title: 'Drag me', blank: false, running: false, completed: false, updatedAt: 0,
|
||||
id: sid('s1'), title: 'Drag me', blank: false, running: false,
|
||||
runningSubagentCount: 0, completed: false, updatedAt: 0,
|
||||
}
|
||||
const inactive = dragProps()
|
||||
const { rerender } = render(
|
||||
|
||||
@@ -95,18 +95,35 @@ describe('deriveGroups', () => {
|
||||
|
||||
it('hides subagent-origin sessions without hiding ordinary forks', () => {
|
||||
const parent = summary('parent', 1)
|
||||
const fork = { ...summary('fork', 2), parentId: parent.id }
|
||||
const subagent = { ...summary('subagent', 3), parentId: parent.id, origin: 'subagent' as const }
|
||||
const sessions = { ...list(parent, fork, subagent), current: subagent.id }
|
||||
const subagent = {
|
||||
...summary('subagent', 3), parentId: parent.id, origin: 'subagent' as const, running: true,
|
||||
}
|
||||
const grandchild = {
|
||||
...summary('grandchild', 4), parentId: subagent.id, origin: 'subagent' as const, running: true,
|
||||
}
|
||||
const fork = { ...summary('fork', 2), parentId: subagent.id }
|
||||
const forkChild = {
|
||||
...summary('fork-child', 5), parentId: fork.id, origin: 'subagent' as const, running: true,
|
||||
}
|
||||
const sessions = { ...list(parent, fork, subagent, grandchild, forkChild), current: subagent.id }
|
||||
const groups = deriveGroups(
|
||||
sessions,
|
||||
[workspace('first', ['parent', 'fork', 'subagent'])],
|
||||
[workspace('first', ['parent', 'fork', 'subagent', 'grandchild', 'fork-child'])],
|
||||
noArchive,
|
||||
view(['first']),
|
||||
)
|
||||
|
||||
expect(groups[0]!.sessions.map(node => node.id)).toEqual([parent.id, fork.id])
|
||||
expect(groups[0]!.sessionCount).toBe(2)
|
||||
expect(groups[0]!.sessions[0]).toMatchObject({ running: false, runningSubagentCount: 2 })
|
||||
expect(groups[0]!.sessions[1]).toMatchObject({ running: false, runningSubagentCount: 1 })
|
||||
expect(deriveFlat(sessions, noArchive).map(node => [node.id, node.runningSubagentCount])).toEqual([
|
||||
[fork.id, 1], [parent.id, 2],
|
||||
])
|
||||
expect(deriveSearchResults(
|
||||
sessions, [workspace('first', ['parent', 'fork'])], 'parent', noArchive,
|
||||
{ items: [], hasMore: false }, 10,
|
||||
).items[0]).toMatchObject({ id: parent.id, runningSubagentCount: 2 })
|
||||
})
|
||||
|
||||
it('ignores fork lineage and sorts every ungrouped session as a top-level row', () => {
|
||||
@@ -274,6 +291,7 @@ describe('deriveSearchResults', () => {
|
||||
title: 'Needle title',
|
||||
workspace: 'Alpha',
|
||||
running: false,
|
||||
runningSubagentCount: 0,
|
||||
pendingInteraction: 'plan-review',
|
||||
completed: false,
|
||||
snippet: 'title session body excerpt',
|
||||
@@ -283,6 +301,7 @@ describe('deriveSearchResults', () => {
|
||||
title: 'Ordinary title',
|
||||
workspace: 'Needle Workspace',
|
||||
running: false,
|
||||
runningSubagentCount: 0,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
@@ -290,6 +309,7 @@ describe('deriveSearchResults', () => {
|
||||
title: 'content-hit',
|
||||
workspace: 'c',
|
||||
running: false,
|
||||
runningSubagentCount: 0,
|
||||
completed: false,
|
||||
snippet: 'body needle excerpt',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user