Skip to content

Commit fd43ff3

Browse files
committed
fix: Escape key now closes slash and mention dropdown menus
- Add close-slash-menu and close-mention-menu action types - Escape removes the trigger character (/ or @) and query from input - Priority 5.5: menus close before stream interrupt or agent unfocus - Add comprehensive tests for menu close behavior
1 parent 2fe7239 commit fd43ff3

File tree

4 files changed

+140
-0
lines changed

4 files changed

+140
-0
lines changed

cli/src/chat.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,36 @@ export const Chat = ({
867867
pauseQueue()
868868
}
869869
},
870+
onCloseSlashMenu: () => {
871+
// Remove the slash and query from input to close the menu
872+
if (slashContext.startIndex >= 0) {
873+
const before = inputValue.slice(0, slashContext.startIndex)
874+
const after = inputValue.slice(
875+
slashContext.startIndex + 1 + slashContext.query.length,
876+
)
877+
setInputValue({
878+
text: before + after,
879+
cursorPosition: before.length,
880+
lastEditDueToNav: false,
881+
})
882+
}
883+
setSlashSelectedIndex(0)
884+
},
885+
onCloseMentionMenu: () => {
886+
// Remove the @ and query from input to close the menu
887+
if (mentionContext.startIndex >= 0) {
888+
const before = inputValue.slice(0, mentionContext.startIndex)
889+
const after = inputValue.slice(
890+
mentionContext.startIndex + 1 + mentionContext.query.length,
891+
)
892+
setInputValue({
893+
text: before + after,
894+
cursorPosition: before.length,
895+
lastEditDueToNav: false,
896+
})
897+
}
898+
setAgentSelectedIndex(0)
899+
},
870900
onSlashMenuDown: () => setSlashSelectedIndex((prev) => prev + 1),
871901
onSlashMenuUp: () => setSlashSelectedIndex((prev) => prev - 1),
872902
onSlashMenuTab: () => {
@@ -1060,6 +1090,7 @@ export const Chat = ({
10601090
setSlashSelectedIndex,
10611091
slashMatches,
10621092
slashSelectedIndex,
1093+
slashContext,
10631094
onSubmitPrompt,
10641095
agentMode,
10651096
handleCommandResult,
@@ -1068,6 +1099,7 @@ export const Chat = ({
10681099
fileMatches,
10691100
agentSelectedIndex,
10701101
mentionContext,
1102+
inputValue,
10711103
cursorPosition,
10721104
openFileMenuWithTab,
10731105
navigateUp,

cli/src/hooks/use-chat-keyboard.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type ChatKeyboardHandlers = {
3333
onInterruptStream: () => void
3434

3535
// Slash menu handlers
36+
onCloseSlashMenu: () => void
3637
onSlashMenuDown: () => void
3738
onSlashMenuUp: () => void
3839
onSlashMenuTab: () => void
@@ -41,6 +42,7 @@ export type ChatKeyboardHandlers = {
4142
onSlashMenuComplete: () => void
4243

4344
// Mention menu handlers
45+
onCloseMentionMenu: () => void
4446
onMentionMenuDown: () => void
4547
onMentionMenuUp: () => void
4648
onMentionMenuTab: () => void
@@ -125,6 +127,12 @@ function dispatchAction(
125127
case 'interrupt-stream':
126128
handlers.onInterruptStream()
127129
return true
130+
case 'close-slash-menu':
131+
handlers.onCloseSlashMenu()
132+
return true
133+
case 'close-mention-menu':
134+
handlers.onCloseMentionMenu()
135+
return true
128136
case 'slash-menu-down':
129137
handlers.onSlashMenuDown()
130138
return true

cli/src/utils/__tests__/keyboard-actions.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,90 @@ describe('resolveChatKeyboardAction', () => {
200200
})
201201
})
202202

203+
describe('escape closes menus', () => {
204+
test('escape closes slash menu when active', () => {
205+
const state: ChatKeyboardState = {
206+
...defaultState,
207+
slashMenuActive: true,
208+
slashMatchesLength: 5,
209+
slashSelectedIndex: 2,
210+
}
211+
expect(resolveChatKeyboardAction(escapeKey, state)).toEqual({
212+
type: 'close-slash-menu',
213+
})
214+
})
215+
216+
test('escape closes mention menu when active', () => {
217+
const state: ChatKeyboardState = {
218+
...defaultState,
219+
mentionMenuActive: true,
220+
totalMentionMatches: 5,
221+
agentSelectedIndex: 2,
222+
}
223+
expect(resolveChatKeyboardAction(escapeKey, state)).toEqual({
224+
type: 'close-mention-menu',
225+
})
226+
})
227+
228+
test('escape does not close slash menu when disabled', () => {
229+
const state: ChatKeyboardState = {
230+
...defaultState,
231+
slashMenuActive: true,
232+
slashMatchesLength: 5,
233+
disableSlashSuggestions: true,
234+
}
235+
expect(resolveChatKeyboardAction(escapeKey, state)).toEqual({
236+
type: 'none',
237+
})
238+
})
239+
240+
test('escape does not close slash menu with no matches', () => {
241+
const state: ChatKeyboardState = {
242+
...defaultState,
243+
slashMenuActive: true,
244+
slashMatchesLength: 0,
245+
}
246+
expect(resolveChatKeyboardAction(escapeKey, state)).toEqual({
247+
type: 'none',
248+
})
249+
})
250+
251+
test('escape does not close mention menu with no matches', () => {
252+
const state: ChatKeyboardState = {
253+
...defaultState,
254+
mentionMenuActive: true,
255+
totalMentionMatches: 0,
256+
}
257+
expect(resolveChatKeyboardAction(escapeKey, state)).toEqual({
258+
type: 'none',
259+
})
260+
})
261+
262+
test('escape in feedback mode exits feedback before closing menu', () => {
263+
const state: ChatKeyboardState = {
264+
...defaultState,
265+
feedbackMode: true,
266+
slashMenuActive: true,
267+
slashMatchesLength: 5,
268+
}
269+
expect(resolveChatKeyboardAction(escapeKey, state)).toEqual({
270+
type: 'exit-feedback-mode',
271+
})
272+
})
273+
274+
test('escape in non-default mode exits mode before closing menu', () => {
275+
const state: ChatKeyboardState = {
276+
...defaultState,
277+
inputMode: 'bash',
278+
slashMenuActive: true,
279+
slashMatchesLength: 5,
280+
}
281+
expect(resolveChatKeyboardAction(escapeKey, state)).toEqual({
282+
type: 'exit-input-mode',
283+
})
284+
})
285+
})
286+
203287
describe('slash menu navigation', () => {
204288
const slashMenuState: ChatKeyboardState = {
205289
...defaultState,

cli/src/utils/keyboard-actions.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export type ChatKeyboardAction =
6060
| { type: 'interrupt-stream' }
6161

6262
// Menu navigation
63+
| { type: 'close-slash-menu' }
64+
| { type: 'close-mention-menu' }
6365
| { type: 'slash-menu-down' }
6466
| { type: 'slash-menu-up' }
6567
| { type: 'slash-menu-tab' }
@@ -187,6 +189,20 @@ export function resolveChatKeyboardAction(
187189
return { type: 'backspace-exit-mode' }
188190
}
189191

192+
// Priority 5.5: Escape closes active menus
193+
if (isEscape) {
194+
if (
195+
state.slashMenuActive &&
196+
state.slashMatchesLength > 0 &&
197+
!state.disableSlashSuggestions
198+
) {
199+
return { type: 'close-slash-menu' }
200+
}
201+
if (state.mentionMenuActive && state.totalMentionMatches > 0) {
202+
return { type: 'close-mention-menu' }
203+
}
204+
}
205+
190206
// Priority 6: Slash menu navigation (when active and not disabled)
191207
// Skip menu navigation for Up/Down if history navigation is enabled (user is paging through history)
192208
if (

0 commit comments

Comments
 (0)