mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
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:
parent
2ac099caaf
commit
6f693f1b71
2 changed files with 154 additions and 18 deletions
143
packages/cli/src/ui/components/shared/TextInput.test.tsx
Normal file
143
packages/cli/src/ui/components/shared/TextInput.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue