Skip to content

Commit ce32c18

Browse files
committed
Improve TUI usability: fix status separator, add log inspector, keyboard hints, focus mode, and persist UI state across sessions.\n\n🤖 Generated with Codebuff\nCo-Authored-By: Codebuff <noreply@codebuff.com>
1 parent b7555b5 commit ce32c18

File tree

13 files changed

+1307
-219
lines changed

13 files changed

+1307
-219
lines changed

cli/src/chat.tsx

Lines changed: 660 additions & 60 deletions
Large diffs are not rendered by default.

cli/src/components/branch-item.tsx

Lines changed: 167 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import { TextAttributes } from '@opentui/core'
22
import React, { type ReactNode } from 'react'
33

4+
import { ShimmerText } from './shimmer-text'
45
import 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+
614
interface 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

1828
const 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

Comments
 (0)