Skip to content

Commit 3ae93aa

Browse files
committed
feat(cli): selection menu for agent modes
1 parent 890d9cb commit 3ae93aa

File tree

2 files changed

+155
-11
lines changed

2 files changed

+155
-11
lines changed

cli/src/chat.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1280,7 +1280,11 @@ export const App = ({
12801280
paddingLeft: 2,
12811281
}}
12821282
>
1283-
<AgentModeToggle mode={agentMode} onToggle={toggleAgentMode} />
1283+
<AgentModeToggle
1284+
mode={agentMode}
1285+
onToggle={toggleAgentMode}
1286+
onSelectMode={setAgentMode}
1287+
/>
12841288
</box>
12851289
</box>
12861290
<Separator width={separatorWidth} />
Lines changed: 150 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { RaisedPill } from './raised-pill'
1+
import React, { useState } from 'react'
2+
import stringWidth from 'string-width'
23
import { useTheme } from '../hooks/use-theme'
34

45
import type { AgentMode } from '../utils/constants'
@@ -14,32 +15,171 @@ const getModeConfig = (theme: ChatTheme) =>
1415
MAX: {
1516
frameColor: theme.modeMaxBg,
1617
textColor: theme.modeMaxText,
17-
label: '💪 MAX',
18+
label: 'MAX',
1819
},
1920
PLAN: {
2021
frameColor: theme.modePlanBg,
2122
textColor: theme.modePlanText,
22-
label: '📋 PLAN',
23+
label: 'PLAN',
2324
},
2425
}) as const
2526

27+
const ALL_MODES: AgentMode[] = ['FAST', 'MAX', 'PLAN']
28+
2629
export const AgentModeToggle = ({
2730
mode,
2831
onToggle,
32+
onSelectMode,
2933
}: {
3034
mode: AgentMode
3135
onToggle: () => void
36+
onSelectMode?: (mode: AgentMode) => void
3237
}) => {
3338
const theme = useTheme()
3439
const config = getModeConfig(theme)
35-
const { frameColor, textColor, label } = config[mode]
40+
const [isOpen, setIsOpen] = useState(false)
41+
42+
const handlePress = (selectedMode: AgentMode) => {
43+
if (selectedMode === mode) {
44+
// Toggle collapsed/expanded when clicking current mode
45+
setIsOpen(!isOpen)
46+
} else {
47+
// Switch to different mode and close the toggle
48+
if (onSelectMode) {
49+
onSelectMode(selectedMode)
50+
} else {
51+
onToggle()
52+
}
53+
setIsOpen(false)
54+
}
55+
}
56+
57+
if (!isOpen) {
58+
// Collapsed state: show only current mode
59+
const { frameColor, textColor, label } = config[mode]
60+
const arrow = ' <'
61+
const contentText = ` ${label}${arrow} `
62+
const contentWidth = stringWidth(contentText)
63+
const horizontal = '─'.repeat(contentWidth)
64+
65+
return (
66+
<box
67+
style={{
68+
flexDirection: 'column',
69+
gap: 0,
70+
backgroundColor: 'transparent',
71+
}}
72+
onMouseDown={() => handlePress(mode)}
73+
>
74+
<text>
75+
<span fg={frameColor}>{`╭${horizontal}╮`}</span>
76+
</text>
77+
<text>
78+
<span fg={frameColor}></span>
79+
<span fg={textColor}>{contentText}</span>
80+
<span fg={frameColor}></span>
81+
</text>
82+
<text>
83+
<span fg={frameColor}>{`╰${horizontal}╯`}</span>
84+
</text>
85+
</box>
86+
)
87+
}
88+
89+
// Expanded state: show all modes with current mode rightmost
90+
const orderedModes = [
91+
...ALL_MODES.filter((m) => m !== mode),
92+
mode,
93+
]
94+
95+
// Calculate widths for each segment
96+
const segmentWidths = orderedModes.map((m) => {
97+
const label = config[m].label
98+
if (m === mode) {
99+
// Active mode shows label with collapse arrow
100+
return stringWidth(` ${label} > `)
101+
}
102+
return stringWidth(` ${label} `)
103+
})
104+
105+
const buildSegment = (
106+
modeItem: AgentMode,
107+
index: number,
108+
isLast: boolean,
109+
) => {
110+
const { frameColor, textColor, label } = config[modeItem]
111+
const isActive = modeItem === mode
112+
const width = segmentWidths[index]
113+
const content = isActive ? ` ${label} > ` : ` ${label} `
114+
const horizontal = '─'.repeat(width)
115+
116+
return {
117+
topBorder: isLast ? `${horizontal}╮` : `${horizontal}┬`,
118+
content,
119+
bottomBorder: isLast ? `${horizontal}╯` : `${horizontal}┴`,
120+
frameColor,
121+
textColor,
122+
}
123+
}
124+
125+
const segments = orderedModes.map((m, idx) =>
126+
buildSegment(m, idx, idx === orderedModes.length - 1),
127+
)
36128

37129
return (
38-
<RaisedPill
39-
segments={[{ text: label, fg: textColor }]}
40-
frameColor={frameColor}
41-
textColor={textColor}
42-
onPress={onToggle}
43-
/>
130+
<box
131+
style={{
132+
flexDirection: 'column',
133+
gap: 0,
134+
backgroundColor: 'transparent',
135+
}}
136+
>
137+
{/* Top border */}
138+
<text>
139+
<span fg={segments[0].frameColor}></span>
140+
{segments.map((seg, idx) => (
141+
<span key={`top-${idx}`} fg={seg.frameColor}>
142+
{seg.topBorder}
143+
</span>
144+
))}
145+
</text>
146+
147+
{/* Content row with clickable segments */}
148+
<box
149+
style={{
150+
flexDirection: 'row',
151+
gap: 0,
152+
}}
153+
>
154+
<text>
155+
<span fg={segments[0].frameColor}></span>
156+
</text>
157+
{segments.map((seg, idx) => {
158+
const modeItem = orderedModes[idx]
159+
return (
160+
<React.Fragment key={`content-${idx}`}>
161+
<box onMouseDown={() => handlePress(modeItem)}>
162+
<text>
163+
<span fg={seg.textColor}>{seg.content}</span>
164+
</text>
165+
</box>
166+
<text>
167+
<span fg={seg.frameColor}></span>
168+
</text>
169+
</React.Fragment>
170+
)
171+
})}
172+
</box>
173+
174+
{/* Bottom border */}
175+
<text>
176+
<span fg={segments[0].frameColor}></span>
177+
{segments.map((seg, idx) => (
178+
<span key={`bottom-${idx}`} fg={seg.frameColor}>
179+
{seg.bottomBorder}
180+
</span>
181+
))}
182+
</text>
183+
</box>
44184
)
45185
}

0 commit comments

Comments
 (0)