diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index 8768dd8d..aad2e6b3 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -191,6 +191,12 @@ export default function TerminalChatInput({ case "/bug": onSubmit(cmd); break; + case "/clear": + onSubmit(cmd); + break; + case "/clearhistory": + onSubmit(cmd); + break; default: break; } @@ -396,19 +402,29 @@ export default function TerminalChatInput({ setInput(""); setSessionId(""); setLastResponseId(""); + // Clear the terminal screen (including scrollback) before resetting context clearTerminal(); + // Emit a system notice in the chat; no raw console writes so Ink keeps control. + // Emit a system message to confirm the clear action. We *append* // it so Ink's treats it as new output and actually renders it. setItems((prev) => { const filteredOldItems = prev.filter((item) => { + // Remove any token‑heavy entries (user/assistant turns and function calls) if ( item.type === "message" && (item.role === "user" || item.role === "assistant") ) { return false; } - return true; + if ( + item.type === "function_call" || + item.type === "function_call_output" + ) { + return false; + } + return true; // keep developer/system and other meta entries }); return [ diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx index 95915934..b03cc963 100644 --- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx @@ -263,17 +263,16 @@ export default function TerminalChatInput({ setInput(""); setSessionId(""); setLastResponseId(""); + // Clear the terminal screen (including scrollback) before resetting context clearTerminal(); - // Emit a system message to confirm the clear action. We *append* - // it so Ink's treats it as new output and actually renders it. - setItems((prev) => [ - ...prev, + // Print a clear confirmation and reset conversation items. + setItems([ { id: `clear-${Date.now()}`, type: "message", role: "system", - content: [{ type: "input_text", text: "Context cleared" }], + content: [{ type: "input_text", text: "Terminal cleared" }], }, ]); diff --git a/codex-cli/src/utils/terminal.ts b/codex-cli/src/utils/terminal.ts index e8187eba..296cbb39 100644 --- a/codex-cli/src/utils/terminal.ts +++ b/codex-cli/src/utils/terminal.ts @@ -50,6 +50,8 @@ export function clearTerminal(): void { if (inkRenderer) { inkRenderer.clear(); } + // Also clear scrollback and primary buffer to ensure a truly blank slate + process.stdout.write("\x1b[3J\x1b[H\x1b[2J"); } export function onExit(): void { diff --git a/codex-cli/tests/clear-command.test.tsx b/codex-cli/tests/clear-command.test.tsx new file mode 100644 index 00000000..bab9b84c --- /dev/null +++ b/codex-cli/tests/clear-command.test.tsx @@ -0,0 +1,174 @@ +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, +): Promise { + 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; + + const props: ComponentProps = { + 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( + , + ); + + 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 = { + 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( + , + ); + + 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"]; + } + }); +});