fix(cli): check NEWLINE before SUBMIT in TextInput to fix multiline input (#3068) (#3094)

The `|| key.name === 'return'` fallback in TextInput matched every Return
keypress (Shift+Enter, Ctrl+Enter, etc.) and routed them all to the submit
path, making the NEWLINE handler dead code. Multiline inputs like the agent
creation description step could not insert newlines via keyboard.

Reorder checks so NEWLINE is evaluated first in multiline mode, and restrict
the broad return fallback to single-line inputs only.
This commit is contained in:
tanzhenxin 2026-04-11 13:02:28 +08:00 committed by GitHub
parent 2ac099caaf
commit 6f693f1b71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 154 additions and 18 deletions

View file

@ -0,0 +1,143 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render } from 'ink-testing-library';
import { TextInput } from './TextInput.js';
import { useKeypress } from '../../hooks/useKeypress.js';
import type { Key } from '../../hooks/useKeypress.js';
vi.mock('../../hooks/useKeypress.js', () => ({
useKeypress: vi.fn(),
}));
vi.mock('../../semantic-colors.js', () => ({
theme: {
text: { accent: 'cyan' },
status: { error: 'red' },
},
}));
const mockedUseKeypress = vi.mocked(useKeypress);
function makeKey(overrides: Partial<Key>): Key {
return {
name: '',
ctrl: false,
meta: false,
shift: false,
paste: false,
sequence: '',
...overrides,
};
}
function captureKeypressHandler(): (key: Key) => void {
const calls = mockedUseKeypress.mock.calls;
if (calls.length === 0) {
throw new Error('useKeypress was not called');
}
// Return the most recent handler
return calls[calls.length - 1]![0] as (key: Key) => void;
}
describe('TextInput', () => {
let onChange: ReturnType<typeof vi.fn>;
let onSubmit: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
onChange = vi.fn();
onSubmit = vi.fn();
});
describe('multiline mode (height > 1)', () => {
it('submits on plain Enter', () => {
render(
<TextInput
value=""
onChange={onChange}
onSubmit={onSubmit}
height={5}
/>,
);
const handler = captureKeypressHandler();
handler(makeKey({ name: 'return', sequence: '\r' }));
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it('does NOT submit on Shift+Enter — inserts newline instead', () => {
render(
<TextInput
value=""
onChange={onChange}
onSubmit={onSubmit}
height={5}
/>,
);
const handler = captureKeypressHandler();
handler(makeKey({ name: 'return', shift: true, sequence: '\r' }));
expect(onSubmit).not.toHaveBeenCalled();
// onChange should be called with the newline character
expect(onChange).toHaveBeenCalled();
});
it('does NOT submit on Ctrl+Enter — inserts newline instead', () => {
render(
<TextInput
value=""
onChange={onChange}
onSubmit={onSubmit}
height={5}
/>,
);
const handler = captureKeypressHandler();
handler(makeKey({ name: 'return', ctrl: true, sequence: '\r' }));
expect(onSubmit).not.toHaveBeenCalled();
expect(onChange).toHaveBeenCalled();
});
});
describe('single-line mode (height = 1)', () => {
it('submits on plain Enter', () => {
render(
<TextInput
value=""
onChange={onChange}
onSubmit={onSubmit}
height={1}
/>,
);
const handler = captureKeypressHandler();
handler(makeKey({ name: 'return', sequence: '\r' }));
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it('submits on Shift+Enter (no newline concept in single-line)', () => {
render(
<TextInput
value=""
onChange={onChange}
onSubmit={onSubmit}
height={1}
/>,
);
const handler = captureKeypressHandler();
handler(makeKey({ name: 'return', shift: true, sequence: '\r' }));
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -93,27 +93,20 @@ export function TextInput({
return;
}
// Submit on Enter
if (keyMatchers[Command.SUBMIT](key) || key.name === 'return') {
if (allowMultiline) {
const [row, col] = buffer.cursor;
const line = buffer.lines[row];
const charBefore = col > 0 ? cpSlice(line, col - 1, col) : '';
if (charBefore === '\\') {
buffer.backspace();
buffer.newline();
} else {
handleSubmit();
}
} else {
handleSubmit();
}
// Multiline newline insertion (Shift+Enter etc.) — check before SUBMIT
// so that modified-Return keys aren't swallowed by the submit branch.
if (allowMultiline && keyMatchers[Command.NEWLINE](key)) {
buffer.newline();
return;
}
// Multiline newline insertion (Shift+Enter etc.)
if (allowMultiline && keyMatchers[Command.NEWLINE](key)) {
buffer.newline();
// Submit on Enter (plain Return). In single-line mode any Return
// variant submits since there is no newline concept.
if (
keyMatchers[Command.SUBMIT](key) ||
(!allowMultiline && key.name === 'return')
) {
handleSubmit();
return;
}