Skip to content

Commit 990494c

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 79abd52 commit 990494c

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

1011
import type { ChatMessage, ContentBlock } from '../types/chat'
@@ -105,78 +106,19 @@ export function useChatMessages({
105106
// Handle blocks within messages
106107
if (!message.blocks) return message
107108

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

177119
return {
178120
...message,
179-
blocks: updateBlocksRecursively(message.blocks),
121+
blocks: updatedBlocks,
180122
}
181123
})
182124
})

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)