Skip to content

Commit b8f2bd9

Browse files
committed
Prevent keypad enter from double submitting suggestions
1 parent f36ea5e commit b8f2bd9

3 files changed

Lines changed: 109 additions & 33 deletions

File tree

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

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { SuggestionMenu, type SuggestionItem } from './suggestion-menu'
1111
import { useAskUserBridge } from '../hooks/use-ask-user-bridge'
1212
import { useEvent } from '../hooks/use-event'
1313
import { useChatStore } from '../state/chat-store'
14+
import { shouldInterceptChatInputKey } from '../utils/chat-input-key-intercept'
1415
import { getInputModeConfig } from '../utils/input-modes'
15-
import { isLinefeedActingAsEnter } from '../utils/terminal-enter-detection'
1616
import { BORDER_CHARS } from '../utils/ui-constants'
1717

1818
import type { useTheme } from '../hooks/use-theme'
@@ -133,38 +133,13 @@ export const ChatInputBar = ({
133133
meta?: boolean
134134
option?: boolean
135135
}) => {
136-
const isPlainEnter =
137-
(key.name === 'return' || key.name === 'enter' ||
138-
(key.name === 'linefeed' && isLinefeedActingAsEnter())) &&
139-
!key.shift &&
140-
!key.ctrl &&
141-
!key.meta &&
142-
!key.option
143-
const isTab = key.name === 'tab' && !key.ctrl && !key.meta && !key.option
144-
const isUp = key.name === 'up' && !key.ctrl && !key.meta && !key.option
145-
const isDown = key.name === 'down' && !key.ctrl && !key.meta && !key.option
146-
const isUpDown = isUp || isDown
147-
148-
const hasSuggestions = hasSlashSuggestions || hasMentionSuggestions
149-
if (hasSuggestions) {
150-
if (isUpDown && lastEditDueToNav) {
151-
return true
152-
}
153-
if (isPlainEnter || isTab || isUpDown) {
154-
return true
155-
}
156-
}
157-
158-
const historyUpEnabled = lastEditDueToNav || cursorPosition === 0
159-
const historyDownEnabled = lastEditDueToNav || cursorPosition === inputValue.length
160-
if (isUp && historyUpEnabled) {
161-
return true
162-
}
163-
if (isDown && historyDownEnabled) {
164-
return true
165-
}
166-
167-
return false
136+
return shouldInterceptChatInputKey(key, {
137+
hasSlashSuggestions,
138+
hasMentionSuggestions,
139+
lastEditDueToNav,
140+
cursorPosition,
141+
inputLength: inputValue.length,
142+
})
168143
},
169144
)
170145

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import { shouldInterceptChatInputKey } from '../chat-input-key-intercept'
4+
5+
const baseState = {
6+
hasSlashSuggestions: false,
7+
hasMentionSuggestions: false,
8+
lastEditDueToNav: false,
9+
cursorPosition: 1,
10+
inputLength: 3,
11+
}
12+
13+
describe('shouldInterceptChatInputKey', () => {
14+
test('intercepts keypad Enter while slash suggestions are visible', () => {
15+
expect(
16+
shouldInterceptChatInputKey(
17+
{ name: 'kpenter', sequence: '\x1b[57414u' },
18+
{ ...baseState, hasSlashSuggestions: true },
19+
),
20+
).toBe(true)
21+
})
22+
23+
test('intercepts raw application keypad Enter while mention suggestions are visible', () => {
24+
expect(
25+
shouldInterceptChatInputKey(
26+
{ sequence: '\x1bOM' },
27+
{ ...baseState, hasMentionSuggestions: true },
28+
),
29+
).toBe(true)
30+
})
31+
32+
test('does not intercept keypad Enter without visible suggestions', () => {
33+
expect(
34+
shouldInterceptChatInputKey(
35+
{ name: 'kpenter', sequence: '\x1b[57414u' },
36+
baseState,
37+
),
38+
).toBe(false)
39+
})
40+
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { isKeypadEnter } from './keypad-keys'
2+
import { isLinefeedActingAsEnter } from './terminal-enter-detection'
3+
4+
type ChatInputKey = {
5+
name?: string
6+
sequence?: string
7+
shift?: boolean
8+
ctrl?: boolean
9+
meta?: boolean
10+
option?: boolean
11+
}
12+
13+
type ChatInputKeyInterceptState = {
14+
hasSlashSuggestions: boolean
15+
hasMentionSuggestions: boolean
16+
lastEditDueToNav: boolean
17+
cursorPosition: number
18+
inputLength: number
19+
}
20+
21+
export function shouldInterceptChatInputKey(
22+
key: ChatInputKey,
23+
state: ChatInputKeyInterceptState,
24+
): boolean {
25+
const isPlainEnter =
26+
(key.name === 'return' ||
27+
key.name === 'enter' ||
28+
isKeypadEnter(key) ||
29+
(key.name === 'linefeed' && isLinefeedActingAsEnter())) &&
30+
!key.shift &&
31+
!key.ctrl &&
32+
!key.meta &&
33+
!key.option
34+
const isTab = key.name === 'tab' && !key.ctrl && !key.meta && !key.option
35+
const isUp = key.name === 'up' && !key.ctrl && !key.meta && !key.option
36+
const isDown = key.name === 'down' && !key.ctrl && !key.meta && !key.option
37+
const isUpDown = isUp || isDown
38+
39+
const hasSuggestions =
40+
state.hasSlashSuggestions || state.hasMentionSuggestions
41+
if (hasSuggestions) {
42+
if (isUpDown && state.lastEditDueToNav) {
43+
return true
44+
}
45+
if (isPlainEnter || isTab || isUpDown) {
46+
return true
47+
}
48+
}
49+
50+
const historyUpEnabled = state.lastEditDueToNav || state.cursorPosition === 0
51+
const historyDownEnabled =
52+
state.lastEditDueToNav || state.cursorPosition === state.inputLength
53+
if (isUp && historyUpEnabled) {
54+
return true
55+
}
56+
if (isDown && historyDownEnabled) {
57+
return true
58+
}
59+
60+
return false
61+
}

0 commit comments

Comments
 (0)