diff --git a/.changeset/bright-cats-new.md b/.changeset/bright-cats-new.md new file mode 100644 index 000000000..fa649373f --- /dev/null +++ b/.changeset/bright-cats-new.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Add `/new` as an alias of `/clear` in the eve dev TUI. It clears model-message history while preserving the current session and its durable resources. diff --git a/docs/guides/dev-tui.md b/docs/guides/dev-tui.md index f7317e19c..6c6dbf724 100644 --- a/docs/guides/dev-tui.md +++ b/docs/guides/dev-tui.md @@ -43,7 +43,7 @@ Each command echoes as an invocation line, asks through a bordered panel that ta | `/traces` | Opens the full-screen local trace viewer. See [Inspect traces](#inspect-traces). | | `/reset` | Starts a fresh session. | | `/cancel` | Cooperatively cancels the running turn while preserving the session and settled context. | -| `/clear` | Clears model-message history while preserving the current session and its durable resources. | +| `/clear` | Clears model-message history while preserving the current session and its durable resources. `/new` is an alias. | | `/compact` | Queues context compaction for the current session without sending a message. | | `/exit` | Quits the TUI. | | `/help` | Lists the commands available for the current local or remote session. | diff --git a/packages/eve/src/cli/dev/tui/prompt-commands.test.ts b/packages/eve/src/cli/dev/tui/prompt-commands.test.ts index 76b1ae9a5..7a15b09c6 100644 --- a/packages/eve/src/cli/dev/tui/prompt-commands.test.ts +++ b/packages/eve/src/cli/dev/tui/prompt-commands.test.ts @@ -17,6 +17,10 @@ describe("parsePromptCommand", () => { expect(parsePromptCommand("/cancel")).toEqual({ type: "cancel" }); }); + it("parses /new as a context clear", () => { + expect(parsePromptCommand("/new")).toEqual({ type: "clear" }); + }); + it("parses /exit and its /quit alias", () => { expect(parsePromptCommand("/exit")).toEqual({ type: "exit" }); expect(parsePromptCommand("/quit")).toEqual({ type: "exit" }); @@ -80,7 +84,6 @@ describe("parsePromptCommand", () => { }); it("rejects near-misses and ordinary prompts", () => { - expect(parsePromptCommand("/new")).toBeNull(); expect(parsePromptCommand("/models")).toBeNull(); expect(parsePromptCommand("/vercel")).toBeNull(); expect(parsePromptCommand("/vc")).toBeNull(); @@ -132,6 +135,7 @@ describe("promptCommandsFor", () => { describe("isPromptControlCommand", () => { it("is true exactly for recognized commands", () => { expect(isPromptControlCommand("/reset")).toBe(true); + expect(isPromptControlCommand("/new")).toBe(true); expect(isPromptControlCommand("/model gpt-5")).toBe(true); expect(isPromptControlCommand("/unknown")).toBe(false); expect(isPromptControlCommand("hello")).toBe(false); diff --git a/packages/eve/src/cli/dev/tui/prompt-commands.ts b/packages/eve/src/cli/dev/tui/prompt-commands.ts index 6cd53a5be..d6be90f32 100644 --- a/packages/eve/src/cli/dev/tui/prompt-commands.ts +++ b/packages/eve/src/cli/dev/tui/prompt-commands.ts @@ -71,7 +71,7 @@ const PROMPT_COMMAND_DEFINITIONS = [ }, { name: "clear", - aliases: [], + aliases: ["new"], description: "Clear the current session context", takesArgument: false, build: () => ({ type: "clear" }), @@ -174,10 +174,10 @@ export function isPromptCommandAvailableFor( /** * Recognizes the slash commands the prompt accepts. `/reset` clears the - * session and transcript; `/cancel` stops the running turn; `/clear` clears - * context; `/compact` queues context compaction; `/exit` (and `/quit`) - * terminate the TUI like Ctrl+C; extension commands are dispatched outside - * the runner. Anything else — including unknown `/text` — is a normal + * session and transcript; `/cancel` stops the running turn; `/clear` (and + * `/new`) clears context; `/compact` queues context compaction; `/exit` (and + * `/quit`) terminate the TUI like Ctrl+C; extension commands are dispatched + * outside the runner. Anything else — including unknown `/text` — is a normal * message. */ export function parsePromptCommand(prompt: string): PromptCommand | null { diff --git a/packages/eve/src/cli/dev/tui/runner.test.ts b/packages/eve/src/cli/dev/tui/runner.test.ts index b424bfd0c..194b276b5 100644 --- a/packages/eve/src/cli/dev/tui/runner.test.ts +++ b/packages/eve/src/cli/dev/tui/runner.test.ts @@ -698,45 +698,48 @@ describe("EveTUIRunner development session continuity", () => { expect(results).toEqual(["Compaction requested."]); }); - it("clears the active session context without sending a prompt", async () => { - const session = sessionYielding([]); - const clear = vi - .spyOn(session, "clear") - .mockResolvedValue({ sessionId: "session_1", status: "accepted" }); - const stream = vi.spyOn(session, "stream").mockReturnValue( - (async function* () { - yield stampTestEvent( - { - data: { continuationToken: "eve:test", wait: "next-user-message" }, - type: "session.waiting", - }, - 0, - ); - })(), - ); - const results: string[] = []; - const prompts: Array = ["/clear", undefined]; - const runner = new EveTUIRunner({ - name: "Weather Agent", - renderer: fakeRenderer({ - readPrompt: vi.fn(async () => prompts.shift()), - renderCommandResult: (message) => results.push(message), - renderStream: vi.fn(async (result) => { - for await (const event of result.events) { - void event; - } + it.each(["/clear", "/new"])( + "%s clears the active session context without sending a prompt", + async (command) => { + const session = sessionYielding([]); + const clear = vi + .spyOn(session, "clear") + .mockResolvedValue({ sessionId: "session_1", status: "accepted" }); + const stream = vi.spyOn(session, "stream").mockReturnValue( + (async function* () { + yield stampTestEvent( + { + data: { continuationToken: "eve:test", wait: "next-user-message" }, + type: "session.waiting", + }, + 0, + ); + })(), + ); + const results: string[] = []; + const prompts: Array = [command, undefined]; + const runner = new EveTUIRunner({ + name: "Weather Agent", + renderer: fakeRenderer({ + readPrompt: vi.fn(async () => prompts.shift()), + renderCommandResult: (message) => results.push(message), + renderStream: vi.fn(async (result) => { + for await (const event of result.events) { + void event; + } + }), }), - }), - session, - }); + session, + }); - await runner.run(); + await runner.run(); - expect(clear).toHaveBeenCalledOnce(); - expect(stream).toHaveBeenCalledOnce(); - expect(session.send).not.toHaveBeenCalled(); - expect(results).toEqual(["Context clear requested."]); - }); + expect(clear).toHaveBeenCalledOnce(); + expect(stream).toHaveBeenCalledOnce(); + expect(session.send).not.toHaveBeenCalled(); + expect(results).toEqual(["Context clear requested."]); + }, + ); it("cancels an active turn without sending a prompt", async () => { const session = stubClient().session({ @@ -1876,6 +1879,7 @@ describe("EveTUIRunner replay guards", () => { describe("parsePromptCommand", () => { it.each([ ["/reset", { type: "reset" }], + ["/new", { type: "clear" }], ["/exit", { type: "exit" }], ["/quit", { type: "exit" }], ["/deploy", { type: "extension", name: "deploy", argument: "" }],