11import { TextAttributes } from '@opentui/core'
22import React , { type ReactNode } from 'react'
33
4+ import { ShimmerText } from './shimmer-text'
45import type { ChatTheme } from '../utils/theme-system'
56
7+ export interface BranchQuickAction {
8+ id : string
9+ label : string
10+ onSelect : ( ) => void
11+ icon ?: string
12+ }
13+
614interface BranchItemProps {
715 name : string
8- content : ReactNode
16+ content : ReactNode | ( ( ) => ReactNode )
917 isCollapsed : boolean
1018 isStreaming : boolean
1119 branchChar : string
1220 streamingPreview : string
1321 finishedPreview : string
1422 theme : ChatTheme
1523 onToggle : ( ) => void
24+ quickActions ?: BranchQuickAction [ ]
25+ variant ?: 'agent' | 'tool'
1626}
1727
1828const BRANCH_BORDER_CHARS = {
@@ -39,18 +49,42 @@ export const BranchItem = ({
3949 finishedPreview,
4050 theme,
4151 onToggle,
52+ quickActions = [ ] ,
53+ variant = 'agent' ,
4254} : BranchItemProps ) => {
4355 const indentPrefix = branchChar ? branchChar . replace ( / ./ g, ' ' ) : ''
44- const cornerColor = theme . agentPrefix
45- const shouldMergeHeader = ! isCollapsed && ! ! content
56+ const baseCornerColor = theme . agentPrefix
57+ const hasContent = typeof content === 'function' ? true : ! ! content
58+ const shouldMergeHeader = ! isCollapsed && hasContent
4659 const outerPaddingRight = branchChar ? 1 : 0
60+ const baseHeaderBackground = isCollapsed
61+ ? theme . agentResponseCount
62+ : theme . agentPrefix
63+ const effectiveCornerColor =
64+ variant === 'tool' ? theme . toolBorder ?? baseCornerColor : baseCornerColor
65+ const headerBackground =
66+ variant === 'tool'
67+ ? theme . toolBorder ?? baseHeaderBackground
68+ : baseHeaderBackground
69+ const contentBackground =
70+ variant === 'tool' ? theme . toolBg ?? theme . agentContentBg : undefined
4771
48- const isTextRenderable = ( value : ReactNode ) : boolean => {
72+ const handleActionSelect = ( action : BranchQuickAction , event : any ) : void => {
73+ if ( event && typeof event . stopPropagation === 'function' ) {
74+ event . stopPropagation ( )
75+ }
4976 if (
50- value === null ||
51- value === undefined ||
52- typeof value === 'boolean '
77+ event &&
78+ 'preventDefault' in event &&
79+ typeof event . preventDefault === 'function '
5380 ) {
81+ event . preventDefault ( )
82+ }
83+ action . onSelect ( )
84+ }
85+
86+ const isTextRenderable = ( value : ReactNode ) : boolean => {
87+ if ( value === null || value === undefined || typeof value === 'boolean' ) {
5488 return false
5589 }
5690
@@ -116,7 +150,10 @@ export const BranchItem = ({
116150 return (
117151 < box key = "expanded-array" style = { { flexDirection : 'column' , gap : 0 } } >
118152 { value . map ( ( child , idx ) => (
119- < box key = { `expanded-array-${ idx } ` } style = { { flexDirection : 'column' , gap : 0 } } >
153+ < box
154+ key = { `expanded-array-${ idx } ` }
155+ style = { { flexDirection : 'column' , gap : 0 } }
156+ >
120157 { child }
121158 </ box >
122159 ) ) }
@@ -136,7 +173,10 @@ export const BranchItem = ({
136173 style = { {
137174 flexDirection : 'row' ,
138175 flexShrink : 0 ,
176+ alignSelf : isCollapsed ? 'flex-start' : 'stretch' ,
139177 paddingRight : outerPaddingRight ,
178+ marginTop : 0 ,
179+ marginBottom : 0 ,
140180 } }
141181 >
142182 < text wrap = { false } > { indentPrefix } </ text >
@@ -146,38 +186,74 @@ export const BranchItem = ({
146186 gap : 0 ,
147187 flexShrink : 1 ,
148188 flexGrow : 1 ,
149- marginTop : isCollapsed ? 1 : 0 ,
150- marginBottom : isCollapsed ? 1 : 0 ,
189+ marginTop : 0 ,
190+ marginBottom : 0 ,
151191 } }
152192 >
153193 { ! shouldMergeHeader && (
154194 < box
155195 style = { {
156196 flexDirection : 'row' ,
157- alignSelf : 'flex-start ' ,
158- backgroundColor : isCollapsed
159- ? theme . agentResponseCount
160- : theme . agentPrefix ,
161- paddingLeft : 4 ,
162- paddingRight : 4 ,
197+ alignItems : 'center ' ,
198+ justifyContent : 'space-between' ,
199+ alignSelf : isCollapsed ? 'flex-start' : 'stretch' ,
200+ backgroundColor : headerBackground ,
201+ paddingLeft : 2 ,
202+ paddingRight : 2 ,
163203 paddingTop : 0 ,
164204 paddingBottom : 0 ,
165205 marginTop : 0 ,
166206 marginBottom : 0 ,
167207 } }
168- onMouseDown = { onToggle }
169208 >
170- < text wrap >
171- < span fg = { theme . agentToggleText } >
172- { isCollapsed ? '▸ ' : '▾ ' }
173- </ span >
174- < span
175- fg = { theme . agentToggleText }
176- attributes = { TextAttributes . BOLD }
209+ < box
210+ style = { { flexDirection : 'row' , alignItems : 'center' , gap : 0 } }
211+ onMouseDown = { onToggle }
212+ >
213+ < text wrap >
214+ < span fg = { theme . agentToggleText } >
215+ { isCollapsed ? '▸ ' : '▾ ' }
216+ </ span >
217+ < span
218+ fg = { theme . agentToggleText }
219+ attributes = { TextAttributes . BOLD }
220+ >
221+ { name }
222+ </ span >
223+ { isStreaming && (
224+ < ShimmerText
225+ text = " ●"
226+ primaryColor = { theme . statusAccent }
227+ interval = { 140 }
228+ />
229+ ) }
230+ </ text >
231+ </ box >
232+ { quickActions . length > 0 && (
233+ < box
234+ style = { {
235+ flexDirection : 'row' ,
236+ alignItems : 'center' ,
237+ gap : 1 ,
238+ } }
177239 >
178- { name }
179- </ span >
180- </ text >
240+ { quickActions . map ( ( action ) => (
241+ < text
242+ key = { action . id }
243+ wrap = { false }
244+ attributes = { TextAttributes . DIM }
245+ fg = { theme . agentToggleText }
246+ onMouseDown = { ( event : any ) =>
247+ handleActionSelect ( action , event )
248+ }
249+ style = { { marginLeft : 1 } }
250+ >
251+ { action . icon ? `${ action . icon } ` : '' }
252+ { action . label }
253+ </ text >
254+ ) ) }
255+ </ box >
256+ ) }
181257 </ box >
182258 ) }
183259 < box style = { { flexShrink : 1 , marginBottom : 0 } } >
@@ -201,13 +277,13 @@ export const BranchItem = ({
201277 { finishedPreview }
202278 </ text >
203279 ) }
204- { ! isCollapsed && content && (
280+ { ! isCollapsed && hasContent && (
205281 < box
206282 style = { {
207283 flexDirection : 'column' ,
208284 gap : 0 ,
209- marginTop : shouldMergeHeader ? - 1 : 1 ,
210- marginBottom : shouldMergeHeader ? 0 : 0 ,
285+ marginTop : shouldMergeHeader ? 0 : 1 ,
286+ marginBottom : 1 ,
211287 } }
212288 >
213289 < box
@@ -217,42 +293,85 @@ export const BranchItem = ({
217293 width : '100%' ,
218294 border : [ 'top' , 'left' , 'right' , 'bottom' ] ,
219295 customBorderChars : BRANCH_BORDER_CHARS ,
220- borderColor : cornerColor ,
296+ borderColor : effectiveCornerColor ,
221297 paddingLeft : 1 ,
222298 paddingRight : 2 ,
223299 paddingTop : shouldMergeHeader ? 0 : 1 ,
224300 paddingBottom : 1 ,
301+ backgroundColor : contentBackground ,
225302 } }
226303 >
227304 { shouldMergeHeader && (
228305 < box
229306 style = { {
230307 flexDirection : 'row' ,
231308 alignItems : 'center' ,
232- justifyContent : 'flex-start ' ,
309+ justifyContent : 'space-between ' ,
233310 backgroundColor : theme . agentPrefix ,
234- paddingLeft : 4 ,
235- paddingRight : 4 ,
311+ paddingLeft : 2 ,
312+ paddingRight : 2 ,
236313 paddingTop : 0 ,
237314 paddingBottom : 0 ,
238315 marginBottom : 1 ,
239316 width : '100%' ,
240317 marginTop : 0 ,
241318 } }
242- onMouseDown = { onToggle }
243319 >
244- < text wrap >
245- < span fg = { theme . agentToggleText } > { '▾ ' } </ span >
246- < span
247- fg = { theme . agentToggleText }
248- attributes = { TextAttributes . BOLD }
320+ < box
321+ style = { {
322+ flexDirection : 'row' ,
323+ alignItems : 'center' ,
324+ gap : 0 ,
325+ } }
326+ onMouseDown = { onToggle }
327+ >
328+ < text wrap >
329+ < span fg = { theme . agentToggleText } > { '▾ ' } </ span >
330+ < span
331+ fg = { theme . agentToggleText }
332+ attributes = { TextAttributes . BOLD }
333+ >
334+ { name }
335+ </ span >
336+ { isStreaming && (
337+ < ShimmerText
338+ text = " ●"
339+ primaryColor = { theme . statusAccent }
340+ interval = { 140 }
341+ />
342+ ) }
343+ </ text >
344+ </ box >
345+ { quickActions . length > 0 && (
346+ < box
347+ style = { {
348+ flexDirection : 'row' ,
349+ alignItems : 'center' ,
350+ gap : 1 ,
351+ } }
249352 >
250- { name }
251- </ span >
252- </ text >
353+ { quickActions . map ( ( action ) => (
354+ < text
355+ key = { `${ action . id } -expanded` }
356+ wrap = { false }
357+ attributes = { TextAttributes . DIM }
358+ fg = { theme . agentToggleText }
359+ onMouseDown = { ( event : any ) =>
360+ handleActionSelect ( action , event )
361+ }
362+ style = { { marginLeft : 1 } }
363+ >
364+ { action . icon ? `${ action . icon } ` : '' }
365+ { action . label }
366+ </ text >
367+ ) ) }
368+ </ box >
369+ ) }
253370 </ box >
254371 ) }
255- { renderExpandedContent ( content ) }
372+ { renderExpandedContent (
373+ typeof content === 'function' ? content ( ) : content ,
374+ ) }
256375 </ box >
257376 < box
258377 style = { {
@@ -264,7 +383,10 @@ export const BranchItem = ({
264383 } }
265384 onMouseDown = { onToggle }
266385 >
267- < text fg = { theme . agentToggleText } attributes = { TextAttributes . DIM } >
386+ < text
387+ fg = { theme . agentToggleText }
388+ attributes = { TextAttributes . DIM }
389+ >
268390 collapse
269391 </ text >
270392 </ box >
0 commit comments