Skip to content

Commit 920ff45

Browse files
committed
Centralize plain enter key handling
1 parent 2fe436d commit 920ff45

12 files changed

Lines changed: 81 additions & 36 deletions

cli/src/components/ask-user/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { getOptionLabel, KEYBOARD_HINTS, CUSTOM_OPTION_INDEX } from './constants'
1717
import { useTheme } from '../../hooks/use-theme'
1818
import { useChatStore } from '../../state/chat-store'
19+
import { isPlainEnterKey } from '../../utils/terminal-enter-detection'
1920
import { BORDER_CHARS } from '../../utils/ui-constants'
2021
import { Button } from '../button'
2122

@@ -338,7 +339,7 @@ export const MultipleChoiceForm: React.FC<MultipleChoiceFormProps> = ({
338339
}
339340
return
340341
}
341-
if (key.name === 'return' || key.name === 'enter' || key.name === 'space') {
342+
if (isPlainEnterKey(key) || key.name === 'space') {
342343
preventDefault()
343344
handleSubmit()
344345
return
@@ -442,7 +443,7 @@ export const MultipleChoiceForm: React.FC<MultipleChoiceFormProps> = ({
442443
return
443444
}
444445

445-
if (key.name === 'return' || key.name === 'enter' || key.name === 'space') {
446+
if (isPlainEnterKey(key) || key.name === 'space') {
446447
preventDefault()
447448

448449
if (expandedIndex === null) {

cli/src/components/chat-history-screen.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
formatRelativeTime,
1313
getAllChats,
1414
} from '../utils/chat-history'
15+
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
1516

1617
import type { SelectableListItem } from './selectable-list'
1718

@@ -170,7 +171,14 @@ export const ChatHistoryScreen: React.FC<ChatHistoryScreenProps> = ({
170171

171172
// Handle keyboard input
172173
const handleKeyIntercept = useCallback(
173-
(key: { name?: string; shift?: boolean; ctrl?: boolean }) => {
174+
(key: {
175+
name?: string
176+
sequence?: string
177+
shift?: boolean
178+
ctrl?: boolean
179+
meta?: boolean
180+
option?: boolean
181+
}) => {
174182
if (key.name === 'escape') {
175183
if (searchQuery.length > 0) {
176184
setSearchQuery('')
@@ -189,7 +197,7 @@ export const ChatHistoryScreen: React.FC<ChatHistoryScreenProps> = ({
189197
setFocusedIndex((prev) => Math.min(maxIndex, prev + 1))
190198
return true
191199
}
192-
if (key.name === 'return' || key.name === 'enter') {
200+
if (isPlainEnterKey(key)) {
193201
const focused = filteredItems[focusedIndex]
194202
if (focused) {
195203
onSelectChat(focused.id)

cli/src/components/feedback-input-mode.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useTheme } from '../hooks/use-theme'
88
import { useChatStore } from '../state/chat-store'
99
import { IS_FREEBUFF } from '../utils/constants'
1010
import { createTextPasteHandler } from '../utils/strings'
11+
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
1112
import { BORDER_CHARS } from '../utils/ui-constants'
1213

1314
import type { FeedbackCategory } from '@codebuff/common/constants/feedback'
@@ -120,8 +121,7 @@ const FeedbackTextSection: React.FC<FeedbackTextSectionProps> = ({
120121
}}
121122
onSubmit={onSubmit}
122123
onKeyIntercept={(key) => {
123-
const isEnter = key.name === 'return' || key.name === 'enter'
124-
if (!isEnter) return false
124+
if (!isPlainEnterKey(key)) return false
125125
// Just add newline on Enter
126126
const newText = value.slice(0, cursor) + '\n' + value.slice(cursor)
127127
onChange(newText)

cli/src/components/project-picker-screen.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useTerminalLayout } from '../hooks/use-terminal-layout'
1515
import { useTheme } from '../hooks/use-theme'
1616
import { formatCwd } from '../utils/path-helpers'
1717
import { loadRecentProjects } from '../utils/recent-projects'
18+
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
1819
import { getLogoBlockColor, getLogoAccentColor } from '../utils/theme-system'
1920

2021
import type { SelectableListItem } from './selectable-list'
@@ -226,7 +227,14 @@ export const ProjectPickerScreen: React.FC<ProjectPickerScreenProps> = ({
226227

227228
// Handle search input keyboard intercept
228229
const handleSearchKeyIntercept = useCallback(
229-
(key: { name?: string; shift?: boolean; ctrl?: boolean }) => {
230+
(key: {
231+
name?: string
232+
sequence?: string
233+
shift?: boolean
234+
ctrl?: boolean
235+
meta?: boolean
236+
option?: boolean
237+
}) => {
230238
if (key.name === 'escape') {
231239
if (searchQuery.length > 0) {
232240
setSearchQuery('')
@@ -246,7 +254,7 @@ export const ProjectPickerScreen: React.FC<ProjectPickerScreenProps> = ({
246254
)
247255
return true
248256
}
249-
if (key.name === 'return' || key.name === 'enter') {
257+
if (isPlainEnterKey(key)) {
250258
// If search looks like a path, try to navigate there directly
251259
if (searchQuery.startsWith('/') || searchQuery.startsWith('~')) {
252260
if (tryNavigateToPath(searchQuery)) {

cli/src/components/publish-container.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useTheme } from '../hooks/use-theme'
1515
import { useChatStore } from '../state/chat-store'
1616
import { usePublishStore } from '../state/publish-store'
1717
import { loadLocalAgents, loadAgentDefinitions } from '../utils/local-agent-registry'
18+
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
1819
import { BORDER_CHARS } from '../utils/ui-constants'
1920

2021

@@ -110,7 +111,14 @@ export const PublishContainer: React.FC<PublishContainerProps> = ({
110111

111112
// Handle keyboard navigation in checklist
112113
const handleSearchKeyIntercept = useCallback(
113-
(key: { name?: string; shift?: boolean }) => {
114+
(key: {
115+
name?: string
116+
sequence?: string
117+
shift?: boolean
118+
ctrl?: boolean
119+
meta?: boolean
120+
option?: boolean
121+
}) => {
114122
if (key.name === 'escape') {
115123
// Escape: clear input if there is any, otherwise exit publish mode
116124
if (searchQuery.length > 0) {
@@ -129,7 +137,7 @@ export const PublishContainer: React.FC<PublishContainerProps> = ({
129137
setFocusedIndex(Math.min(filteredAgents.length - 1, focusedIndex + 1))
130138
return true
131139
}
132-
if (key.name === 'return' || key.name === 'enter') {
140+
if (isPlainEnterKey(key)) {
133141
// Enter: toggle selection
134142
const agent = filteredAgents[focusedIndex]
135143
if (agent) {

cli/src/components/review-screen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react'
33

44
import { buildReviewPrompt, REVIEW_BASE_PROMPT } from '../commands/prompt-builders'
55
import { useTheme } from '../hooks/use-theme'
6+
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
67
import { BORDER_CHARS } from '../utils/ui-constants'
78

89
import type { KeyEvent } from '@opentui/core'
@@ -61,7 +62,7 @@ export const ReviewScreen: React.FC<ReviewScreenProps> = ({
6162
setSelectedIndex((prev) => Math.min(REVIEW_OPTIONS.length - 1, prev + 1))
6263
return
6364
}
64-
if (key.name === 'return' || key.name === 'enter') {
65+
if (isPlainEnterKey(key)) {
6566
const option = REVIEW_OPTIONS[selectedIndex]
6667
if (option) {
6768
handleSelect(option)

cli/src/components/session-ended-banner.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { useTheme } from '../hooks/use-theme'
1212
import { useFreebuffSessionStore } from '../state/freebuff-session-store'
1313
import { formatSessionUnits } from '../utils/format-session-units'
14+
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
1415
import { BORDER_CHARS } from '../utils/ui-constants'
1516

1617
import type { KeyEvent } from '@opentui/core'
@@ -89,7 +90,7 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
8990
useCallback(
9091
(key: KeyEvent) => {
9192
if (!canRestart) return
92-
if (key.name === 'return' || key.name === 'enter') {
93+
if (isPlainEnterKey(key)) {
9394
key.preventDefault?.()
9495
startSameChatSession()
9596
return

cli/src/hooks/use-login-keyboard-handlers.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useKeyboard } from '@opentui/react'
22
import { useCallback } from 'react'
33

4+
import { isPlainEnterKey } from '../utils/terminal-enter-detection'
5+
46
import type { KeyEvent } from '@opentui/core'
57

68
interface UseLoginKeyboardHandlersParams {
@@ -27,11 +29,7 @@ export function useLoginKeyboardHandlers({
2729
useKeyboard(
2830
useCallback(
2931
(key: KeyEvent) => {
30-
const isEnter =
31-
(key.name === 'return' || key.name === 'enter') &&
32-
!key.ctrl &&
33-
!key.meta &&
34-
!key.shift
32+
const isEnter = isPlainEnterKey(key)
3533

3634
const isCKey = key.name === 'c' && !key.ctrl && !key.meta && !key.shift
3735
const isCtrlC = key.ctrl && key.name === 'c'

cli/src/utils/__tests__/terminal-enter-detection.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
22

33
import {
44
isLinefeedActingAsEnter,
5+
isPlainEnterKey,
56
markReturnKeySeenForKey,
67
resetReturnKeySeenForTests,
78
shouldMarkReturnKeySeen,
@@ -49,4 +50,21 @@ describe('terminal enter detection', () => {
4950

5051
expect(isLinefeedActingAsEnter()).toBe(true)
5152
})
53+
54+
test('recognizes keypad Enter as plain Enter', () => {
55+
expect(
56+
isPlainEnterKey({ name: 'kpenter', sequence: '\x1b[57414u' }),
57+
).toBe(true)
58+
expect(isPlainEnterKey({ name: '', sequence: '\x1bOM' })).toBe(true)
59+
})
60+
61+
test('does not recognize modified keypad Enter as plain Enter', () => {
62+
expect(
63+
isPlainEnterKey({
64+
name: 'kpenter',
65+
sequence: '\x1b[57414u',
66+
shift: true,
67+
}),
68+
).toBe(false)
69+
})
5270
})

cli/src/utils/chat-input-key-intercept.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { isKeypadEnter } from './keypad-keys'
2-
import { isLinefeedActingAsEnter } from './terminal-enter-detection'
1+
import { isPlainEnterKey } from './terminal-enter-detection'
32

43
type ChatInputKey = {
54
name?: string
@@ -22,15 +21,7 @@ export function shouldInterceptChatInputKey(
2221
key: ChatInputKey,
2322
state: ChatInputKeyInterceptState,
2423
): 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
24+
const isPlainEnter = isPlainEnterKey(key)
3425
const isTab = key.name === 'tab' && !key.ctrl && !key.meta && !key.option
3526
const isUp = key.name === 'up' && !key.ctrl && !key.meta && !key.option
3627
const isDown = key.name === 'down' && !key.ctrl && !key.meta && !key.option

0 commit comments

Comments
 (0)