feat: allow multi-line input (#438)
## Description This PR implements multi-line input support for Codex when it asks for user feedback (Issue #344). Users can now use Shift+Enter to add new lines in their responses, making it easier to provide formatted code snippets, lists, or other structured content. ## Changes - Replace the single-line TextInput component with the MultilineTextEditor component in terminal-chat-input.tsx - Add support for Shift+Enter to create new lines - Update key handling logic to properly handle history navigation in a multi-line context - Add reference to the editor to access cursor position information - Update help text to inform users about the Shift+Enter functionality - Add tests for the new functionality ## Testing - Added new test file (terminal-chat-input-multiline.test.tsx) to test the multi-line input functionality - All existing tests continue to pass - Manually tested the feature to ensure it works as expected ## Fixes Closes #344 ## Screenshots N/A ## Additional Notes This implementation maintains backward compatibility while adding the requested multi-line input functionality. The UI remains clean and intuitive, with a simple hint about using Shift+Enter for new lines. --------- Co-authored-by: Thibault Sottiaux <tibo@openai.com>
This commit is contained in:
157
codex-cli/tests/terminal-chat-input-multiline.test.tsx
Normal file
157
codex-cli/tests/terminal-chat-input-multiline.test.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React from "react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { renderTui } from "./ui-test-helpers.js";
|
||||
import TerminalChatInput from "../src/components/chat/terminal-chat-input.js";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
// Helper that lets us type and then immediately flush ink's async timers
|
||||
async function type(
|
||||
stdin: NodeJS.WritableStream,
|
||||
text: string,
|
||||
flush: () => Promise<void>,
|
||||
) {
|
||||
stdin.write(text);
|
||||
await flush();
|
||||
}
|
||||
|
||||
// Mock the createInputItem function to avoid filesystem operations
|
||||
vi.mock("../src/utils/input-utils.js", () => ({
|
||||
createInputItem: vi.fn(async (text: string) => ({
|
||||
role: "user",
|
||||
type: "message",
|
||||
content: [{ type: "input_text", text }],
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("TerminalChatInput multiline functionality", () => {
|
||||
it("renders the multiline editor component", async () => {
|
||||
const props: ComponentProps<typeof TerminalChatInput> = {
|
||||
isNew: false,
|
||||
loading: false,
|
||||
submitInput: () => {},
|
||||
confirmationPrompt: null,
|
||||
explanation: undefined,
|
||||
submitConfirmation: () => {},
|
||||
setLastResponseId: () => {},
|
||||
setItems: () => {},
|
||||
contextLeftPercent: 50,
|
||||
openOverlay: () => {},
|
||||
openDiffOverlay: () => {},
|
||||
openModelOverlay: () => {},
|
||||
openApprovalOverlay: () => {},
|
||||
openHelpOverlay: () => {},
|
||||
onCompact: () => {},
|
||||
interruptAgent: () => {},
|
||||
active: true,
|
||||
thinkingSeconds: 0,
|
||||
};
|
||||
|
||||
const { lastFrameStripped } = renderTui(<TerminalChatInput {...props} />);
|
||||
const frame = lastFrameStripped();
|
||||
|
||||
// Check that the help text mentions shift+enter for new line
|
||||
expect(frame).toContain("shift+enter for new line");
|
||||
});
|
||||
|
||||
it("allows multiline input with shift+enter", async () => {
|
||||
const submitInput = vi.fn();
|
||||
|
||||
const props: ComponentProps<typeof TerminalChatInput> = {
|
||||
isNew: false,
|
||||
loading: false,
|
||||
submitInput,
|
||||
confirmationPrompt: null,
|
||||
explanation: undefined,
|
||||
submitConfirmation: () => {},
|
||||
setLastResponseId: () => {},
|
||||
setItems: () => {},
|
||||
contextLeftPercent: 50,
|
||||
openOverlay: () => {},
|
||||
openDiffOverlay: () => {},
|
||||
openModelOverlay: () => {},
|
||||
openApprovalOverlay: () => {},
|
||||
openHelpOverlay: () => {},
|
||||
onCompact: () => {},
|
||||
interruptAgent: () => {},
|
||||
active: true,
|
||||
thinkingSeconds: 0,
|
||||
};
|
||||
|
||||
const { stdin, lastFrameStripped, flush, cleanup } = renderTui(
|
||||
<TerminalChatInput {...props} />,
|
||||
);
|
||||
|
||||
// Type some text
|
||||
await type(stdin, "first line", flush);
|
||||
|
||||
// Send Shift+Enter (CSI-u format)
|
||||
await type(stdin, "\u001B[13;2u", flush);
|
||||
|
||||
// Type more text
|
||||
await type(stdin, "second line", flush);
|
||||
|
||||
// Check that both lines are visible in the editor
|
||||
const frame = lastFrameStripped();
|
||||
expect(frame).toContain("first line");
|
||||
expect(frame).toContain("second line");
|
||||
|
||||
// Submit the multiline input with Enter
|
||||
await type(stdin, "\r", flush);
|
||||
|
||||
// Check that submitInput was called with the multiline text
|
||||
expect(submitInput).toHaveBeenCalledTimes(1);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("allows multiline input with shift+enter (modifyOtherKeys=1 format)", async () => {
|
||||
const submitInput = vi.fn();
|
||||
|
||||
const props: ComponentProps<typeof TerminalChatInput> = {
|
||||
isNew: false,
|
||||
loading: false,
|
||||
submitInput,
|
||||
confirmationPrompt: null,
|
||||
explanation: undefined,
|
||||
submitConfirmation: () => {},
|
||||
setLastResponseId: () => {},
|
||||
setItems: () => {},
|
||||
contextLeftPercent: 50,
|
||||
openOverlay: () => {},
|
||||
openDiffOverlay: () => {},
|
||||
openModelOverlay: () => {},
|
||||
openApprovalOverlay: () => {},
|
||||
openHelpOverlay: () => {},
|
||||
onCompact: () => {},
|
||||
interruptAgent: () => {},
|
||||
active: true,
|
||||
thinkingSeconds: 0,
|
||||
};
|
||||
|
||||
const { stdin, lastFrameStripped, flush, cleanup } = renderTui(
|
||||
<TerminalChatInput {...props} />,
|
||||
);
|
||||
|
||||
// Type some text
|
||||
await type(stdin, "first line", flush);
|
||||
|
||||
// Send Shift+Enter (modifyOtherKeys=1 format)
|
||||
await type(stdin, "\u001B[27;2;13~", flush);
|
||||
|
||||
// Type more text
|
||||
await type(stdin, "second line", flush);
|
||||
|
||||
// Check that both lines are visible in the editor
|
||||
const frame = lastFrameStripped();
|
||||
expect(frame).toContain("first line");
|
||||
expect(frame).toContain("second line");
|
||||
|
||||
// Submit the multiline input with Enter
|
||||
await type(stdin, "\r", flush);
|
||||
|
||||
// Check that submitInput was called with the multiline text
|
||||
expect(submitInput).toHaveBeenCalledTimes(1);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user