mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:40:44 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user