Skip to content

Commit 3d2146d

Browse files
feat(multiline-input): make newline handling more predictable and shortcut-friendly (Ctrl+J, Option+Enter) by refactoring enter-key detection.
This improves user experience by honoring common keyboard shortcuts and preventing unintended submissions during multi-line input. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 2d89c32 commit 3d2146d

File tree

1 file changed

+72
-7
lines changed

1 file changed

+72
-7
lines changed

cli/src/components/multiline-input.tsx

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { useCallback, useState, useEffect, useMemo, useRef } from 'react'
33

44
import { TextAttributes, type ScrollBoxRenderable } from '@opentui/core'
55

6+
import { logger } from '../utils/logger'
7+
68
const mixColors = (foreground: string, background: string, alpha = 0.4): string => {
79
const parseHex = (hex: string) => {
810
const normalized = hex.trim().replace('#', '')
@@ -197,15 +199,72 @@ export function MultilineInput({
197199
key.sequence[1] !== '['),
198200
)
199201

200-
// Enter (without shift) submits
201-
if (key.name === 'return' && !key.shift) {
202-
if ('preventDefault' in key) (key as any).preventDefault()
203-
onSubmit()
204-
return
202+
const isEnterKey = key.name === 'return' || key.name === 'enter'
203+
const hasEscapePrefix =
204+
typeof key.sequence === 'string' &&
205+
key.sequence.length > 0 &&
206+
key.sequence.charCodeAt(0) === 0x1b
207+
const isPlainEnter =
208+
isEnterKey &&
209+
!key.shift &&
210+
!key.ctrl &&
211+
!key.meta &&
212+
!key.alt &&
213+
!key.option &&
214+
!isAltLikeModifier &&
215+
!hasEscapePrefix &&
216+
key.sequence === '\r'
217+
const isShiftEnter =
218+
isEnterKey &&
219+
(Boolean(key.shift) || key.sequence === '\n')
220+
const isOptionEnter = isEnterKey && (isAltLikeModifier || hasEscapePrefix)
221+
const isCtrlJ =
222+
key.ctrl &&
223+
!key.meta &&
224+
!key.option &&
225+
!key.alt &&
226+
(lowerKeyName === 'j' || isEnterKey)
227+
228+
if (isEnterKey || lowerKeyName === 'j') {
229+
const snapshot: Record<string, unknown> = {
230+
name: key.name,
231+
sequence: key.sequence,
232+
raw: (key as any).raw,
233+
ctrl: Boolean(key.ctrl),
234+
meta: Boolean(key.meta),
235+
alt: Boolean(key.alt),
236+
option: Boolean(key.option),
237+
shift: Boolean(key.shift),
238+
isEnterKey,
239+
hasEscapePrefix,
240+
code: (key as any).code,
241+
charCode: key.sequence ? key.sequence.charCodeAt(0) : null,
242+
}
243+
try {
244+
const ownProps = Object.getOwnPropertyNames(key)
245+
for (const prop of ownProps) {
246+
if (prop in snapshot) continue
247+
const value = (key as any)[prop]
248+
if (typeof value === 'function') continue
249+
snapshot[prop] = value
250+
}
251+
for (const prop in key) {
252+
if (prop in snapshot) continue
253+
const value = (key as any)[prop]
254+
if (typeof value === 'function') continue
255+
snapshot[prop] = value
256+
}
257+
} catch {
258+
// ignore property introspection errors
259+
}
260+
logger.info('[input-debug] keypress', {
261+
...snapshot,
262+
})
205263
}
206264

207-
// Shift+Enter creates newline
208-
if (key.name === 'return' && key.shift) {
265+
const shouldInsertNewline = isShiftEnter || isOptionEnter || isCtrlJ
266+
267+
if (shouldInsertNewline) {
209268
if ('preventDefault' in key) (key as any).preventDefault()
210269
const newValue =
211270
value.slice(0, cursorPosition) + '\n' + value.slice(cursorPosition)
@@ -214,6 +273,12 @@ export function MultilineInput({
214273
return
215274
}
216275

276+
if (isPlainEnter) {
277+
if ('preventDefault' in key) (key as any).preventDefault()
278+
onSubmit()
279+
return
280+
}
281+
217282
// Calculate boundaries for shortcuts
218283
const lineStart = findLineStart(value, cursorPosition)
219284
const lineEnd = findLineEnd(value, cursorPosition)

0 commit comments

Comments
 (0)