Skip to content

Commit eebabc8

Browse files
committed
refactor(cli): consolidate block utils and add tree traversal primitives (Commit 2.2)
Creates centralized block-tree-utils.ts with reusable tree traversal primitives: - traverseBlocks: visit all blocks with early exit support - findBlockByPredicate: find first matching block - mapBlocks: transform blocks with reference preservation - updateBlockById: update by any ID type (agentId, toolCallId, thinkingId, id) - updateAgentBlockById: update specifically agent blocks - toggleBlockCollapse: toggle collapsed state with userOpened tracking Comprehensive tests (33 tests) including: - Deep nesting (3+ levels) coverage - Proper type narrowing (isAgentBlock guard instead of "as any") - Multiple toggle cycle verification - Parent + children transformation tests
1 parent ed2c97f commit eebabc8

File tree

5 files changed

+884
-189
lines changed

5 files changed

+884
-189
lines changed

cli/src/hooks/use-chat-messages.ts

Lines changed: 11 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
77

8+
import { toggleBlockCollapse, updateBlockById } from '../utils/block-tree-utils'
89
import { buildMessageTree } from '../utils/message-tree-utils'
910
import { setAllBlocksCollapsedState, hasAnyExpandedBlocks } from '../utils/collapse-helpers'
1011

@@ -108,78 +109,19 @@ export function useChatMessages({
108109
// Handle blocks within messages
109110
if (!message.blocks) return message
110111

111-
const updateBlocksRecursively = (
112-
blocks: ContentBlock[],
113-
): ContentBlock[] => {
114-
let foundTarget = false
115-
const result = blocks.map((block) => {
116-
// Handle thinking blocks - just match by thinkingId
117-
if (block.type === 'text' && block.thinkingId === id) {
118-
foundTarget = true
119-
const wasCollapsed = block.isCollapsed ?? false
120-
return {
121-
...block,
122-
isCollapsed: !wasCollapsed,
123-
userOpened: wasCollapsed, // Mark as user-opened if expanding
124-
}
125-
}
126-
127-
// Handle agent blocks
128-
if (block.type === 'agent' && block.agentId === id) {
129-
foundTarget = true
130-
const wasCollapsed = block.isCollapsed ?? false
131-
return {
132-
...block,
133-
isCollapsed: !wasCollapsed,
134-
userOpened: wasCollapsed, // Mark as user-opened if expanding
135-
}
136-
}
137-
138-
// Handle tool blocks
139-
if (block.type === 'tool' && block.toolCallId === id) {
140-
foundTarget = true
141-
const wasCollapsed = block.isCollapsed ?? false
142-
return {
143-
...block,
144-
isCollapsed: !wasCollapsed,
145-
userOpened: wasCollapsed, // Mark as user-opened if expanding
146-
}
147-
}
148-
149-
// Handle agent-list blocks
150-
if (block.type === 'agent-list' && block.id === id) {
151-
foundTarget = true
152-
const wasCollapsed = block.isCollapsed ?? false
153-
return {
154-
...block,
155-
isCollapsed: !wasCollapsed,
156-
userOpened: wasCollapsed, // Mark as user-opened if expanding
157-
}
158-
}
159-
160-
// Recursively update nested blocks inside agent blocks
161-
if (block.type === 'agent' && block.blocks) {
162-
const updatedBlocks = updateBlocksRecursively(block.blocks)
163-
// Only create new block if nested blocks actually changed
164-
if (updatedBlocks !== block.blocks) {
165-
foundTarget = true
166-
return {
167-
...block,
168-
blocks: updatedBlocks,
169-
}
170-
}
171-
}
172-
173-
return block
174-
})
175-
176-
// Return original array reference if nothing changed
177-
return foundTarget ? result : blocks
178-
}
112+
// Use unified block tree utility + shared collapse helper
113+
const updatedBlocks = updateBlockById(
114+
message.blocks,
115+
id,
116+
toggleBlockCollapse,
117+
)
118+
119+
// Only create new message if blocks actually changed
120+
if (updatedBlocks === message.blocks) return message
179121

180122
return {
181123
...message,
182-
blocks: updateBlocksRecursively(message.blocks),
124+
blocks: updatedBlocks,
183125
}
184126
})
185127
})

cli/src/types/chat.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ export type ContentBlock =
141141
| ToolContentBlock
142142
| PlanContentBlock
143143

144+
/**
145+
* Block types that support collapse state (isCollapsed/userOpened).
146+
* Used for type-safe collapse toggling operations.
147+
*/
148+
export type CollapsibleBlock =
149+
| AgentContentBlock
150+
| AgentListContentBlock
151+
| ImageContentBlock
152+
| TextContentBlock
153+
| ToolContentBlock
154+
144155
export type AgentMessage = {
145156
agentName: string
146157
agentType: string
@@ -225,3 +236,16 @@ export function isAskUserBlock(
225236
export function isImageBlock(block: ContentBlock): block is ImageContentBlock {
226237
return block.type === 'image'
227238
}
239+
240+
/**
241+
* Type guard for blocks that support collapse state (isCollapsed/userOpened).
242+
*/
243+
export function isCollapsibleBlock(block: ContentBlock): block is CollapsibleBlock {
244+
return (
245+
block.type === 'agent' ||
246+
block.type === 'agent-list' ||
247+
block.type === 'image' ||
248+
block.type === 'text' ||
249+
block.type === 'tool'
250+
)
251+
}

0 commit comments

Comments
 (0)