Skip to content

Commit d466a3d

Browse files
committed
[codecane] feat(cli): implement expandable agent mode toggle with hover interaction
- Add collapsed/expanded states with horizontal segmented control - Arrow on left indicates expand (◀) when collapsed - Active mode shows bold label with collapse arrow when expanded - Hover to open menu with 1-second delayed close - Click anywhere in segment column to select mode (full vertical clickability) - Modes reorder with active mode rightmost when expanded - Auto-closes when selecting different mode - Preserve Shift+Tab keyboard shortcut functionality
1 parent 3ae93aa commit d466a3d

File tree

1 file changed

+93
-48
lines changed

1 file changed

+93
-48
lines changed

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

Lines changed: 93 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react'
1+
import React, { useRef, useState } from 'react'
22
import stringWidth from 'string-width'
33
import { useTheme } from '../hooks/use-theme'
44

@@ -38,6 +38,7 @@ export const AgentModeToggle = ({
3838
const theme = useTheme()
3939
const config = getModeConfig(theme)
4040
const [isOpen, setIsOpen] = useState(false)
41+
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null)
4142

4243
const handlePress = (selectedMode: AgentMode) => {
4344
if (selectedMode === mode) {
@@ -54,11 +55,28 @@ export const AgentModeToggle = ({
5455
}
5556
}
5657

58+
const handleMouseOver = () => {
59+
// Cancel any pending close
60+
if (closeTimeoutRef.current) {
61+
clearTimeout(closeTimeoutRef.current)
62+
closeTimeoutRef.current = null
63+
}
64+
setIsOpen(true)
65+
}
66+
67+
const handleMouseOut = () => {
68+
// Delay closing by 1 second
69+
closeTimeoutRef.current = setTimeout(() => {
70+
setIsOpen(false)
71+
closeTimeoutRef.current = null
72+
}, 1000)
73+
}
74+
5775
if (!isOpen) {
5876
// Collapsed state: show only current mode
5977
const { frameColor, textColor, label } = config[mode]
60-
const arrow = ' <'
61-
const contentText = ` ${label}${arrow} `
78+
const arrow = '< '
79+
const contentText = ` ${arrow}${label} `
6280
const contentWidth = stringWidth(contentText)
6381
const horizontal = '─'.repeat(contentWidth)
6482

@@ -70,13 +88,19 @@ export const AgentModeToggle = ({
7088
backgroundColor: 'transparent',
7189
}}
7290
onMouseDown={() => handlePress(mode)}
91+
onMouseOver={handleMouseOver}
92+
onMouseOut={handleMouseOut}
7393
>
7494
<text>
7595
<span fg={frameColor}>{`╭${horizontal}╮`}</span>
7696
</text>
7797
<text>
7898
<span fg={frameColor}></span>
79-
<span fg={textColor}>{contentText}</span>
99+
<span fg={textColor}> {arrow}</span>
100+
<b>
101+
<span fg={textColor}>{label}</span>
102+
</b>
103+
<span fg={textColor}> </span>
80104
<span fg={frameColor}></span>
81105
</text>
82106
<text>
@@ -97,7 +121,7 @@ export const AgentModeToggle = ({
97121
const label = config[m].label
98122
if (m === mode) {
99123
// Active mode shows label with collapse arrow
100-
return stringWidth(` ${label} > `)
124+
return stringWidth(` < ${label} `)
101125
}
102126
return stringWidth(` ${label} `)
103127
})
@@ -110,15 +134,18 @@ export const AgentModeToggle = ({
110134
const { frameColor, textColor, label } = config[modeItem]
111135
const isActive = modeItem === mode
112136
const width = segmentWidths[index]
113-
const content = isActive ? ` ${label} > ` : ` ${label} `
137+
const content = isActive ? ` < ${label} ` : ` ${label} `
114138
const horizontal = '─'.repeat(width)
115139

116140
return {
117-
topBorder: isLast ? `${horizontal}╮` : `${horizontal}┬`,
141+
topBorder: horizontal,
118142
content,
119-
bottomBorder: isLast ? `${horizontal}╯` : `${horizontal}┴`,
143+
bottomBorder: horizontal,
120144
frameColor,
121145
textColor,
146+
isActive,
147+
label,
148+
width,
122149
}
123150
}
124151

@@ -129,57 +156,75 @@ export const AgentModeToggle = ({
129156
return (
130157
<box
131158
style={{
132-
flexDirection: 'column',
159+
flexDirection: 'row',
133160
gap: 0,
134161
backgroundColor: 'transparent',
135162
}}
163+
onMouseOver={handleMouseOver}
164+
onMouseOut={handleMouseOut}
136165
>
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-
>
166+
{/* Left edge */}
167+
<box style={{ flexDirection: 'column', gap: 0 }}>
168+
<text>
169+
<span fg={segments[0].frameColor}></span>
170+
</text>
154171
<text>
155172
<span fg={segments[0].frameColor}></span>
156173
</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>
174+
<text>
175+
<span fg={segments[0].frameColor}></span>
176+
</text>
177+
</box>
178+
179+
{/* Segments as vertical columns */}
180+
{segments.map((seg, idx) => {
181+
const modeItem = orderedModes[idx]
182+
const isLast = idx === segments.length - 1
183+
return (
184+
<React.Fragment key={`segment-${idx}`}>
185+
<box
186+
onMouseDown={() => handlePress(modeItem)}
187+
style={{
188+
flexDirection: 'column',
189+
gap: 0,
190+
width: seg.width,
191+
minWidth: seg.width,
192+
}}
193+
>
194+
<text>
195+
<span fg={seg.frameColor}>{seg.topBorder}</span>
196+
</text>
197+
<text>
198+
{seg.isActive ? (
199+
<>
200+
<span fg={seg.textColor}> {'< '}</span>
201+
<b>
202+
<span fg={seg.textColor}>{seg.label}</span>
203+
</b>
204+
<span fg={seg.textColor}> </span>
205+
</>
206+
) : (
163207
<span fg={seg.textColor}>{seg.content}</span>
164-
</text>
165-
</box>
208+
)}
209+
</text>
210+
<text>
211+
<span fg={seg.frameColor}>{seg.bottomBorder}</span>
212+
</text>
213+
</box>
214+
<box style={{ flexDirection: 'column', gap: 0 }}>
215+
<text>
216+
<span fg={seg.frameColor}>{isLast ? '╮' : '┬'}</span>
217+
</text>
166218
<text>
167219
<span fg={seg.frameColor}></span>
168220
</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>
221+
<text>
222+
<span fg={seg.frameColor}>{isLast ? '╯' : '┴'}</span>
223+
</text>
224+
</box>
225+
</React.Fragment>
226+
)
227+
})}
183228
</box>
184229
)
185230
}

0 commit comments

Comments
 (0)