Files
llmx/codex-cli/tests/terminal-chat-input-multiline.test.tsx
Brayden Moon 6d6ca454cd 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>
2025-04-20 08:51:38 -07:00

158 lines
4.5 KiB
TypeScript

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();
});
});