Files
llmx/codex-cli/tests/clear-command.test.tsx
nerdielol 797eba4930 fix: /clear now clears terminal screen and resets context left indicator (#425)
## What does this PR do?
* Implements the full `/clear` command in **codex‑cli**:
  * Resets chat history **and** wipes the terminal screen.
  * Shows a single system message: `Context cleared`.
* Adds comprehensive unit tests for the new behaviour.

## Why is it needed?
* Fixes user‑reported bugs:  
  * **#395**  
  * **#405**

## How is it implemented?
* **Code** – Adds `process.stdout.write('\x1b[3J\x1b[H\x1b[2J')` in
`terminal.tsx`. Removed reference to `prev` in `
        setItems((prev) => [
          ...prev,
` in `terminal-chat-new-input.tsx` & `terminal-chat-input.tsx`.

## CI / QA
All commands pass locally:
```bash
pnpm test      # green
pnpm run lint  # green
pnpm run typecheck  # zero TS errors
```

## Results



https://github.com/user-attachments/assets/11dcf05c-e054-495a-8ecb-ac6ef21a9da4

---------

Co-authored-by: Thibault Sottiaux <tibo@openai.com>
2025-04-21 12:39:46 -04:00

175 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import type { ComponentProps } from "react";
import { describe, it, expect, vi } from "vitest";
import { renderTui } from "./ui-test-helpers.js";
import TerminalChatInput from "../src/components/chat/terminal-chat-input.js";
import TerminalChatNewInput from "../src/components/chat/terminal-chat-new-input.js";
import * as TermUtils from "../src/utils/terminal.js";
// -------------------------------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------------------------------
async function type(
stdin: NodeJS.WritableStream,
text: string,
flush: () => Promise<void>,
): Promise<void> {
stdin.write(text);
await flush();
}
// -------------------------------------------------------------------------------------------------
// Tests
// -------------------------------------------------------------------------------------------------
describe("/clear command", () => {
it("invokes clearTerminal and resets context in TerminalChatInput", async () => {
const clearSpy = vi
.spyOn(TermUtils, "clearTerminal")
.mockImplementation(() => {});
const setItems = vi.fn();
// Minimal stub of a ResponseItem cast to bypass exhaustive type checks in this test context
const existingItems = [
{
id: "dummy-1",
type: "message",
role: "system",
content: [{ type: "input_text", text: "Old item" }],
},
] as Array<any>;
const props: ComponentProps<typeof TerminalChatInput> = {
isNew: false,
loading: false,
submitInput: () => {},
confirmationPrompt: null,
explanation: undefined,
submitConfirmation: () => {},
setLastResponseId: () => {},
setItems,
contextLeftPercent: 100,
openOverlay: () => {},
openModelOverlay: () => {},
openApprovalOverlay: () => {},
openHelpOverlay: () => {},
openDiffOverlay: () => {},
onCompact: () => {},
interruptAgent: () => {},
active: true,
thinkingSeconds: 0,
items: existingItems,
};
const { stdin, flush, cleanup } = renderTui(
<TerminalChatInput {...props} />,
);
await flush();
await type(stdin, "/clear", flush);
await type(stdin, "\r", flush); // press Enter
// Allow any asynchronous state updates to propagate
await flush();
expect(clearSpy).toHaveBeenCalledTimes(2);
expect(setItems).toHaveBeenCalledTimes(2);
const stateUpdater = setItems.mock.calls[0]![0];
expect(typeof stateUpdater).toBe("function");
const newItems = stateUpdater(existingItems);
expect(Array.isArray(newItems)).toBe(true);
expect(newItems).toHaveLength(2);
expect(newItems.at(-1)).toMatchObject({
role: "system",
type: "message",
content: [{ type: "input_text", text: "Terminal cleared" }],
});
cleanup();
clearSpy.mockRestore();
});
it("invokes clearTerminal and resets context in TerminalChatNewInput", async () => {
const clearSpy = vi
.spyOn(TermUtils, "clearTerminal")
.mockImplementation(() => {});
const setItems = vi.fn();
const props: ComponentProps<typeof TerminalChatNewInput> = {
isNew: false,
loading: false,
submitInput: () => {},
confirmationPrompt: null,
explanation: undefined,
submitConfirmation: () => {},
setLastResponseId: () => {},
setItems,
contextLeftPercent: 100,
openOverlay: () => {},
openModelOverlay: () => {},
openApprovalOverlay: () => {},
openHelpOverlay: () => {},
openDiffOverlay: () => {},
interruptAgent: () => {},
active: true,
thinkingSeconds: 0,
};
const { stdin, flush, cleanup } = renderTui(
<TerminalChatNewInput {...props} />,
);
await flush();
await type(stdin, "/clear", flush);
await type(stdin, "\r", flush); // press Enter
await flush();
expect(clearSpy).toHaveBeenCalledTimes(1);
expect(setItems).toHaveBeenCalledTimes(1);
const firstArg = setItems.mock.calls[0]![0];
expect(Array.isArray(firstArg)).toBe(true);
expect(firstArg).toHaveLength(1);
expect(firstArg[0]).toMatchObject({
role: "system",
type: "message",
content: [{ type: "input_text", text: "Terminal cleared" }],
});
cleanup();
clearSpy.mockRestore();
});
});
describe("clearTerminal", () => {
it("writes escape sequence to stdout", () => {
const originalQuiet = process.env["CODEX_QUIET_MODE"];
delete process.env["CODEX_QUIET_MODE"];
process.env["CODEX_QUIET_MODE"] = "0";
const writeSpy = vi
.spyOn(process.stdout, "write")
.mockImplementation(() => true);
TermUtils.clearTerminal();
expect(writeSpy).toHaveBeenCalledWith("\x1b[3J\x1b[H\x1b[2J");
writeSpy.mockRestore();
if (originalQuiet !== undefined) {
process.env["CODEX_QUIET_MODE"] = originalQuiet;
} else {
delete process.env["CODEX_QUIET_MODE"];
}
});
});