Skip to content

Commit 1186c9b

Browse files
committed
Refactor UI components to use Button component
- Add new reusable Button component - Replace box elements with Button in interactive elements - Update event handlers from onMouseDown to onClick - Improve semantic HTML and accessibility across components
1 parent a98cd6a commit 1186c9b

12 files changed

+104
-42
lines changed

cli/src/chat.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
getStatusIndicatorState,
1717
} from './components/status-indicator'
1818
import { SuggestionMenu } from './components/suggestion-menu'
19+
import { Button } from './components/button'
1920
import { SLASH_COMMANDS } from './data/slash-commands'
2021
import { useAgentValidation } from './hooks/use-agent-validation'
2122
import { useAuthState } from './hooks/use-auth-state'
@@ -694,9 +695,9 @@ export const Chat = ({
694695
{/* Center section - scroll indicator (always centered) */}
695696
<box style={{ flexShrink: 0 }}>
696697
{!isAtBottom && (
697-
<box
698+
<Button
698699
style={{ paddingLeft: 2, paddingRight: 2 }}
699-
onMouseDown={() => scrollToLatest()}
700+
onClick={() => scrollToLatest()}
700701
onMouseOver={() => setScrollIndicatorHovered(true)}
701702
onMouseOut={() => setScrollIndicatorHovered(false)}
702703
>
@@ -712,7 +713,7 @@ export const Chat = ({
712713
{scrollIndicatorHovered ? '↓ Scroll to bottom ↓' : '↓'}
713714
</span>
714715
</text>
715-
</box>
716+
</Button>
716717
)}
717718
</box>
718719

cli/src/components/agent-branch-item.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { memo, type ReactNode } from 'react'
44
import { useTheme } from '../hooks/use-theme'
55
import { BORDER_CHARS } from '../utils/ui-constants'
66
import { useWhyDidYouUpdateById } from '../hooks/use-why-did-you-update'
7+
import { Button } from './button'
78

89
interface AgentBranchItemProps {
910
name: string
@@ -180,7 +181,7 @@ export const AgentBranchItem = memo((props: AgentBranchItemProps) => {
180181
width: '100%',
181182
}}
182183
>
183-
<box
184+
<Button
184185
style={{
185186
flexDirection: 'row',
186187
alignItems: 'center',
@@ -190,7 +191,7 @@ export const AgentBranchItem = memo((props: AgentBranchItemProps) => {
190191
paddingBottom: isCollapsed ? 0 : 1,
191192
width: '100%',
192193
}}
193-
onMouseDown={onToggle}
194+
onClick={onToggle}
194195
>
195196
<text style={{ wrapMode: 'none' }}>
196197
<span fg={toggleIconColor}>{toggleLabel}</span>
@@ -214,7 +215,7 @@ export const AgentBranchItem = memo((props: AgentBranchItemProps) => {
214215
</span>
215216
) : null}
216217
</text>
217-
</box>
218+
</Button>
218219

219220
{isCollapsed ? (
220221
showCollapsedPreview ? (
@@ -280,17 +281,17 @@ export const AgentBranchItem = memo((props: AgentBranchItemProps) => {
280281
)}
281282
{renderExpandedContent(children)}
282283
{onToggle && (
283-
<box
284+
<Button
284285
style={{
285286
alignSelf: 'flex-end',
286287
marginTop: 1,
287288
}}
288-
onMouseDown={onToggle}
289+
onClick={onToggle}
289290
>
290291
<text fg={theme.secondary} style={{ wrapMode: 'none' }}>
291292
▴ collapse
292293
</text>
293-
</box>
294+
</Button>
294295
)}
295296
</box>
296297
)}

cli/src/components/agent-mode-toggle.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from 'react'
33
import { SegmentedControl } from './segmented-control'
44
import { useTheme } from '../hooks/use-theme'
55
import { BORDER_CHARS } from '../utils/ui-constants'
6+
import { Button } from './button'
67

78
import type { Segment } from './segmented-control'
89
import type { AgentMode } from '../utils/constants'
@@ -194,7 +195,7 @@ export const AgentModeToggle = ({
194195

195196
if (!hoverToggle.isOpen) {
196197
return (
197-
<box
198+
<Button
198199
style={{
199200
flexDirection: 'row',
200201
alignItems: 'center',
@@ -204,7 +205,7 @@ export const AgentModeToggle = ({
204205
borderColor: isCollapsedHovered ? theme.foreground : theme.border,
205206
customBorderChars: BORDER_CHARS,
206207
}}
207-
onMouseDown={() => {
208+
onClick={() => {
208209
hoverToggle.clearAllTimers()
209210
hoverToggle.openNow()
210211
}}
@@ -225,7 +226,7 @@ export const AgentModeToggle = ({
225226
`< ${MODE_LABELS[mode]}`
226227
)}
227228
</text>
228-
</box>
229+
</Button>
229230
)
230231
}
231232

cli/src/components/build-mode-buttons.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState } from 'react'
22
import type { ChatTheme } from '../types/theme-system'
33
import { BORDER_CHARS } from '../utils/ui-constants'
44
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
5+
import { Button } from './button'
56
export const BuildModeButtons = ({
67
theme,
78
onBuildFast,
@@ -39,7 +40,7 @@ export const BuildModeButtons = ({
3940
gap: 1,
4041
}}
4142
>
42-
<box
43+
<Button
4344
style={{
4445
flexDirection: 'row',
4546
alignItems: 'center',
@@ -50,15 +51,15 @@ export const BuildModeButtons = ({
5051
hoveredButton === 'fast' ? theme.foreground : theme.secondary,
5152
customBorderChars: BORDER_CHARS,
5253
}}
53-
onMouseDown={onBuildFast}
54+
onClick={onBuildFast}
5455
onMouseOver={() => setHoveredButton('fast')}
5556
onMouseOut={() => setHoveredButton(null)}
5657
>
5758
<text wrapMode="none" selectable={false}>
5859
<span fg={theme.foreground}>Build DEFAULT</span>
5960
</text>
60-
</box>
61-
<box
61+
</Button>
62+
<Button
6263
style={{
6364
flexDirection: 'row',
6465
alignItems: 'center',
@@ -69,14 +70,14 @@ export const BuildModeButtons = ({
6970
hoveredButton === 'max' ? theme.foreground : theme.secondary,
7071
customBorderChars: BORDER_CHARS,
7172
}}
72-
onMouseDown={onBuildMax}
73+
onClick={onBuildMax}
7374
onMouseOver={() => setHoveredButton('max')}
7475
onMouseOut={() => setHoveredButton(null)}
7576
>
7677
<text wrapMode="none" selectable={false}>
7778
<span fg={theme.foreground}>Build MAX</span>
7879
</text>
79-
</box>
80+
</Button>
8081
</box>
8182
</box>
8283
)

cli/src/components/button.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React, { cloneElement, isValidElement, memo, type ReactElement, type ReactNode } from 'react'
2+
3+
interface ButtonProps {
4+
onClick?: (e?: unknown) => void | Promise<unknown>
5+
onMouseOver?: () => void
6+
onMouseOut?: () => void
7+
style?: Record<string, unknown>
8+
children?: ReactNode
9+
// pass-through for box host props
10+
[key: string]: unknown
11+
}
12+
13+
function makeTextUnselectable(node: ReactNode): ReactNode {
14+
if (node === null || node === undefined || typeof node === 'boolean') return node
15+
if (typeof node === 'string' || typeof node === 'number') return node
16+
17+
if (Array.isArray(node)) {
18+
return node.map((child, idx) => <React.Fragment key={idx}>{makeTextUnselectable(child)}</React.Fragment>)
19+
}
20+
21+
if (!isValidElement(node)) return node
22+
23+
const el = node as ReactElement
24+
const type = el.type
25+
26+
// Ensure text nodes are not selectable
27+
if (typeof type === 'string' && type === 'text') {
28+
const nextProps = { ...el.props, selectable: false }
29+
const nextChildren = el.props?.children ? makeTextUnselectable(el.props.children) : el.props?.children
30+
return cloneElement(el, nextProps, nextChildren)
31+
}
32+
33+
// Recurse into other host elements and components' children
34+
const nextChildren = el.props?.children ? makeTextUnselectable(el.props.children) : el.props?.children
35+
return cloneElement(el, el.props, nextChildren)
36+
}
37+
38+
export const Button = memo(({ onClick, onMouseOver, onMouseOut, style, children, ...rest }: ButtonProps) => {
39+
const processedChildren = makeTextUnselectable(children)
40+
return (
41+
<box
42+
{...rest}
43+
style={style}
44+
onMouseDown={onClick}
45+
onMouseOver={onMouseOver}
46+
onMouseOut={onMouseOut}
47+
>
48+
{processedChildren}
49+
</box>
50+
)
51+
})

cli/src/components/message-renderer.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react'
44

55
import { MessageBlock } from './message-block'
66
import { ModeDivider } from './mode-divider'
7+
import { Button } from './button'
78
import {
89
renderMarkdown,
910
hasMarkdown,
@@ -506,26 +507,26 @@ const AgentMessage = memo(
506507
flexGrow: 1,
507508
}}
508509
>
509-
<box
510+
<Button
510511
style={{
511512
flexDirection: 'row',
512513
alignSelf: 'flex-start',
513514
backgroundColor: isCollapsed ? theme.muted : theme.success,
514515
paddingLeft: 1,
515516
paddingRight: 1,
516517
}}
517-
onMouseDown={handleTitleClick}
518+
onClick={handleTitleClick}
518519
>
519520
<text style={{ wrapMode: 'word' }}>
520521
<span fg={theme.foreground}>{isCollapsed ? '▸ ' : '▾ '}</span>
521522
<span fg={theme.foreground} attributes={TextAttributes.BOLD}>
522523
{agentInfo.agentName}
523524
</span>
524525
</text>
525-
</box>
526-
<box
526+
</Button>
527+
<Button
527528
style={{ flexShrink: 1, marginBottom: isCollapsed ? 1 : 0 }}
528-
onMouseDown={handleContentClick}
529+
onClick={handleContentClick}
529530
>
530531
{isStreaming && isCollapsed && streamingPreview && (
531532
<text
@@ -551,7 +552,7 @@ const AgentMessage = memo(
551552
{displayContent}
552553
</text>
553554
)}
554-
</box>
555+
</Button>
555556
</box>
556557
</box>
557558
{agentChildren.length > 0 && (

cli/src/components/raised-pill.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
22
import stringWidth from 'string-width'
3+
import { Button } from './button'
34

45
type PillSegment = {
56
text: string
@@ -55,14 +56,14 @@ export const RaisedPill = ({
5556
const horizontal = buildHorizontal(contentWidth)
5657

5758
return (
58-
<box
59+
<Button
5960
style={{
6061
flexDirection: 'column',
6162
gap: 0,
6263
backgroundColor: 'transparent',
6364
...style,
6465
}}
65-
onMouseDown={onPress}
66+
onClick={onPress}
6667
>
6768
<text selectable={false}>
6869
<span fg={frameColor}>{`╭${horizontal}╮`}</span>
@@ -88,6 +89,6 @@ export const RaisedPill = ({
8889
<text selectable={false}>
8990
<span fg={frameColor}>{`╰${horizontal}╯`}</span>
9091
</text>
91-
</box>
92+
</Button>
9293
)
9394
}

cli/src/components/segmented-control.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
22
import stringWidth from 'string-width'
33

44
import { useTheme } from '../hooks/use-theme'
5+
import { Button } from './button'
56

67
import type { ChatTheme } from '../types/theme-system'
78

@@ -75,8 +76,8 @@ export const SegmentedControl = ({
7576
</box>
7677
) : null}
7778

78-
<box
79-
onMouseDown={() => onSegmentClick && onSegmentClick(seg.id)}
79+
<Button
80+
onClick={() => onSegmentClick && onSegmentClick(seg.id)}
8081
onMouseOver={() => {
8182
setHoveredId(seg.id)
8283
setHasHoveredSinceOpen(true)
@@ -99,7 +100,7 @@ export const SegmentedControl = ({
99100
)}
100101
</text>
101102
<text fg={seg.frameColor} selectable={false}>{seg.bottomBorder}</text>
102-
</box>
103+
</Button>
103104

104105
{rightOfHovered ? (
105106
<box style={{ flexDirection: 'column', gap: 0 }}>

cli/src/components/terminal-link.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback, useMemo, useState } from 'react'
22

33
import { useTheme } from '../hooks/use-theme'
4+
import { Button } from './button'
45

56
type FormatLinesFn = (text: string, maxWidth?: number) => string[]
67

@@ -63,7 +64,7 @@ export const TerminalLink: React.FC<TerminalLinkProps> = ({
6364
}
6465

6566
return (
66-
<box
67+
<Button
6768
style={{
6869
flexDirection: 'column',
6970
alignItems: 'flex-start',
@@ -73,7 +74,7 @@ export const TerminalLink: React.FC<TerminalLinkProps> = ({
7374
}}
7475
onMouseOver={() => setIsHovered(true)}
7576
onMouseOut={() => setIsHovered(false)}
76-
onMouseDown={handleActivate}
77+
onClick={handleActivate}
7778
>
7879
{displayLines.map((line: string, index: number) => {
7980
const coloredText = <span fg={displayColor}>{line}</span>
@@ -83,6 +84,6 @@ export const TerminalLink: React.FC<TerminalLinkProps> = ({
8384
</text>
8485
)
8586
})}
86-
</box>
87+
</Button>
8788
)
8889
}

0 commit comments

Comments
 (0)