From 0dd4112f0d519ce311e8752f38d86a2356e17bd5 Mon Sep 17 00:00:00 2001 From: KamijoToma Date: Thu, 18 Jun 2026 22:32:28 +0800 Subject: [PATCH 1/3] feat(coding-agent): save slash command text for /btw /tan /omfg /memory /rename /move to TUI history These commands take user-provided arguments that are worth recalling with the up arrow. Panel-only commands like /settings remain unrecorded. --- packages/coding-agent/CHANGELOG.md | 4 ++++ .../coding-agent/src/slash-commands/builtin-registry.ts | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5c830974a..54f9538ee 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Changed + +- Changed `/btw`, `/tan`, `/omfg`, `/memory`, `/rename`, and `/move` to save the typed command text to TUI prompt history so they can be recalled with the up arrow. + ## [16.0.5] - 2026-06-17 diff --git a/packages/coding-agent/src/slash-commands/builtin-registry.ts b/packages/coding-agent/src/slash-commands/builtin-registry.ts index 2ce79a6cd..f722b202c 100644 --- a/packages/coding-agent/src/slash-commands/builtin-registry.ts +++ b/packages/coding-agent/src/slash-commands/builtin-registry.ts @@ -1328,6 +1328,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ allowArgs: true, handleTui: async (command, runtime) => { const question = command.text.slice(`/${command.name}`.length).trim(); + runtime.ctx.editor.addToHistory(command.text); runtime.ctx.editor.setText(""); await runtime.ctx.handleBtwCommand(question); }, @@ -1339,6 +1340,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ allowArgs: true, handleTui: async (command, runtime) => { const work = command.text.slice(`/${command.name}`.length).trim(); + runtime.ctx.editor.addToHistory(command.text); runtime.ctx.editor.setText(""); await runtime.ctx.handleTanCommand(work); }, @@ -1350,6 +1352,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ allowArgs: true, handleTui: async (command, runtime) => { const complaint = command.text.slice(`/${command.name}`.length).trim(); + runtime.ctx.editor.addToHistory(command.text); runtime.ctx.editor.setText(""); await runtime.ctx.handleOmfgCommand(complaint); }, @@ -1458,6 +1461,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ } }, handleTui: async (command, runtime) => { + runtime.ctx.editor.addToHistory(command.text); runtime.ctx.editor.setText(""); await runtime.ctx.handleMemoryCommand(command.text); }, @@ -1485,6 +1489,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ runtime.ctx.editor.setText(""); return; } + runtime.ctx.editor.addToHistory(command.text); runtime.ctx.editor.setText(""); await runtime.ctx.handleRenameCommand(title); }, @@ -1527,6 +1532,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ runtime.ctx.editor.setText(""); return; } + runtime.ctx.editor.addToHistory(command.text); runtime.ctx.editor.setText(""); await runtime.ctx.handleMoveCommand(targetPath); }, From e50ca8d21ae0dbe911c1efee8ccd9d970aa513d0 Mon Sep 17 00:00:00 2001 From: KamijoToma Date: Thu, 18 Jun 2026 22:59:59 +0800 Subject: [PATCH 2/3] Address review feedback: guard blank /btw /tan /omfg and add history regression tests - Only add /btw, /tan, /omfg to history when their argument is non-empty, so blank invocations rejected by the controller are not persisted. - Add regression coverage for addToHistory behavior on /btw, /tan, /omfg, /memory, /rename, and /move. --- .../src/slash-commands/builtin-registry.ts | 12 +++-- .../test/slash-commands/btw.test.ts | 17 ++++++- .../test/slash-commands/memory.test.ts | 43 +++++++++++++++++ .../test/slash-commands/move.test.ts | 48 +++++++++++++++++++ .../test/slash-commands/omfg.test.ts | 17 ++++++- .../test/slash-commands/rename.test.ts | 48 +++++++++++++++++++ .../test/slash-commands/tan.test.ts | 17 ++++++- 7 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 packages/coding-agent/test/slash-commands/memory.test.ts create mode 100644 packages/coding-agent/test/slash-commands/move.test.ts create mode 100644 packages/coding-agent/test/slash-commands/rename.test.ts diff --git a/packages/coding-agent/src/slash-commands/builtin-registry.ts b/packages/coding-agent/src/slash-commands/builtin-registry.ts index f722b202c..4ce50a9db 100644 --- a/packages/coding-agent/src/slash-commands/builtin-registry.ts +++ b/packages/coding-agent/src/slash-commands/builtin-registry.ts @@ -1328,7 +1328,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ allowArgs: true, handleTui: async (command, runtime) => { const question = command.text.slice(`/${command.name}`.length).trim(); - runtime.ctx.editor.addToHistory(command.text); + if (question) { + runtime.ctx.editor.addToHistory(command.text); + } runtime.ctx.editor.setText(""); await runtime.ctx.handleBtwCommand(question); }, @@ -1340,7 +1342,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ allowArgs: true, handleTui: async (command, runtime) => { const work = command.text.slice(`/${command.name}`.length).trim(); - runtime.ctx.editor.addToHistory(command.text); + if (work) { + runtime.ctx.editor.addToHistory(command.text); + } runtime.ctx.editor.setText(""); await runtime.ctx.handleTanCommand(work); }, @@ -1352,7 +1356,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray = [ allowArgs: true, handleTui: async (command, runtime) => { const complaint = command.text.slice(`/${command.name}`.length).trim(); - runtime.ctx.editor.addToHistory(command.text); + if (complaint) { + runtime.ctx.editor.addToHistory(command.text); + } runtime.ctx.editor.setText(""); await runtime.ctx.handleOmfgCommand(complaint); }, diff --git a/packages/coding-agent/test/slash-commands/btw.test.ts b/packages/coding-agent/test/slash-commands/btw.test.ts index 1349fa895..3615609b7 100644 --- a/packages/coding-agent/test/slash-commands/btw.test.ts +++ b/packages/coding-agent/test/slash-commands/btw.test.ts @@ -5,12 +5,14 @@ import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-comm function createRuntime() { const handleBtwCommand = vi.fn(async () => {}); const setText = vi.fn(); + const addToHistory = vi.fn(); return { handleBtwCommand, setText, + addToHistory, runtime: { ctx: { - editor: { setText } as unknown as InteractiveModeContext["editor"], + editor: { setText, addToHistory } as unknown as InteractiveModeContext["editor"], handleBtwCommand, } as unknown as InteractiveModeContext, }, @@ -24,6 +26,7 @@ describe("/btw slash command", () => { const handled = await executeBuiltinSlashCommand("/btw why is it doing that?", harness.runtime); expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/btw why is it doing that?"); expect(harness.setText).toHaveBeenCalledWith(""); expect(harness.handleBtwCommand).toHaveBeenCalledWith("why is it doing that?"); }); @@ -37,6 +40,18 @@ describe("/btw slash command", () => { ); expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/btw explain why the cache reuse matters here"); expect(harness.handleBtwCommand).toHaveBeenCalledWith("explain why the cache reuse matters here"); }); + + it("does not add a blank /btw invocation to history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/btw ", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).not.toHaveBeenCalled(); + expect(harness.setText).toHaveBeenCalledWith(""); + expect(harness.handleBtwCommand).toHaveBeenCalledWith(""); + }); }); diff --git a/packages/coding-agent/test/slash-commands/memory.test.ts b/packages/coding-agent/test/slash-commands/memory.test.ts new file mode 100644 index 000000000..2bb97e346 --- /dev/null +++ b/packages/coding-agent/test/slash-commands/memory.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from "bun:test"; +import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; +import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-commands/builtin-registry"; + +function createRuntime() { + const handleMemoryCommand = vi.fn(async () => {}); + const setText = vi.fn(); + const addToHistory = vi.fn(); + return { + handleMemoryCommand, + setText, + addToHistory, + runtime: { + ctx: { + editor: { setText, addToHistory } as unknown as InteractiveModeContext["editor"], + handleMemoryCommand, + } as unknown as InteractiveModeContext, + }, + }; +} + +describe("/memory slash command", () => { + it("routes the full command text through the memory handler and saves it to history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/memory view", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/memory view"); + expect(harness.setText).toHaveBeenCalledWith(""); + expect(harness.handleMemoryCommand).toHaveBeenCalledWith("/memory view"); + }); + + it("preserves the raw command text for history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/memory stats", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/memory stats"); + expect(harness.handleMemoryCommand).toHaveBeenCalledWith("/memory stats"); + }); +}); diff --git a/packages/coding-agent/test/slash-commands/move.test.ts b/packages/coding-agent/test/slash-commands/move.test.ts new file mode 100644 index 000000000..05ecdbb8d --- /dev/null +++ b/packages/coding-agent/test/slash-commands/move.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from "bun:test"; +import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; +import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-commands/builtin-registry"; + +function createRuntime() { + const handleMoveCommand = vi.fn(async () => {}); + const showError = vi.fn(); + const setText = vi.fn(); + const addToHistory = vi.fn(); + return { + handleMoveCommand, + showError, + setText, + addToHistory, + runtime: { + ctx: { + editor: { setText, addToHistory } as unknown as InteractiveModeContext["editor"], + showError, + handleMoveCommand, + } as unknown as InteractiveModeContext, + }, + }; +} + +describe("/move slash command", () => { + it("routes the path through the move handler and saves the full command to history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/move /tmp/project", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/move /tmp/project"); + expect(harness.setText).toHaveBeenCalledWith(""); + expect(harness.handleMoveCommand).toHaveBeenCalledWith("/tmp/project"); + }); + + it("does not add a blank /move invocation to history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/move ", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).not.toHaveBeenCalled(); + expect(harness.showError).toHaveBeenCalledWith("Usage: /move "); + expect(harness.setText).toHaveBeenCalledWith(""); + expect(harness.handleMoveCommand).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/coding-agent/test/slash-commands/omfg.test.ts b/packages/coding-agent/test/slash-commands/omfg.test.ts index a95bc41f2..e52b8979c 100644 --- a/packages/coding-agent/test/slash-commands/omfg.test.ts +++ b/packages/coding-agent/test/slash-commands/omfg.test.ts @@ -5,12 +5,14 @@ import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-comm function createRuntime() { const handleOmfgCommand = vi.fn(async () => {}); const setText = vi.fn(); + const addToHistory = vi.fn(); return { handleOmfgCommand, setText, + addToHistory, runtime: { ctx: { - editor: { setText } as unknown as InteractiveModeContext["editor"], + editor: { setText, addToHistory } as unknown as InteractiveModeContext["editor"], handleOmfgCommand, } as unknown as InteractiveModeContext, }, @@ -24,6 +26,7 @@ describe("/omfg slash command", () => { const handled = await executeBuiltinSlashCommand("/omfg This guy used any again....", harness.runtime); expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/omfg This guy used any again...."); expect(harness.setText).toHaveBeenCalledWith(""); expect(harness.handleOmfgCommand).toHaveBeenCalledWith("This guy used any again...."); }); @@ -37,6 +40,18 @@ describe("/omfg slash command", () => { ); expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/omfg stop making unchecked casts in generated TypeScript"); expect(harness.handleOmfgCommand).toHaveBeenCalledWith("stop making unchecked casts in generated TypeScript"); }); + + it("does not add a blank /omfg invocation to history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/omfg ", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).not.toHaveBeenCalled(); + expect(harness.setText).toHaveBeenCalledWith(""); + expect(harness.handleOmfgCommand).toHaveBeenCalledWith(""); + }); }); diff --git a/packages/coding-agent/test/slash-commands/rename.test.ts b/packages/coding-agent/test/slash-commands/rename.test.ts new file mode 100644 index 000000000..aaf5b599c --- /dev/null +++ b/packages/coding-agent/test/slash-commands/rename.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from "bun:test"; +import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; +import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-commands/builtin-registry"; + +function createRuntime() { + const handleRenameCommand = vi.fn(async () => {}); + const showError = vi.fn(); + const setText = vi.fn(); + const addToHistory = vi.fn(); + return { + handleRenameCommand, + showError, + setText, + addToHistory, + runtime: { + ctx: { + editor: { setText, addToHistory } as unknown as InteractiveModeContext["editor"], + showError, + handleRenameCommand, + } as unknown as InteractiveModeContext, + }, + }; +} + +describe("/rename slash command", () => { + it("routes the title through the rename handler and saves the full command to history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/rename my session", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/rename my session"); + expect(harness.setText).toHaveBeenCalledWith(""); + expect(harness.handleRenameCommand).toHaveBeenCalledWith("my session"); + }); + + it("does not add a blank /rename invocation to history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/rename ", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).not.toHaveBeenCalled(); + expect(harness.showError).toHaveBeenCalledWith("Usage: /rename "); + expect(harness.setText).toHaveBeenCalledWith(""); + expect(harness.handleRenameCommand).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/coding-agent/test/slash-commands/tan.test.ts b/packages/coding-agent/test/slash-commands/tan.test.ts index 348978db4..b8fde0d6d 100644 --- a/packages/coding-agent/test/slash-commands/tan.test.ts +++ b/packages/coding-agent/test/slash-commands/tan.test.ts @@ -5,12 +5,14 @@ import { executeBuiltinSlashCommand } from "@oh-my-pi/pi-coding-agent/slash-comm function createRuntime() { const handleTanCommand = vi.fn(async () => {}); const setText = vi.fn(); + const addToHistory = vi.fn(); return { handleTanCommand, setText, + addToHistory, runtime: { ctx: { - editor: { setText } as unknown as InteractiveModeContext["editor"], + editor: { setText, addToHistory } as unknown as InteractiveModeContext["editor"], handleTanCommand, } as unknown as InteractiveModeContext, }, @@ -24,6 +26,7 @@ describe("/tan slash command", () => { const handled = await executeBuiltinSlashCommand("/tan add a changelog note", harness.runtime); expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/tan add a changelog note"); expect(harness.setText).toHaveBeenCalledWith(""); expect(harness.handleTanCommand).toHaveBeenCalledWith("add a changelog note"); }); @@ -37,6 +40,18 @@ describe("/tan slash command", () => { ); expect(handled).toBe(true); + expect(harness.addToHistory).toHaveBeenCalledWith("/tan investigate why prompt cache reuse matters here"); expect(harness.handleTanCommand).toHaveBeenCalledWith("investigate why prompt cache reuse matters here"); }); + + it("does not add a blank /tan invocation to history", async () => { + const harness = createRuntime(); + + const handled = await executeBuiltinSlashCommand("/tan ", harness.runtime); + + expect(handled).toBe(true); + expect(harness.addToHistory).not.toHaveBeenCalled(); + expect(harness.setText).toHaveBeenCalledWith(""); + expect(harness.handleTanCommand).toHaveBeenCalledWith(""); + }); }); From 5a54c2234643666c5ee3c92225208d33cb9326b2 Mon Sep 17 00:00:00 2001 From: KamijoToma <admin@misakacloud.net> Date: Thu, 18 Jun 2026 23:31:08 +0800 Subject: [PATCH 3/3] fix(coding-agent): update input-controller escape test for /btw history The test previously asserted that /btw during streaming did not add to history. After the intentional change to save /btw commands, update the assertion to expect the command text in history. --- packages/coding-agent/test/input-controller-escape.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/test/input-controller-escape.test.ts b/packages/coding-agent/test/input-controller-escape.test.ts index e53c6eda2..5755609ea 100644 --- a/packages/coding-agent/test/input-controller-escape.test.ts +++ b/packages/coding-agent/test/input-controller-escape.test.ts @@ -307,7 +307,7 @@ describe("InputController escape behavior", () => { expect(spies.handleBtwCommand).toHaveBeenCalledWith("why is it doing that?"); expect(spies.prompt).not.toHaveBeenCalled(); - expect(editor.addToHistory).not.toHaveBeenCalled(); + expect(editor.addToHistory).toHaveBeenCalledWith("/btw why is it doing that?"); expect(editor.getText()).toBe(""); });