Add TUI context mode selector (#71760)

Co-authored-by: kevinlin-openai <kevin@dendron.so>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
kevinlin-openai
2026-04-25 14:16:03 -07:00
committed by GitHub
parent ea4da7dfcc
commit 289ed9830a
2 changed files with 105 additions and 9 deletions

View File

@@ -3,9 +3,16 @@ import { createCommandHandlers } from "./tui-command-handlers.js";
type LoadHistoryMock = ReturnType<typeof vi.fn> & (() => Promise<void>);
type RunAuthFlow = NonNullable<Parameters<typeof createCommandHandlers>[0]["runAuthFlow"]>;
type SelectableOverlay = {
onSelect?: (item: { value: string; label?: string; description?: string }) => void;
};
type SetActivityStatusMock = ReturnType<typeof vi.fn> & ((text: string) => void);
type SetSessionMock = ReturnType<typeof vi.fn> & ((key: string) => Promise<void>);
async function flushAsyncSelect() {
await new Promise<void>((resolve) => setImmediate(resolve));
}
function createHarness(params?: {
sendChat?: ReturnType<typeof vi.fn>;
getGatewayStatus?: ReturnType<typeof vi.fn>;
@@ -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();

View File

@@ -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",