Skip to content

Commit c8051cb

Browse files
committed
feat(cli): add click and hover support to suggestion menus
- Make slash command and @ mention menu items clickable - Add hover highlighting with hasHoveredSinceOpen pattern - Hover only activates after mouse movement to avoid false highlights - Reset hover state when menu items change
1 parent 4ea59e8 commit c8051cb

File tree

3 files changed

+101
-9
lines changed

3 files changed

+101
-9
lines changed

cli/src/chat.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,62 @@ export const Chat = ({
635635
})
636636
})
637637

638+
// Click handlers for suggestion menu items
639+
const handleSlashItemClick = useCallback(
640+
(index: number) => {
641+
const selected = slashMatches[index]
642+
if (!selected || slashContext.startIndex < 0) return
643+
const before = inputValue.slice(0, slashContext.startIndex)
644+
const after = inputValue.slice(
645+
slashContext.startIndex + 1 + slashContext.query.length,
646+
)
647+
const replacement = `/${selected.id} `
648+
setInputValue({
649+
text: before + replacement + after,
650+
cursorPosition: before.length + replacement.length,
651+
lastEditDueToNav: false,
652+
})
653+
setSlashSelectedIndex(0)
654+
},
655+
[slashMatches, slashContext, inputValue, setInputValue, setSlashSelectedIndex],
656+
)
657+
658+
const handleMentionItemClick = useCallback(
659+
(index: number) => {
660+
if (mentionContext.startIndex < 0) return
661+
662+
let replacement: string
663+
if (index < agentMatches.length) {
664+
const selected = agentMatches[index]
665+
if (!selected) return
666+
replacement = `@${selected.displayName} `
667+
} else {
668+
const fileIndex = index - agentMatches.length
669+
const selectedFile = fileMatches[fileIndex]
670+
if (!selectedFile) return
671+
replacement = `@${selectedFile.filePath} `
672+
}
673+
const before = inputValue.slice(0, mentionContext.startIndex)
674+
const after = inputValue.slice(
675+
mentionContext.startIndex + 1 + mentionContext.query.length,
676+
)
677+
setInputValue({
678+
text: before + replacement + after,
679+
cursorPosition: before.length + replacement.length,
680+
lastEditDueToNav: false,
681+
})
682+
setAgentSelectedIndex(0)
683+
},
684+
[
685+
mentionContext,
686+
agentMatches,
687+
fileMatches,
688+
inputValue,
689+
setInputValue,
690+
setAgentSelectedIndex,
691+
],
692+
)
693+
638694
const { inputWidth, handleBuildFast, handleBuildMax } = useChatInput({
639695
setInputValue,
640696
agentMode,
@@ -1228,6 +1284,8 @@ export const Chat = ({
12281284
fileSuggestionItems={fileSuggestionItems}
12291285
slashSelectedIndex={slashSelectedIndex}
12301286
agentSelectedIndex={agentSelectedIndex}
1287+
onSlashItemClick={handleSlashItemClick}
1288+
onMentionItemClick={handleMentionItemClick}
12311289
theme={theme}
12321290
terminalHeight={terminalHeight}
12331291
separatorWidth={separatorWidth}

cli/src/components/chat-input-bar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ interface ChatInputBarProps {
4444
fileSuggestionItems: SuggestionItem[]
4545
slashSelectedIndex: number
4646
agentSelectedIndex: number
47+
onSlashItemClick?: (index: number) => void
48+
onMentionItemClick?: (index: number) => void
4749

4850
// Layout
4951
theme: Theme
@@ -82,6 +84,8 @@ export const ChatInputBar = ({
8284
fileSuggestionItems,
8385
slashSelectedIndex,
8486
agentSelectedIndex,
87+
onSlashItemClick,
88+
onMentionItemClick,
8589
theme,
8690
terminalHeight,
8791
separatorWidth,
@@ -287,6 +291,7 @@ export const ChatInputBar = ({
287291
selectedIndex={slashSelectedIndex}
288292
maxVisible={5}
289293
prefix="/"
294+
onItemClick={onSlashItemClick}
290295
/>
291296
) : null}
292297
{hasMentionSuggestions ? (
@@ -295,6 +300,7 @@ export const ChatInputBar = ({
295300
selectedIndex={agentSelectedIndex}
296301
maxVisible={5}
297302
prefix="@"
303+
onItemClick={onMentionItemClick}
298304
/>
299305
) : null}
300306
<box
@@ -361,6 +367,7 @@ export const ChatInputBar = ({
361367
selectedIndex={slashSelectedIndex}
362368
maxVisible={10}
363369
prefix="/"
370+
onItemClick={onSlashItemClick}
364371
/>
365372
) : null}
366373
{hasMentionSuggestions ? (
@@ -369,6 +376,7 @@ export const ChatInputBar = ({
369376
selectedIndex={agentSelectedIndex}
370377
maxVisible={10}
371378
prefix="@"
379+
onItemClick={onMentionItemClick}
372380
/>
373381
) : null}
374382
<box

cli/src/components/suggestion-menu.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React from 'react'
1+
import React, { useEffect, useState } from 'react'
22

3+
import { Button } from './button'
34
import { HighlightedSubsequenceText } from './highlighted-text'
45
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
56
import { useTheme } from '../hooks/use-theme'
@@ -17,19 +18,31 @@ interface SuggestionMenuProps {
1718
selectedIndex: number
1819
maxVisible: number
1920
prefix?: string
21+
onItemClick?: (index: number) => void
2022
}
2123

2224
export const SuggestionMenu = ({
2325
items,
2426
selectedIndex,
2527
maxVisible,
2628
prefix = '/',
29+
onItemClick,
2730
}: SuggestionMenuProps) => {
2831
const theme = useTheme()
2932
const { terminalWidth } = useTerminalDimensions()
3033
const screenPadding = 4
3134
const menuWidth = Math.max(10, terminalWidth - screenPadding * 2)
3235

36+
// Hover state: only highlight on hover after user has moved mouse
37+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
38+
const [hasHoveredSinceOpen, setHasHoveredSinceOpen] = useState(false)
39+
40+
// Reset hover state when items change (new menu session)
41+
useEffect(() => {
42+
setHasHoveredSinceOpen(false)
43+
setHoveredIndex(null)
44+
}, [items])
45+
3346
if (items.length === 0) {
3447
return null
3548
}
@@ -66,27 +79,37 @@ export const SuggestionMenu = ({
6679
const renderSuggestionItem = (item: SuggestionItem, idx: number) => {
6780
const absoluteIndex = start + idx
6881
const isSelected = absoluteIndex === clampedSelected
82+
const isHovered = hasHoveredSinceOpen && absoluteIndex === hoveredIndex
83+
const isHighlighted = isSelected || isHovered
6984
const labelLength = effectivePrefix.length + item.label.length
70-
const textColor = isSelected ? theme.foreground : theme.inputFg
71-
const descriptionColor = isSelected ? theme.foreground : theme.muted
85+
const textColor = isHighlighted ? theme.foreground : theme.inputFg
86+
const descriptionColor = isHighlighted ? theme.foreground : theme.muted
7287
const highlightColor = theme.primary
7388

89+
const handleClick = onItemClick ? () => onItemClick(absoluteIndex) : undefined
90+
const handleMouseOver = () => {
91+
setHoveredIndex(absoluteIndex)
92+
setHasHoveredSinceOpen(true)
93+
}
94+
7495
if (useSameLine) {
7596
// Calculate padding to align descriptions
7697
const paddingLength = maxLabelLength - labelLength
7798
const padding = ' '.repeat(paddingLength)
7899
// Wide terminal: description on same line with 2-space gap
79100
return (
80-
<box
101+
<Button
81102
key={item.id}
103+
onClick={handleClick}
104+
onMouseOver={handleMouseOver}
82105
style={{
83106
flexDirection: 'column',
84107
gap: 0,
85108
paddingLeft: 1,
86109
paddingRight: 1,
87110
paddingTop: 0,
88111
paddingBottom: 0,
89-
backgroundColor: isSelected ? theme.surfaceHover : theme.background,
112+
backgroundColor: isHighlighted ? theme.surfaceHover : theme.background,
90113
width: '100%',
91114
}}
92115
>
@@ -111,21 +134,23 @@ export const SuggestionMenu = ({
111134
highlightColor={highlightColor}
112135
/>
113136
</text>
114-
</box>
137+
</Button>
115138
)
116139
} else {
117140
// Narrow terminal: description on next line
118141
return (
119-
<box
142+
<Button
120143
key={item.id}
144+
onClick={handleClick}
145+
onMouseOver={handleMouseOver}
121146
style={{
122147
flexDirection: 'column',
123148
gap: 0,
124149
paddingLeft: 1,
125150
paddingRight: 1,
126151
paddingTop: 0,
127152
paddingBottom: 0,
128-
backgroundColor: isSelected ? theme.surfaceHover : theme.background,
153+
backgroundColor: isHighlighted ? theme.surfaceHover : theme.background,
129154
width: '100%',
130155
}}
131156
>
@@ -157,7 +182,7 @@ export const SuggestionMenu = ({
157182
highlightColor={highlightColor}
158183
/>
159184
</text>
160-
</box>
185+
</Button>
161186
)
162187
}
163188
}
@@ -174,6 +199,7 @@ export const SuggestionMenu = ({
174199
backgroundColor: theme.surface,
175200
width: '100%',
176201
}}
202+
onMouseOut={() => setHoveredIndex(null)}
177203
>
178204
{visibleItems.map(renderSuggestionItem)}
179205
</box>

0 commit comments

Comments
 (0)