11import React , { useCallback , useState } from 'react'
22import { TextAttributes } from '@opentui/core'
33
4- import { Button } from '../button'
54import { defineToolComponent } from './types'
65import { useTheme } from '../../hooks/use-theme'
76import { useChatStore } from '../../state/chat-store'
@@ -10,26 +9,24 @@ import { useTerminalDimensions } from '../../hooks/use-terminal-dimensions'
109import type { ToolRenderConfig } from './types'
1110import type { SuggestedFollowup } from '../../state/chat-store'
1211
13- interface FollowupCardProps {
12+ interface FollowupLineProps {
1413 followup : SuggestedFollowup
1514 index : number
1615 isClicked : boolean
17- isStacked : boolean
1816 onSendFollowup : ( prompt : string , index : number ) => void
1917}
2018
21- const FollowupCard = ( {
19+ const FollowupLine = ( {
2220 followup,
2321 index,
2422 isClicked,
25- isStacked,
2623 onSendFollowup,
27- } : FollowupCardProps ) => {
24+ } : FollowupLineProps ) => {
2825 const theme = useTheme ( )
26+ const { terminalWidth } = useTerminalDimensions ( )
2927 const [ isHovered , setIsHovered ] = useState ( false )
3028
3129 const handleClick = useCallback ( ( ) => {
32- // Don't allow clicking already-selected followups
3330 if ( isClicked ) return
3431 onSendFollowup ( followup . prompt , index )
3532 } , [ followup . prompt , index , onSendFollowup , isClicked ] )
@@ -38,52 +35,79 @@ const FollowupCard = ({
3835 const handleMouseOut = useCallback ( ( ) => setIsHovered ( false ) , [ ] )
3936
4037 const hasLabel = Boolean ( followup . label )
38+ // "→ " = 2 chars (icon + space), " · " separator = 3 chars, "…" = 1 char
39+ const iconWidth = 2
40+ const separatorWidth = hasLabel ? 3 : 0
41+ const ellipsisWidth = 1
42+ const maxWidth = terminalWidth - 6 // Extra margin for safety
43+
44+ // Build the display text with label and prompt
45+ let labelText = followup . label || ''
46+ let promptText = followup . prompt
47+
48+ // Calculate available space
49+ const availableForContent = maxWidth - iconWidth
50+
51+ if ( hasLabel ) {
52+ // Show: label · prompt (truncated)
53+ const labelWithSeparator = labelText . length + separatorWidth
54+ const totalLength = labelWithSeparator + promptText . length
55+
56+ if ( totalLength > availableForContent ) {
57+ // Truncate prompt to fit
58+ const availableForPrompt = availableForContent - labelWithSeparator - ellipsisWidth
59+ if ( availableForPrompt > 0 ) {
60+ promptText = promptText . slice ( 0 , availableForPrompt ) + '…'
61+ } else {
62+ // Not enough space for prompt, just show label truncated
63+ promptText = ''
64+ if ( labelText . length > availableForContent - ellipsisWidth ) {
65+ labelText = labelText . slice ( 0 , availableForContent - ellipsisWidth ) + '…'
66+ }
67+ }
68+ }
69+ } else {
70+ // No label, just show prompt (truncated)
71+ if ( promptText . length > availableForContent ) {
72+ promptText = promptText . slice ( 0 , availableForContent - ellipsisWidth ) + '…'
73+ }
74+ }
4175
4276 // Determine colors based on state
43- const borderColor = isClicked
77+ const iconColor = isClicked
4478 ? theme . success
4579 : isHovered
4680 ? theme . primary
47- : theme . border
48- const labelColor = isClicked ? theme . muted : theme . secondary
49- const promptColor = isClicked ? theme . muted : theme . foreground
81+ : theme . muted
82+ const labelColor = isClicked
83+ ? theme . muted
84+ : isHovered
85+ ? theme . primary
86+ : theme . foreground
87+ const promptColor = isClicked
88+ ? theme . muted
89+ : isHovered
90+ ? theme . primary
91+ : theme . muted
5092
5193 return (
52- < Button
53- onClick = { handleClick }
94+ < box
95+ onMouseDown = { handleClick }
5496 onMouseOver = { handleMouseOver }
5597 onMouseOut = { handleMouseOut }
56- style = { {
57- paddingLeft : 2 ,
58- paddingRight : 2 ,
59- paddingTop : 0 ,
60- paddingBottom : 0 ,
61- ...( isStacked ? { width : '100%' } : { flexGrow : 1 , flexShrink : 1 } ) ,
62- borderColor,
63- } }
6498 >
65- < box style = { { flexDirection : 'column' } } >
66- { hasLabel && (
67- < text
68- style = { {
69- fg : labelColor ,
70- } }
71- attributes = { TextAttributes . BOLD }
72- >
73- { isClicked ? < span fg = { theme . success } > ✓ </ span > : < span > → </ span > }
74- < span > { followup . label } </ span >
75- </ text >
99+ < text selectable = { false } >
100+ < span fg = { iconColor } > { isClicked ? '✓' : '→' } </ span >
101+ < span fg = { labelColor } attributes = { isHovered ? TextAttributes . UNDERLINE : undefined } >
102+ { ' ' } { hasLabel ? labelText : promptText }
103+ </ span >
104+ { hasLabel && promptText && (
105+ < span fg = { promptColor } >
106+ { ' · ' } { promptText }
107+ </ span >
76108 ) }
77- < text
78- style = { {
79- fg : promptColor ,
80- } }
81- >
82- { ! hasLabel && isClicked && < span fg = { theme . success } > ✓ </ span > }
83- < span > { followup . prompt } </ span >
84- </ text >
85- </ box >
86- </ Button >
109+ </ text >
110+ </ box >
87111 )
88112}
89113
@@ -93,16 +117,12 @@ interface SuggestFollowupsItemProps {
93117 onSendFollowup : ( prompt : string , index : number ) => void
94118}
95119
96- // Threshold width to switch between horizontal and stacked layouts
97- const WIDE_SCREEN_THRESHOLD = 100
98-
99120const SuggestFollowupsItem = ( {
100121 toolCallId,
101122 followups,
102123 onSendFollowup,
103124} : SuggestFollowupsItemProps ) => {
104125 const theme = useTheme ( )
105- const { terminalWidth } = useTerminalDimensions ( )
106126 const suggestedFollowups = useChatStore ( ( state ) => state . suggestedFollowups )
107127
108128 // Get clicked indices for this specific tool call
@@ -111,35 +131,20 @@ const SuggestFollowupsItem = ({
111131 ? suggestedFollowups . clickedIndices
112132 : new Set < number > ( )
113133
114- // Use stacked layout on narrow screens
115- const isStacked = terminalWidth < WIDE_SCREEN_THRESHOLD
116-
117134 return (
118- < box
119- style = { {
120- flexDirection : 'column' ,
121- gap : 1 ,
122- } }
123- >
124- < text style = { { fg : theme . primary } } attributes = { TextAttributes . BOLD } >
125- Suggested next steps:
135+ < box style = { { flexDirection : 'column' } } >
136+ < text style = { { fg : theme . muted } } >
137+ Next steps:
126138 </ text >
127- < box
128- style = { {
129- flexDirection : isStacked ? 'column' : 'row' ,
130- } }
131- >
132- { followups . map ( ( followup , index ) => (
133- < FollowupCard
134- key = { `followup-${ index } ` }
135- followup = { followup }
136- index = { index }
137- isClicked = { clickedIndices . has ( index ) }
138- isStacked = { isStacked }
139- onSendFollowup = { onSendFollowup }
140- />
141- ) ) }
142- </ box >
139+ { followups . map ( ( followup , index ) => (
140+ < FollowupLine
141+ key = { `followup-${ index } ` }
142+ followup = { followup }
143+ index = { index }
144+ isClicked = { clickedIndices . has ( index ) }
145+ onSendFollowup = { onSendFollowup }
146+ />
147+ ) ) }
143148 </ box >
144149 )
145150}
0 commit comments