diff --git a/src/tui/tui-command-handlers.test.ts b/src/tui/tui-command-handlers.test.ts index 612659afa37..587fc8ab1a4 100644 --- a/src/tui/tui-command-handlers.test.ts +++ b/src/tui/tui-command-handlers.test.ts @@ -3,9 +3,16 @@ import { createCommandHandlers } from "./tui-command-handlers.js"; type LoadHistoryMock = ReturnType & (() => Promise); type RunAuthFlow = NonNullable[0]["runAuthFlow"]>; +type SelectableOverlay = { + onSelect?: (item: { value: string; label?: string; description?: string }) => void; +}; type SetActivityStatusMock = ReturnType & ((text: string) => void); type SetSessionMock = ReturnType & ((key: string) => Promise); +async function flushAsyncSelect() { + await new Promise((resolve) => setImmediate(resolve)); +} + function createHarness(params?: { sendChat?: ReturnType; getGatewayStatus?: ReturnType; @@ -37,6 +44,8 @@ function createHarness(params?: { const refreshSessionInfo = params?.refreshSessionInfo ?? vi.fn().mockResolvedValue(undefined); const applySessionInfoFromPatch = params?.applySessionInfoFromPatch ?? vi.fn(); const setActivityStatus = params?.setActivityStatus ?? (vi.fn() as SetActivityStatusMock); + const openOverlay = vi.fn(); + const closeOverlay = vi.fn(); const requestExit = vi.fn(); const runAuthFlow: RunAuthFlow | undefined = params?.runAuthFlow ?? @@ -59,8 +68,8 @@ function createHarness(params?: { opts: params?.opts ?? {}, state: state as never, deliverDefault: false, - openOverlay: vi.fn(), - closeOverlay: vi.fn(), + openOverlay, + closeOverlay, refreshSessionInfo: refreshSessionInfo as never, loadHistory, setSession, @@ -81,6 +90,8 @@ function createHarness(params?: { handleCommand, getGatewayStatus, sendChat, + openOverlay, + closeOverlay, patchSession, resetSession, setSession, @@ -115,7 +126,7 @@ describe("tui command handlers", () => { setActivityStatus, }); - const pending = handleCommand("/context"); + const pending = handleCommand("/context detail"); await Promise.resolve(); expect(setActivityStatus).toHaveBeenCalledWith("sending"); @@ -131,19 +142,73 @@ describe("tui command handlers", () => { it("forwards unknown slash commands to the gateway", async () => { const { handleCommand, sendChat, addUser, addSystem, requestRender } = createHarness(); - await handleCommand("/context"); + await handleCommand("/unregistered-command"); expect(addSystem).not.toHaveBeenCalled(); - expect(addUser).toHaveBeenCalledWith("/context"); + expect(addUser).toHaveBeenCalledWith("/unregistered-command"); expect(sendChat).toHaveBeenCalledWith( expect.objectContaining({ sessionKey: "agent:main:main", - message: "/context", + message: "/unregistered-command", }), ); expect(requestRender).toHaveBeenCalled(); }); + it("opens a context mode selector for /context without sending immediately", async () => { + const { handleCommand, sendChat, openOverlay } = createHarness(); + + await handleCommand("/context"); + + expect(sendChat).not.toHaveBeenCalled(); + expect(openOverlay).toHaveBeenCalledTimes(1); + }); + + it("sends the selected context mode through the gateway command path", async () => { + const { handleCommand, sendChat, openOverlay, closeOverlay } = createHarness(); + + await handleCommand("/context"); + const selector = openOverlay.mock.calls[0]?.[0] as SelectableOverlay | undefined; + selector?.onSelect?.({ value: "detail", label: "detail" }); + await flushAsyncSelect(); + + expect(sendChat).toHaveBeenCalledWith( + expect.objectContaining({ + sessionKey: "agent:main:main", + message: "/context detail", + }), + ); + expect(closeOverlay).toHaveBeenCalledTimes(1); + }); + + it("forwards /context list directly", async () => { + const { handleCommand, sendChat, openOverlay } = createHarness(); + + await handleCommand("/context list"); + + expect(openOverlay).not.toHaveBeenCalled(); + expect(sendChat).toHaveBeenCalledWith( + expect.objectContaining({ + sessionKey: "agent:main:main", + message: "/context list", + }), + ); + }); + + it("forwards /context help directly", async () => { + const { handleCommand, sendChat, openOverlay } = createHarness(); + + await handleCommand("/context help"); + + expect(openOverlay).not.toHaveBeenCalled(); + expect(sendChat).toHaveBeenCalledWith( + expect.objectContaining({ + sessionKey: "agent:main:main", + message: "/context help", + }), + ); + }); + it("forwards /status to the shared gateway command path", async () => { const { handleCommand, sendChat, addUser, addSystem } = createHarness(); @@ -202,7 +267,7 @@ describe("tui command handlers", () => { it("defers local run binding until gateway events provide a real run id", async () => { const { handleCommand, noteLocalRunId, state } = createHarness(); - await handleCommand("/context"); + await handleCommand("/context detail"); expect(noteLocalRunId).not.toHaveBeenCalled(); expect(state.activeChatRunId).toBeNull(); @@ -261,7 +326,7 @@ describe("tui command handlers", () => { setActivityStatus, }); - await handleCommand("/context"); + await handleCommand("/context detail"); expect(addSystem).toHaveBeenCalledWith("send failed: Error: gateway down"); expect(setActivityStatus).toHaveBeenLastCalledWith("error"); @@ -288,7 +353,7 @@ describe("tui command handlers", () => { isConnected: false, }); - await handleCommand("/context"); + await handleCommand("/context detail"); expect(sendChat).not.toHaveBeenCalled(); expect(addUser).not.toHaveBeenCalled(); diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index 01460743748..7c657282617 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -163,6 +163,30 @@ export function createCommandHandlers(context: CommandHandlerContext) { }); }; + const openContextModeSelector = () => { + const items = [ + { + value: "list", + label: "list", + description: "Short context breakdown", + }, + { + value: "detail", + label: "detail", + description: "Per-file, per-tool, per-skill, and system prompt size", + }, + { + value: "json", + label: "json", + description: "Machine-readable context report", + }, + ]; + const selector = createSearchableSelectList(items, 9); + openSelector(selector, async (value) => { + await sendMessage(`/context ${value}`); + }); + }; + const openSessionSelector = async () => { try { const result = await client.listSessions({ @@ -331,6 +355,13 @@ export function createCommandHandlers(context: CommandHandlerContext) { case "agents": await openAgentSelector(); break; + case "context": + if (!args) { + openContextModeSelector(); + } else { + await sendMessage(raw); + } + break; case "crestodian": chatLog.addSystem( args ? `returning to Crestodian with request: ${args}` : "returning to Crestodian",