{visibleSections.map((section, index) => {
const sectionTitle = getSectionTitle(section.id, t)
@@ -476,15 +513,15 @@ export function SessionActivityPanel({
0 ? 'border-t border-[var(--color-border)] pt-4' : undefined}
+ className={index > 0 ? 'border-t border-[var(--color-border)] pt-3' : undefined}
>
-
-
-
+
+
+
{sectionTitle}
{section.rows.length > 0 ? (
-
+
{section.rows.length}
) : null}
diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx
index cd9eefd1..49f71ce7 100644
--- a/desktop/src/components/chat/MessageList.test.tsx
+++ b/desktop/src/components/chat/MessageList.test.tsx
@@ -951,6 +951,8 @@ describe('MessageList nested tool calls', () => {
const { container } = render()
expect(screen.getAllByText('Running').length).toBeGreaterThan(0)
+ expect(screen.queryByText(/Read .*example\.ts.*done/i)).toBeNull()
+ fireEvent.click(screen.getByRole('button', { name: /dispatched an agent/i }))
expect(screen.getByText(/Read .*example\.ts.*done/i)).toBeTruthy()
expect(container.textContent).toContain('Agent')
})
@@ -1107,6 +1109,9 @@ describe('MessageList nested tool calls', () => {
const groupSummary = screen.getByText('TaskUpdate (1), ran a command')
const groupButton = groupSummary.closest('button')
expect(groupButton?.textContent).not.toContain('check_circle')
+ expect(screen.queryByText('local_bash')).toBeNull()
+
+ fireEvent.click(groupButton!)
expect(screen.getByText('local_bash')).toBeTruthy()
})
@@ -1237,6 +1242,11 @@ describe('MessageList nested tool calls', () => {
render()
expect(screen.getByText('Saved 1 memory item(s)')).toBeTruthy()
+ expect(screen.queryByText('preferences.md')).toBeNull()
+ expect(screen.queryByText('Tool details')).toBeNull()
+
+ fireEvent.click(screen.getByRole('button', { name: /Saved 1 memory item/i }))
+
expect(screen.getByText('preferences.md')).toBeTruthy()
expect(screen.getByText('Tool details')).toBeTruthy()
const memoryCardClassName = screen.getByTestId('memory-tool-activity-card').className
@@ -1434,6 +1444,218 @@ describe('MessageList nested tool calls', () => {
])
})
+ it('honors a manual collapse of an agent group while more SubAgents stream in', async () => {
+ const initialMessages: UIMessage[] = [
+ {
+ id: 'tool-agent-a',
+ type: 'tool_use',
+ toolName: 'Agent',
+ toolUseId: 'agent-a',
+ input: { description: 'Review renderer' },
+ timestamp: 1,
+ },
+ {
+ id: 'result-agent-a',
+ type: 'tool_result',
+ toolUseId: 'agent-a',
+ content: 'Async agent launched successfully.',
+ isError: false,
+ timestamp: 2,
+ },
+ {
+ id: 'tool-agent-b',
+ type: 'tool_use',
+ toolName: 'Agent',
+ toolUseId: 'agent-b',
+ input: { description: 'Review stores' },
+ timestamp: 3,
+ },
+ {
+ id: 'result-agent-b',
+ type: 'tool_result',
+ toolUseId: 'agent-b',
+ content: 'Async agent launched successfully.',
+ isError: false,
+ timestamp: 4,
+ },
+ ]
+
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({ messages: initialMessages }),
+ },
+ })
+
+ render()
+
+ const agentGroupButton = screen.getByRole('button', { name: /dispatched 2 agents/i })
+ expect(screen.queryByText('Review renderer')).toBeNull()
+ fireEvent.click(agentGroupButton)
+ expect(screen.getByText('Review renderer')).toBeTruthy()
+ fireEvent.click(agentGroupButton)
+ expect(screen.queryByText('Review renderer')).toBeNull()
+
+ act(() => {
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({
+ chatState: 'tool_executing',
+ messages: [
+ ...initialMessages,
+ {
+ id: 'tool-agent-c',
+ type: 'tool_use',
+ toolName: 'Agent',
+ toolUseId: 'agent-c',
+ input: { description: 'Review coverage' },
+ timestamp: 5,
+ },
+ ],
+ }),
+ },
+ })
+ })
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /dispatched 3 agents/i })).toBeTruthy()
+ })
+ expect(screen.queryByText('Review renderer')).toBeNull()
+ expect(screen.queryByText('Review coverage')).toBeNull()
+ })
+
+ it('honors a manual collapse of mixed tool groups when nested tool calls arrive', async () => {
+ const initialMessages: UIMessage[] = [
+ {
+ id: 'tool-task-update',
+ type: 'tool_use',
+ toolName: 'TaskUpdate',
+ toolUseId: 'task-update-1',
+ input: { id: '1', status: 'in_progress' },
+ timestamp: 1,
+ },
+ {
+ id: 'tool-bash',
+ type: 'tool_use',
+ toolName: 'Bash',
+ toolUseId: 'bash-1',
+ input: { command: 'git status --short' },
+ timestamp: 2,
+ },
+ ]
+
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({
+ chatState: 'tool_executing',
+ messages: initialMessages,
+ }),
+ },
+ })
+
+ render()
+
+ const mixedGroupButton = screen.getByRole('button', { name: /TaskUpdate \(1\), ran a command/i })
+ expect(screen.queryByText('git status --short')).toBeNull()
+ fireEvent.click(mixedGroupButton)
+ expect(screen.getByText('git status --short')).toBeTruthy()
+ fireEvent.click(mixedGroupButton)
+ expect(screen.queryByText('git status --short')).toBeNull()
+
+ act(() => {
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({
+ chatState: 'tool_executing',
+ messages: [
+ ...initialMessages,
+ {
+ id: 'tool-child-read',
+ type: 'tool_use',
+ toolName: 'Read',
+ toolUseId: 'read-1',
+ input: { file_path: '/workspace/package.json' },
+ timestamp: 3,
+ parentToolUseId: 'task-update-1',
+ },
+ ],
+ }),
+ },
+ })
+ })
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /TaskUpdate \(1\), ran a command/i })).toBeTruthy()
+ })
+ expect(screen.queryByText('git status --short')).toBeNull()
+ expect(screen.queryByText('package.json')).toBeNull()
+ })
+
+ it('honors a manual collapse of memory activity when regular tools join the group', async () => {
+ const initialMessages: UIMessage[] = [
+ {
+ id: 'tool-memory-write',
+ type: 'tool_use',
+ toolName: 'Write',
+ toolUseId: 'memory-write-1',
+ input: {
+ file_path: '/Users/test/.codex/memory/project-notes.md',
+ content: 'Persisted context',
+ },
+ timestamp: 1,
+ },
+ {
+ id: 'result-memory-write',
+ type: 'tool_result',
+ toolUseId: 'memory-write-1',
+ content: 'Wrote 1 line to /Users/test/.codex/memory/project-notes.md',
+ isError: false,
+ timestamp: 2,
+ },
+ ]
+
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({ messages: initialMessages }),
+ },
+ })
+
+ render()
+
+ expect(screen.getByText('Saved 1 memory item(s)')).toBeTruthy()
+ const memoryActivityButton = screen.getByRole('button', { name: /Saved 1 memory item/i })
+ expect(screen.queryByText('project-notes.md')).toBeNull()
+ fireEvent.click(memoryActivityButton)
+ expect(screen.getByText('project-notes.md')).toBeTruthy()
+ fireEvent.click(memoryActivityButton)
+ expect(screen.queryByText('project-notes.md')).toBeNull()
+
+ act(() => {
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({
+ chatState: 'tool_executing',
+ messages: [
+ ...initialMessages,
+ {
+ id: 'tool-bash',
+ type: 'tool_use',
+ toolName: 'Bash',
+ toolUseId: 'bash-1',
+ input: { command: 'bun test memory.test.ts' },
+ timestamp: 3,
+ },
+ ],
+ }),
+ },
+ })
+ })
+
+ await waitFor(() => {
+ expect(screen.getByText('bun test memory.test.ts')).toBeTruthy()
+ })
+ expect(screen.queryByText('project-notes.md')).toBeNull()
+ })
+
it('keeps later nested tool calls under their parent after an interleaved user message', () => {
const messages: UIMessage[] = [
{
@@ -1541,6 +1763,7 @@ describe('MessageList nested tool calls', () => {
render()
+ fireEvent.click(screen.getByRole('button', { name: /dispatched an agent/i }))
expect(screen.getByText('Failed')).toBeTruthy()
expect(screen.getByText('Explore agent unavailable in this session')).toBeTruthy()
})
@@ -1584,6 +1807,7 @@ describe('MessageList nested tool calls', () => {
render()
+ fireEvent.click(screen.getByRole('button', { name: /dispatched an agent/i }))
expect(screen.getByText('Done')).toBeTruthy()
expect(screen.getByRole('button', { name: 'View result' })).toBeTruthy()
@@ -1616,6 +1840,7 @@ describe('MessageList nested tool calls', () => {
render()
+ fireEvent.click(screen.getByRole('button', { name: /dispatched an agent/i }))
fireEvent.click(screen.getByRole('button', { name: 'Open run Inspect src/components' }))
const expectedTabId = '__subagent__active-tab__agent-1'
@@ -1702,6 +1927,7 @@ describe('MessageList nested tool calls', () => {
render()
+ fireEvent.click(screen.getByRole('button', { name: /dispatched an agent/i }))
expect(screen.getByText('Done')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'View result' }))
@@ -1756,6 +1982,7 @@ describe('MessageList nested tool calls', () => {
render()
+ fireEvent.click(screen.getByRole('button', { name: /dispatched an agent/i }))
expect(screen.getByText(/最终报告应该按 Markdown 展示。/)).toBeTruthy()
expect(screen.queryByText(/raw structured JSON should not be shown/)).toBeNull()
@@ -1819,6 +2046,7 @@ describe('MessageList nested tool calls', () => {
render()
+ fireEvent.click(screen.getByRole('button', { name: /dispatched an agent/i }))
expect(screen.getByText(/git:v0\.2\.6\.\.v0\.2\.7:0/)).toBeTruthy()
expect(screen.queryByText(/\{"results"/)).toBeNull()
diff --git a/desktop/src/components/chat/ToolCallGroup.tsx b/desktop/src/components/chat/ToolCallGroup.tsx
index b0e4bdb3..efe4da18 100644
--- a/desktop/src/components/chat/ToolCallGroup.tsx
+++ b/desktop/src/components/chat/ToolCallGroup.tsx
@@ -1,4 +1,4 @@
-import { memo, useEffect, useState } from 'react'
+import { memo, useCallback, useState } from 'react'
import { BookMarked, ChevronDown, ChevronRight, Settings } from 'lucide-react'
import { ToolCallBlock } from './ToolCallBlock'
import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
@@ -27,6 +27,16 @@ type MemoryToolActivity = {
files: MemoryToolFile[]
}
+function useExpandableCardState() {
+ const [expanded, setExpanded] = useState(false)
+
+ const toggleExpanded = useCallback(() => {
+ setExpanded((value) => !value)
+ }, [])
+
+ return { expanded, toggleExpanded }
+}
+
type Props = {
sessionId?: string | null
toolCalls: ToolCall[]
@@ -34,7 +44,7 @@ type Props = {
childToolCallsByParent: Map
agentTaskNotifications: Record
showOpenRun?: boolean
- /** When true, the last tool is still executing — show expanded */
+ /** When true, the last tool is still executing. */
isStreaming?: boolean
}
@@ -124,16 +134,16 @@ export const ToolCallGroup = memo(function ToolCallGroup({
if (memoryActivity) {
const memoryToolCalls = toolCalls.filter(isMemoryToolCall)
const regularToolCalls = toolCalls.filter((toolCall) => !isMemoryToolCall(toolCall))
- if (regularToolCalls.length > 0) {
- return (
-
-
+ return (
+
0 ? 'mb-2 space-y-2' : ''}>
+
+ {regularToolCalls.length > 0 ? (
-
- )
- }
- return (
-
+ ) : null}
+
)
}
@@ -231,7 +232,7 @@ function MemoryToolActivityGroup({
childToolCallsByParent: Map
isStreaming?: boolean
}) {
- const [expanded, setExpanded] = useState(activity.action === 'saved')
+ const { expanded, toggleExpanded } = useExpandableCardState()
const [detailsExpanded, setDetailsExpanded] = useState(false)
const t = useTranslation()
const titleKey = activity.action === 'saved'
@@ -240,10 +241,6 @@ function MemoryToolActivityGroup({
const visibleFiles = activity.files.slice(0, 4)
const hiddenCount = Math.max(0, activity.files.length - visibleFiles.length)
- useEffect(() => {
- if (isStreaming) setExpanded(true)
- }, [isStreaming])
-
return (