Skip to content

Commit 29f098b

Browse files
committed
Refactor message block tool branches
1 parent 2052bb0 commit 29f098b

File tree

3 files changed

+281
-27
lines changed

3 files changed

+281
-27
lines changed
Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,11 @@
11
import { TextAttributes, type BorderCharacters } from '@opentui/core'
22
import React, { type ReactNode } from 'react'
33

4-
const containerBorderChars: BorderCharacters = {
5-
topLeft: '╭',
6-
topRight: '╮',
7-
bottomLeft: '╰',
8-
bottomRight: '╯',
9-
horizontal: '─',
10-
vertical: '│',
11-
topT: '┬',
12-
bottomT: '┴',
13-
leftT: '├',
14-
rightT: '┤',
15-
cross: '┼',
16-
}
4+
import { RaisedPill } from './raised-pill'
175

186
import type { ChatTheme } from '../utils/theme-system'
19-
import { RaisedPill } from './raised-pill'
207

21-
interface BranchItemProps {
8+
interface AgentBranchItemProps {
229
name: string
2310
content: ReactNode
2411
prompt?: string
@@ -38,7 +25,21 @@ interface BranchItemProps {
3825
titleSuffix?: string
3926
}
4027

41-
export const BranchItem = ({
28+
const containerBorderChars: BorderCharacters = {
29+
topLeft: '╭',
30+
topRight: '╮',
31+
bottomLeft: '╰',
32+
bottomRight: '╯',
33+
horizontal: '─',
34+
vertical: '│',
35+
topT: '┬',
36+
bottomT: '┴',
37+
leftT: '├',
38+
rightT: '┤',
39+
cross: '┼',
40+
}
41+
42+
export const AgentBranchItem = ({
4243
name,
4344
content,
4445
prompt,
@@ -56,7 +57,7 @@ export const BranchItem = ({
5657
showBorder = true,
5758
toggleEnabled = true,
5859
titleSuffix,
59-
}: BranchItemProps) => {
60+
}: AgentBranchItemProps) => {
6061
const resolveFg = (
6162
color?: string | null,
6263
fallback?: string | null,
@@ -99,8 +100,7 @@ export const BranchItem = ({
99100
: `${statusIndicator} ${statusLabel}`
100101
: null
101102
const showCollapsedPreview =
102-
(isStreaming && !!streamingPreview) ||
103-
(!isStreaming && !!finishedPreview)
103+
(isStreaming && !!streamingPreview) || (!isStreaming && !!finishedPreview)
104104

105105
const isTextRenderable = (value: ReactNode): boolean => {
106106
if (value === null || value === undefined || typeof value === 'boolean') {

cli/src/components/message-block.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import stringWidth from 'string-width'
44

55
import { pluralize } from '@codebuff/common/util/string'
66

7-
import { BranchItem } from './branch-item'
7+
import { AgentBranchItem } from './agent-branch-item'
8+
import { ToolCallItem } from './tool-call-item'
89
import { getToolDisplayInfo } from '../utils/codebuff-client'
910
import { getToolRenderConfig } from './tool-renderer'
1011
import {
@@ -161,7 +162,7 @@ export const MessageBlock = ({
161162
}
162163

163164
const displayInfo = getToolDisplayInfo(toolBlock.toolName)
164-
const isCollapsed = true
165+
const isCollapsed = collapsedAgents.has(toolBlock.toolCallId)
165166
const isStreaming = streamingAgents.has(toolBlock.toolCallId)
166167

167168
const inputContent = `\`\`\`json\n${JSON.stringify(toolBlock.input, null, 2)}\n\`\`\``
@@ -271,18 +272,16 @@ export const MessageBlock = ({
271272
key={keyPrefix}
272273
ref={(el: any) => registerAgentRef(toolBlock.toolCallId, el)}
273274
>
274-
<BranchItem
275+
<ToolCallItem
275276
name={headerName}
276277
content={combinedContent}
277-
agentId={toolBlock.agentId}
278278
isCollapsed={isCollapsed}
279279
isStreaming={isStreaming}
280280
branchChar={branchChar}
281281
streamingPreview={streamingPreview}
282282
finishedPreview={finishedPreview}
283283
theme={theme}
284-
showBorder={false}
285-
toggleEnabled={false}
284+
onToggle={() => onToggleCollapsed(toolBlock.toolCallId)}
286285
titleSuffix={toolRenderConfig.path}
287286
/>
288287
</box>
@@ -349,7 +348,7 @@ export const MessageBlock = ({
349348
ref={(el: any) => registerAgentRef(agentBlock.agentId, el)}
350349
style={{ flexDirection: 'column', gap: 0 }}
351350
>
352-
<BranchItem
351+
<AgentBranchItem
353352
name={agentBlock.agentName}
354353
content={displayContent}
355354
prompt={agentBlock.initialPrompt}
@@ -438,7 +437,7 @@ export const MessageBlock = ({
438437
key={keyPrefix}
439438
ref={(el: any) => registerAgentRef(agentListBlock.id, el)}
440439
>
441-
<BranchItem
440+
<AgentBranchItem
442441
name={headerText}
443442
content={agentListContent}
444443
agentId={agentListBlock.id}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { TextAttributes } from '@opentui/core'
2+
import React, { type ReactNode } from 'react'
3+
4+
import type { ChatTheme } from '../utils/theme-system'
5+
import { resolveThemeColor } from '../utils/theme-system'
6+
7+
interface ToolCallItemProps {
8+
name: string
9+
content: ReactNode
10+
isCollapsed: boolean
11+
isStreaming: boolean
12+
branchChar: string
13+
streamingPreview: string
14+
finishedPreview: string
15+
theme: ChatTheme
16+
onToggle?: () => void
17+
titleSuffix?: string
18+
}
19+
20+
const isTextRenderable = (value: ReactNode): boolean => {
21+
if (value === null || value === undefined || typeof value === 'boolean') {
22+
return false
23+
}
24+
25+
if (typeof value === 'string' || typeof value === 'number') {
26+
return true
27+
}
28+
29+
if (Array.isArray(value)) {
30+
return value.every((child) => isTextRenderable(child))
31+
}
32+
33+
if (React.isValidElement(value)) {
34+
if (value.type === React.Fragment) {
35+
return isTextRenderable(value.props.children)
36+
}
37+
38+
if (typeof value.type === 'string') {
39+
if (
40+
value.type === 'span' ||
41+
value.type === 'strong' ||
42+
value.type === 'em'
43+
) {
44+
return isTextRenderable(value.props.children)
45+
}
46+
47+
return false
48+
}
49+
}
50+
51+
return false
52+
}
53+
54+
const renderExpandedContent = (
55+
value: ReactNode,
56+
theme: ChatTheme,
57+
fallbackTextColor: string,
58+
getAttributes: (extra?: number) => number | undefined,
59+
): ReactNode => {
60+
if (
61+
value === null ||
62+
value === undefined ||
63+
value === false ||
64+
value === true
65+
) {
66+
return null
67+
}
68+
69+
if (isTextRenderable(value)) {
70+
return (
71+
<text
72+
fg={resolveThemeColor(theme.agentText) ?? fallbackTextColor}
73+
key="tool-expanded-text"
74+
attributes={getAttributes()}
75+
>
76+
{value}
77+
</text>
78+
)
79+
}
80+
81+
if (React.isValidElement(value)) {
82+
if (value.key === null || value.key === undefined) {
83+
return (
84+
<box key="tool-expanded-node" style={{ flexDirection: 'column', gap: 0 }}>
85+
{value}
86+
</box>
87+
)
88+
}
89+
return value
90+
}
91+
92+
if (Array.isArray(value)) {
93+
return (
94+
<box key="tool-expanded-array" style={{ flexDirection: 'column', gap: 0 }}>
95+
{value.map((child, idx) => (
96+
<box
97+
key={`tool-expanded-array-${idx}`}
98+
style={{ flexDirection: 'column', gap: 0 }}
99+
>
100+
{child}
101+
</box>
102+
))}
103+
</box>
104+
)
105+
}
106+
107+
return (
108+
<box key="tool-expanded-unknown" style={{ flexDirection: 'column', gap: 0 }}>
109+
{value}
110+
</box>
111+
)
112+
}
113+
114+
export const ToolCallItem = ({
115+
name,
116+
content,
117+
isCollapsed,
118+
isStreaming,
119+
branchChar,
120+
streamingPreview,
121+
finishedPreview,
122+
theme,
123+
onToggle,
124+
titleSuffix,
125+
}: ToolCallItemProps) => {
126+
const resolveFg = (
127+
color?: string | null,
128+
fallback?: string | null,
129+
): string | undefined => {
130+
if (color && color !== 'default') return color
131+
if (fallback && fallback !== 'default') return fallback
132+
return undefined
133+
}
134+
135+
const fallbackTextColor =
136+
resolveFg(theme.agentContentText) ??
137+
resolveFg(theme.chromeText) ??
138+
'#d1d5e5'
139+
140+
const baseTextAttributes = theme.messageTextAttributes ?? 0
141+
const getAttributes = (extra: number = 0): number | undefined => {
142+
const combined = baseTextAttributes | extra
143+
return combined === 0 ? undefined : combined
144+
}
145+
146+
const isExpanded = !isCollapsed
147+
const toggleLabelColor = theme.chromeText ?? theme.agentToggleHeaderBg
148+
const toggleIndicator = onToggle ? (isCollapsed ? '▸ ' : '▾ ') : ''
149+
const toggleLabel = `${branchChar}${toggleIndicator}`
150+
const toggleLabelFg = resolveFg(toggleLabelColor, fallbackTextColor)
151+
const headerFg = resolveFg(theme.agentToggleHeaderText, fallbackTextColor)
152+
const collapsedPreviewText = isStreaming ? streamingPreview : finishedPreview
153+
const showCollapsedPreview = collapsedPreviewText.length > 0
154+
155+
return (
156+
<box style={{ flexDirection: 'column', gap: 0, width: '100%' }}>
157+
<box
158+
style={{
159+
flexDirection: 'column',
160+
gap: 0,
161+
paddingLeft: 0,
162+
paddingRight: 0,
163+
paddingTop: 0,
164+
paddingBottom: 0,
165+
width: '100%',
166+
}}
167+
>
168+
<box
169+
style={{
170+
flexDirection: 'row',
171+
alignItems: 'center',
172+
paddingLeft: 0,
173+
paddingRight: 0,
174+
paddingTop: 0,
175+
paddingBottom: isCollapsed ? 0 : 1,
176+
width: '100%',
177+
}}
178+
onMouseDown={onToggle}
179+
>
180+
<text style={{ wrapMode: 'none' }}>
181+
<span
182+
{...(toggleLabelFg ? { fg: toggleLabelFg } : undefined)}
183+
attributes={isExpanded ? TextAttributes.BOLD : undefined}
184+
>
185+
{toggleLabel}
186+
</span>
187+
<span
188+
{...(headerFg ? { fg: headerFg } : undefined)}
189+
attributes={TextAttributes.BOLD}
190+
>
191+
{name}
192+
</span>
193+
{titleSuffix ? (
194+
<span
195+
{...(headerFg ? { fg: headerFg } : undefined)}
196+
attributes={TextAttributes.BOLD}
197+
>
198+
{` ${titleSuffix}`}
199+
</span>
200+
) : null}
201+
{isStreaming ? (
202+
<span
203+
fg={resolveFg(theme.statusAccent, fallbackTextColor)}
204+
attributes={TextAttributes.DIM}
205+
>
206+
{' running'}
207+
</span>
208+
) : null}
209+
</text>
210+
</box>
211+
212+
{isCollapsed ? (
213+
showCollapsedPreview ? (
214+
<box
215+
style={{
216+
paddingLeft: 0,
217+
paddingRight: 0,
218+
paddingTop: 0,
219+
paddingBottom: 0,
220+
}}
221+
>
222+
<text
223+
fg={resolveFg(
224+
isStreaming ? theme.agentText : theme.agentResponseCount,
225+
fallbackTextColor,
226+
)}
227+
attributes={getAttributes(TextAttributes.ITALIC)}
228+
>
229+
{collapsedPreviewText}
230+
</text>
231+
</box>
232+
) : null
233+
) : (
234+
<box
235+
style={{
236+
flexDirection: 'column',
237+
gap: 0,
238+
paddingLeft: 0,
239+
paddingRight: 0,
240+
paddingTop: 0,
241+
paddingBottom: 0,
242+
}}
243+
>
244+
{renderExpandedContent(
245+
content,
246+
theme,
247+
fallbackTextColor ?? '#d1d5e5',
248+
getAttributes,
249+
)}
250+
</box>
251+
)}
252+
</box>
253+
</box>
254+
)
255+
}

0 commit comments

Comments
 (0)