Skip to content

Commit 9d41657

Browse files
committed
refactor(cli): use MultilineInput for ask-user "Other" text field
Replace manual keyboard handling in the ask-user form's free-text input with the shared MultilineInput component. This delegates character input, backspace, and cursor movement to MultilineInput while letting navigation keys (up/down/tab) bubble up to the form's keyboard handler.
1 parent dc4445a commit 9d41657

File tree

3 files changed

+75
-33
lines changed

3 files changed

+75
-33
lines changed

cli/src/components/ask-user/components/other-text-input.tsx

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,60 @@
33
*/
44

55
import React from 'react'
6-
import { TextAttributes } from '@opentui/core'
6+
import { TextAttributes, type KeyEvent } from '@opentui/core'
77
import { Button } from '../../button'
8+
import { MultilineInput } from '../../multiline-input'
89
import { useTheme } from '../../../hooks/use-theme'
910
import { SYMBOLS } from '../constants'
11+
import type { InputValue } from '../../../state/chat-store'
12+
13+
// Width taken by symbol + "Other:" label + padding
14+
const LABEL_WIDTH = 10
1015

1116
export interface OtherTextInputProps {
1217
text: string
1318
isFocused: boolean
1419
hasText: boolean
1520
isSelected: boolean
21+
width: number
22+
cursorPosition: number
1623
onClick: () => void
1724
onMouseOver: () => void
25+
onChange: (value: InputValue) => void
26+
onSubmit: () => void
1827
}
1928

2029
export const OtherTextInput: React.FC<OtherTextInputProps> = ({
2130
text,
2231
isFocused,
2332
hasText,
2433
isSelected,
34+
width,
35+
cursorPosition,
2536
onClick,
2637
onMouseOver,
38+
onChange,
39+
onSubmit,
2740
}) => {
2841
const theme = useTheme()
2942

30-
// Display placeholder or actual text with cursor
31-
const displayText = text || (isFocused ? '' : 'Type your own answer...')
32-
const textWithCursor = isFocused && text ? `${text}|` : isFocused ? '|' : displayText
43+
const placeholder = 'Type your own answer...'
44+
45+
// Calculate available width for the input (full width minus label and padding)
46+
const inputWidth = Math.max(10, width - LABEL_WIDTH)
47+
48+
// Intercept navigation keys that should be handled by the ask-user form
49+
const handleKeyIntercept = (key: KeyEvent): boolean => {
50+
// Let Up/Down/Tab be handled by the form's navigation
51+
if (
52+
(key.name === 'up' || key.name === 'down' || key.name === 'tab') &&
53+
!key.ctrl &&
54+
!key.meta
55+
) {
56+
return true
57+
}
58+
return false
59+
}
3360

3461
return (
3562
<Button
@@ -46,7 +73,12 @@ export const OtherTextInput: React.FC<OtherTextInputProps> = ({
4673
>
4774
<text
4875
style={{
49-
fg: hasText || isSelected ? theme.primary : isFocused ? theme.foreground : theme.muted,
76+
fg:
77+
hasText || isSelected
78+
? theme.primary
79+
: isFocused
80+
? theme.foreground
81+
: theme.muted,
5082
attributes: isFocused ? TextAttributes.BOLD : undefined,
5183
}}
5284
>
@@ -60,14 +92,21 @@ export const OtherTextInput: React.FC<OtherTextInputProps> = ({
6092
>
6193
Other:
6294
</text>
63-
<text
64-
style={{
65-
fg: hasText ? theme.primary : theme.muted,
66-
attributes: isFocused ? TextAttributes.BOLD : undefined,
67-
}}
68-
>
69-
{textWithCursor}
70-
</text>
95+
<box style={{ flexGrow: 1, minWidth: 0, paddingLeft: 1 }}>
96+
<MultilineInput
97+
value={text}
98+
onChange={onChange}
99+
onSubmit={onSubmit}
100+
onKeyIntercept={handleKeyIntercept}
101+
placeholder={placeholder}
102+
focused={isFocused}
103+
maxHeight={3}
104+
minHeight={1}
105+
width={inputWidth}
106+
textAttributes={undefined}
107+
cursorPosition={cursorPosition}
108+
/>
109+
</box>
71110
</Button>
72111
)
73112
}

cli/src/components/ask-user/hooks/use-keyboard-navigation.ts

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,9 @@ export function useKeyboardNavigation(params: KeyboardNavigationParams) {
205205
!key.meta &&
206206
!key.shift
207207
) {
208-
// Handle space in text input: add space character and return
209-
if (isFocusOnTextInput(focus) && key.name === 'space') {
210-
const currentText = otherTexts[currentQuestionIndex] || ''
211-
onOtherTextChange(currentQuestionIndex, currentText + ' ')
208+
// When focused on the free-text input, let the MultilineInput component
209+
// handle character input and submission behavior.
210+
if (isFocusOnTextInput(focus)) {
212211
return
213212
}
214213

@@ -219,9 +218,6 @@ export function useKeyboardNavigation(params: KeyboardNavigationParams) {
219218
onSelectAnswer(focus.questionIndex, focus.optionIndex)
220219
// Let auto-advance hook handle the rest
221220
onAutoAdvance(focus.optionIndex)
222-
} else if (isFocusOnTextInput(focus)) {
223-
// Advance if text input has content
224-
onTextInputAdvance()
225221
} else if (isFocusOnConfirmSubmit(focus)) {
226222
// Submit from confirm screen
227223
onSubmit()
@@ -247,20 +243,10 @@ export function useKeyboardNavigation(params: KeyboardNavigationParams) {
247243
// Text Input Handling
248244
// ====================
249245

246+
// When focused on the free-text input, let the shared MultilineInput
247+
// component handle all text editing (characters, backspace, etc.).
250248
if (isFocusOnTextInput(focus)) {
251-
// Backspace
252-
if (key.name === 'backspace') {
253-
const currentText = otherTexts[currentQuestionIndex] || ''
254-
onOtherTextChange(currentQuestionIndex, currentText.slice(0, -1))
255-
return
256-
}
257-
258-
// Character input
259-
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
260-
const currentText = otherTexts[currentQuestionIndex] || ''
261-
onOtherTextChange(currentQuestionIndex, currentText + key.sequence)
262-
return
263-
}
249+
return
264250
}
265251
},
266252
[] // Empty deps - callback is stable, values accessed via ref

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export const MultipleChoiceForm: React.FC<MultipleChoiceFormProps> = ({
4141
const theme = useTheme()
4242
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
4343
const [isOnConfirmScreen, setIsOnConfirmScreen] = useState(false)
44+
const [otherCursorPositions, setOtherCursorPositions] = useState<number[]>(
45+
() => questions.map(() => 0),
46+
)
4447

4548
// Notify parent when question changes
4649
React.useEffect(() => {
@@ -265,8 +268,22 @@ export const MultipleChoiceForm: React.FC<MultipleChoiceFormProps> = ({
265268
}
266269
hasText={!!otherTexts[currentQuestionIndex]?.trim()}
267270
isSelected={false}
271+
width={width}
272+
cursorPosition={
273+
otherCursorPositions[currentQuestionIndex] ??
274+
(otherTexts[currentQuestionIndex] || '').length
275+
}
268276
onClick={() => focusActions.selectTextInput(currentQuestionIndex)}
269277
onMouseOver={() => focusActions.selectTextInput(currentQuestionIndex)}
278+
onChange={({ text, cursorPosition }) => {
279+
onOtherTextChange(currentQuestionIndex, text)
280+
setOtherCursorPositions((prev) => {
281+
const next = [...prev]
282+
next[currentQuestionIndex] = cursorPosition
283+
return next
284+
})
285+
}}
286+
onSubmit={handleTextInputAdvance}
270287
/>
271288
</box>
272289
</box>

0 commit comments

Comments
 (0)