Skip to content

Commit 6396ac4

Browse files
committed
feat: auto-expand root level agents only and remember the agents user
manually opens
1 parent 1e8669d commit 6396ac4

File tree

4 files changed

+122
-1
lines changed

4 files changed

+122
-1
lines changed

cli/src/chat.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { useShallow } from 'zustand/react/shallow'
33

4+
import type { ContentBlock } from './types/chat'
5+
46
import { routeUserPrompt } from './commands/router'
57
import { AgentModeToggle } from './components/agent-mode-toggle'
68
import { BuildModeButtons } from './components/build-mode-buttons'
@@ -87,6 +89,11 @@ export const App = ({
8789
null,
8890
)
8991

92+
// Track which agent toggles the user has manually opened.
93+
const [userOpenedAgents, setUserOpenedAgents] = useState<Set<string>>(
94+
new Set(),
95+
)
96+
9097
const {
9198
inputValue,
9299
setInputValue,
@@ -149,6 +156,29 @@ export const App = ({
149156
})),
150157
)
151158

159+
// Memoize toggle IDs extraction - only recompute when messages change
160+
const allToggleIds = useMemo(() => {
161+
const ids = new Set<string>()
162+
163+
const extractFromBlocks = (blocks: ContentBlock[] | undefined) => {
164+
if (!blocks) return
165+
for (const block of blocks) {
166+
if (block.type === 'agent') {
167+
ids.add(block.agentId)
168+
extractFromBlocks(block.blocks)
169+
} else if (block.type === 'tool') {
170+
ids.add(block.toolCallId)
171+
}
172+
}
173+
}
174+
175+
for (const message of messages) {
176+
extractFromBlocks(message.blocks)
177+
}
178+
179+
return ids
180+
}, [messages])
181+
152182
const {
153183
isAuthenticated,
154184
setIsAuthenticated,
@@ -559,12 +589,15 @@ export const App = ({
559589
)
560590

561591
const { sendMessage, clearMessages } = useSendMessage({
592+
messages,
593+
allToggleIds,
562594
setMessages,
563595
setFocusedAgentId,
564596
setInputFocused,
565597
inputRef,
566598
setStreamingAgents,
567599
setCollapsedAgents,
600+
userOpenedAgents,
568601
activeSubagentsRef,
569602
isChainInProgressRef,
570603
setActiveSubagents,
@@ -694,6 +727,8 @@ export const App = ({
694727
timer: mainAgentTimer,
695728
setCollapsedAgents,
696729
setFocusedAgentId,
730+
userOpenedAgents,
731+
setUserOpenedAgents,
697732
})
698733

699734
const virtualizationNotice =

cli/src/hooks/use-message-renderer.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ interface UseMessageRendererProps {
2727
timer: ElapsedTimeTracker
2828
setCollapsedAgents: React.Dispatch<React.SetStateAction<Set<string>>>
2929
setFocusedAgentId: React.Dispatch<React.SetStateAction<string | null>>
30+
userOpenedAgents: Set<string>
31+
setUserOpenedAgents: React.Dispatch<React.SetStateAction<Set<string>>>
3032
}
3133

3234
export const useMessageRenderer = (
@@ -45,6 +47,8 @@ export const useMessageRenderer = (
4547
timer,
4648
setCollapsedAgents,
4749
setFocusedAgentId,
50+
userOpenedAgents,
51+
setUserOpenedAgents,
4852
} = props
4953

5054
return useMemo(() => {
@@ -94,6 +98,8 @@ export const useMessageRenderer = (
9498
e.stopPropagation()
9599
}
96100

101+
const wasCollapsed = collapsedAgents.has(message.id)
102+
97103
setCollapsedAgents((prev) => {
98104
const next = new Set(prev)
99105

@@ -108,6 +114,19 @@ export const useMessageRenderer = (
108114
return next
109115
})
110116

117+
// Track user interaction: if they're opening it, mark as user-opened
118+
setUserOpenedAgents((prev) => {
119+
const next = new Set(prev)
120+
if (wasCollapsed) {
121+
// User is opening it, mark as user-opened
122+
next.add(message.id)
123+
} else {
124+
// User is closing it, remove from user-opened
125+
next.delete(message.id)
126+
}
127+
return next
128+
})
129+
111130
setFocusedAgentId(message.id)
112131
}
113132

@@ -349,6 +368,7 @@ export const useMessageRenderer = (
349368
collapsedAgents={collapsedAgents}
350369
streamingAgents={streamingAgents}
351370
onToggleCollapsed={(id: string) => {
371+
const wasCollapsed = collapsedAgents.has(id)
352372
setCollapsedAgents((prev) => {
353373
const next = new Set(prev)
354374
if (next.has(id)) {
@@ -358,6 +378,18 @@ export const useMessageRenderer = (
358378
}
359379
return next
360380
})
381+
// Track user interaction
382+
setUserOpenedAgents((prev) => {
383+
const next = new Set(prev)
384+
if (wasCollapsed) {
385+
// User is opening it, mark as user-opened
386+
next.add(id)
387+
} else {
388+
// User is closing it, remove from user-opened
389+
next.delete(id)
390+
}
391+
return next
392+
})
361393
}}
362394
/>
363395
</box>
@@ -397,6 +429,7 @@ export const useMessageRenderer = (
397429
collapsedAgents={collapsedAgents}
398430
streamingAgents={streamingAgents}
399431
onToggleCollapsed={(id: string) => {
432+
const wasCollapsed = collapsedAgents.has(id)
400433
setCollapsedAgents((prev) => {
401434
const next = new Set(prev)
402435
if (next.has(id)) {
@@ -406,6 +439,18 @@ export const useMessageRenderer = (
406439
}
407440
return next
408441
})
442+
// Track user interaction
443+
setUserOpenedAgents((prev) => {
444+
const next = new Set(prev)
445+
if (wasCollapsed) {
446+
// User is opening it, mark as user-opened
447+
next.add(id)
448+
} else {
449+
// User is closing it, remove from user-opened
450+
next.delete(id)
451+
}
452+
return next
453+
})
409454
}}
410455
/>
411456
</box>
@@ -443,6 +488,8 @@ export const useMessageRenderer = (
443488
streamingAgents,
444489
isWaitingForResponse,
445490
setCollapsedAgents,
491+
setUserOpenedAgents,
446492
setFocusedAgentId,
493+
userOpenedAgents,
447494
])
448495
}

cli/src/hooks/use-send-message.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { has } from 'lodash'
22
import { useCallback, useEffect, useRef } from 'react'
33

44
import { getCodebuffClient, formatToolOutput } from '../utils/codebuff-client'
5-
import { shouldHideAgent } from '../utils/constants'
5+
import { MAIN_AGENT_ID, shouldHideAgent } from '../utils/constants'
66
import { createValidationErrorBlocks } from '../utils/create-validation-error-blocks'
77
import { getErrorObject } from '../utils/error'
88
import { formatTimestamp } from '../utils/helpers'
@@ -49,6 +49,7 @@ const updateBlocksRecursively = (
4949
})
5050
}
5151

52+
5253
export type SendMessageTimerEvent =
5354
| {
5455
type: 'start'
@@ -142,12 +143,15 @@ export const createSendMessageTimerController = (
142143
}
143144

144145
interface UseSendMessageOptions {
146+
messages: ChatMessage[]
147+
allToggleIds: Set<string>
145148
setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>
146149
setFocusedAgentId: (id: string | null) => void
147150
setInputFocused: (focused: boolean) => void
148151
inputRef: React.MutableRefObject<any>
149152
setStreamingAgents: React.Dispatch<React.SetStateAction<Set<string>>>
150153
setCollapsedAgents: React.Dispatch<React.SetStateAction<Set<string>>>
154+
userOpenedAgents: Set<string>
151155
activeSubagentsRef: React.MutableRefObject<Set<string>>
152156
isChainInProgressRef: React.MutableRefObject<boolean>
153157
setActiveSubagents: React.Dispatch<React.SetStateAction<Set<string>>>
@@ -171,12 +175,15 @@ interface UseSendMessageOptions {
171175
}
172176

173177
export const useSendMessage = ({
178+
messages,
179+
allToggleIds,
174180
setMessages,
175181
setFocusedAgentId,
176182
setInputFocused,
177183
inputRef,
178184
setStreamingAgents,
179185
setCollapsedAgents,
186+
userOpenedAgents,
180187
activeSubagentsRef,
181188
isChainInProgressRef,
182189
setActiveSubagents,
@@ -340,6 +347,10 @@ export const useSendMessage = ({
340347
agentId,
341348
})
342349

350+
// Use memoized toggle IDs from the store selector
351+
// This is computed efficiently in the Zustand store
352+
const previousToggleIds = allToggleIds
353+
343354
// Add user message to UI first
344355
const userMessage: ChatMessage = {
345356
id: `user-${Date.now()}`,
@@ -360,6 +371,19 @@ export const useSendMessage = ({
360371
})
361372
await yieldToEventLoop()
362373

374+
// Auto-collapse previous message toggles to minimize clutter.
375+
// Respects user intent by keeping toggles open that the user manually expanded.
376+
setCollapsedAgents((prev) => {
377+
const next = new Set(prev)
378+
// Add all previous toggle IDs to collapsed, except those the user manually opened
379+
for (const id of previousToggleIds) {
380+
if (!userOpenedAgents.has(id)) {
381+
next.add(id)
382+
}
383+
}
384+
return next
385+
})
386+
363387
// Scroll to bottom after user message appears
364388
setTimeout(() => scrollToLatest(), 0)
365389

@@ -966,6 +990,10 @@ export const useSendMessage = ({
966990
setCollapsedAgents((prev) => {
967991
const next = new Set(prev)
968992
next.delete(tempId)
993+
// Only collapse if parent is NOT main agent (i.e., it's a nested agent)
994+
if (event.parentAgentId && event.parentAgentId !== MAIN_AGENT_ID) {
995+
next.add(event.agentId)
996+
}
969997
return next
970998
})
971999

@@ -1062,6 +1090,10 @@ export const useSendMessage = ({
10621090
)
10631091

10641092
setStreamingAgents((prev) => new Set(prev).add(event.agentId))
1093+
// Only collapse if parent is NOT main agent (i.e., it's a nested agent)
1094+
if (event.parentAgentId && event.parentAgentId !== MAIN_AGENT_ID) {
1095+
setCollapsedAgents((prev) => new Set(prev).add(event.agentId))
1096+
}
10651097
}
10661098
}
10671099
} else if (event.type === 'subagent_finish') {
@@ -1438,6 +1470,8 @@ export const useSendMessage = ({
14381470
inputRef,
14391471
setStreamingAgents,
14401472
setCollapsedAgents,
1473+
allToggleIds,
1474+
userOpenedAgents,
14411475
activeSubagentsRef,
14421476
isChainInProgressRef,
14431477
setIsWaitingForResponse,

cli/src/utils/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,10 @@ export const shouldHideAgent = (agentId: string): boolean => {
88
return HIDDEN_AGENT_IDS.some((hiddenId) => agentId.includes(hiddenId))
99
}
1010

11+
/**
12+
* The parent agent ID for all root-level agents
13+
*/
14+
export const MAIN_AGENT_ID = 'main-agent'
15+
1116
const agentModes = ['FAST', 'MAX', 'PLAN'] as const
1217
export type AgentMode = (typeof agentModes)[number]

0 commit comments

Comments
 (0)