mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:40:44 +00:00
fix(control-ui): create sessions for typed /new
Route typed Control UI `/new` through the dashboard session create-and-switch flow used by the New Chat button. Keep typed `/reset` as the explicit in-place gateway reset path, and document the Control UI slash-command boundary. Fixes #69599.
This commit is contained in:
@@ -469,9 +469,72 @@ describe("handleSendChat", () => {
|
||||
expect(host.refreshSessionsAfterChat.size).toBe(0);
|
||||
});
|
||||
|
||||
it("sends button-triggered /new resets after confirmation", async () => {
|
||||
it("runs the fresh-session action for confirmed /new overrides", async () => {
|
||||
const confirm = vi.fn(() => true);
|
||||
vi.stubGlobal("confirm", confirm);
|
||||
const request = vi.fn(async (method: string) => {
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const onSlashAction = vi.fn();
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatMessage: "restore me",
|
||||
sessionKey: "agent:main",
|
||||
onSlashAction,
|
||||
});
|
||||
|
||||
await handleSendChat(host, "/new", { confirmReset: true, restoreDraft: true });
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expect(onSlashAction).toHaveBeenCalledWith("new-session");
|
||||
expect(host.chatMessage).toBe("restore me");
|
||||
expect(host.refreshSessionsAfterChat.size).toBe(0);
|
||||
});
|
||||
|
||||
it("routes typed /new through the fresh-session action without confirmation", async () => {
|
||||
const confirm = vi.fn(() => false);
|
||||
vi.stubGlobal("confirm", confirm);
|
||||
const request = vi.fn(async (method: string) => {
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const onSlashAction = vi.fn();
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatMessage: "/new",
|
||||
sessionKey: "agent:main",
|
||||
onSlashAction,
|
||||
});
|
||||
|
||||
await handleSendChat(host);
|
||||
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expect(onSlashAction).toHaveBeenCalledWith("new-session");
|
||||
expect(host.chatMessage).toBe("");
|
||||
});
|
||||
|
||||
it("does not queue typed /new behind an active run", async () => {
|
||||
const onSlashAction = vi.fn();
|
||||
const host = makeHost({
|
||||
chatMessage: "/new",
|
||||
chatRunId: "run-main",
|
||||
chatStream: "Working...",
|
||||
onSlashAction,
|
||||
});
|
||||
|
||||
await handleSendChat(host);
|
||||
|
||||
expect(onSlashAction).toHaveBeenCalledWith("new-session");
|
||||
expect(host.chatQueue).toEqual([]);
|
||||
expect(host.chatRunId).toBe("run-main");
|
||||
expect(host.chatStream).toBe("Working...");
|
||||
expect(host.chatMessage).toBe("");
|
||||
});
|
||||
|
||||
it("preserves typed /reset command dispatch without confirmation", async () => {
|
||||
const confirm = vi.fn(() => false);
|
||||
vi.stubGlobal("confirm", confirm);
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "chat.send") {
|
||||
return { status: "started" };
|
||||
@@ -480,57 +543,23 @@ describe("handleSendChat", () => {
|
||||
});
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatMessage: "restore me",
|
||||
chatMessage: "/reset",
|
||||
sessionKey: "agent:main",
|
||||
});
|
||||
|
||||
await handleSendChat(host, "/new", { confirmReset: true, restoreDraft: true });
|
||||
await handleSendChat(host);
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"chat.send",
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main",
|
||||
message: "/new",
|
||||
deliver: false,
|
||||
idempotencyKey: expect.any(String),
|
||||
message: "/reset",
|
||||
}),
|
||||
);
|
||||
expect(host.chatMessage).toBe("restore me");
|
||||
expect(host.refreshSessionsAfterChat).toContain(host.chatRunId);
|
||||
expect(host.chatMessage).toBe("");
|
||||
});
|
||||
|
||||
it.each(["/new", "/reset"])(
|
||||
"preserves typed %s command dispatch without confirmation",
|
||||
async (command) => {
|
||||
const confirm = vi.fn(() => false);
|
||||
vi.stubGlobal("confirm", confirm);
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "chat.send") {
|
||||
return { status: "started" };
|
||||
}
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatMessage: command,
|
||||
sessionKey: "agent:main",
|
||||
});
|
||||
|
||||
await handleSendChat(host);
|
||||
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"chat.send",
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main",
|
||||
message: command,
|
||||
}),
|
||||
);
|
||||
expect(host.chatMessage).toBe("");
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps slash-command model changes in sync with the chat header cache", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@@ -69,7 +69,7 @@ export type ChatHost = ChatInputHistoryState & {
|
||||
pendingAbort?: { runId: string; sessionKey: string } | null;
|
||||
chatSubmitGuards?: Map<string, Promise<void>>;
|
||||
/** Callback for slash-command side effects that need app-level access. */
|
||||
onSlashAction?: (action: string) => void;
|
||||
onSlashAction?: (action: string) => void | Promise<void>;
|
||||
};
|
||||
|
||||
export type ChatSendOptions = {
|
||||
@@ -527,7 +527,7 @@ export async function handleSendChat(
|
||||
}
|
||||
|
||||
function shouldQueueLocalSlashCommand(name: string): boolean {
|
||||
return !["stop", "focus", "export-session", "steer", "redirect"].includes(name);
|
||||
return !["stop", "focus", "export-session", "steer", "redirect", "new"].includes(name);
|
||||
}
|
||||
|
||||
// ── Slash Command Dispatch ──
|
||||
@@ -543,11 +543,11 @@ async function dispatchSlashCommand(
|
||||
await handleAbortChat(host);
|
||||
return;
|
||||
case "new":
|
||||
await sendChatMessageNow(host, "/new", {
|
||||
refreshSessions: true,
|
||||
previousDraft: sendOpts?.previousDraft,
|
||||
restoreDraft: sendOpts?.restoreDraft,
|
||||
});
|
||||
if (!host.onSlashAction) {
|
||||
host.lastError = "New Chat is unavailable.";
|
||||
return;
|
||||
}
|
||||
await host.onSlashAction("new-session");
|
||||
return;
|
||||
case "reset":
|
||||
await sendChatMessageNow(host, "/reset", {
|
||||
@@ -560,10 +560,10 @@ async function dispatchSlashCommand(
|
||||
await clearChatHistory(host);
|
||||
return;
|
||||
case "focus":
|
||||
host.onSlashAction?.("toggle-focus");
|
||||
await host.onSlashAction?.("toggle-focus");
|
||||
return;
|
||||
case "export-session":
|
||||
host.onSlashAction?.("export");
|
||||
await host.onSlashAction?.("export");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -596,7 +596,7 @@ async function dispatchSlashCommand(
|
||||
...host.chatModelOverrides,
|
||||
[targetSessionKey]: result.sessionPatch.modelOverride ?? null,
|
||||
};
|
||||
host.onSlashAction?.("refresh-tools-effective");
|
||||
await host.onSlashAction?.("refresh-tools-effective");
|
||||
}
|
||||
|
||||
if (result.action === "refresh") {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
handleFirstUpdated,
|
||||
handleUpdated,
|
||||
} from "./app-lifecycle.ts";
|
||||
import { createChatSession as createChatSessionInternal } from "./app-render.helpers.ts";
|
||||
import { renderApp } from "./app-render.ts";
|
||||
import {
|
||||
exportLogs as exportLogsInternal,
|
||||
@@ -225,7 +226,7 @@ export class OpenClawApp extends LitElement {
|
||||
private chatMobileControlsTrigger: HTMLElement | null = null;
|
||||
@state() navDrawerOpen = false;
|
||||
|
||||
onSlashAction?: (action: string) => void;
|
||||
onSlashAction?: (action: string) => void | Promise<void>;
|
||||
chatLocalInputHistoryBySession: Record<string, Array<{ text: string; ts: number }>> = {};
|
||||
chatInputHistorySessionKey: string | null = null;
|
||||
chatInputHistoryItems: string[] | null = null;
|
||||
@@ -605,8 +606,11 @@ export class OpenClawApp extends LitElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.onSlashAction = (action: string) => {
|
||||
this.onSlashAction = async (action: string) => {
|
||||
switch (action) {
|
||||
case "new-session":
|
||||
await createChatSessionInternal(this as unknown as AppViewState);
|
||||
break;
|
||||
case "toggle-focus":
|
||||
this.applySettings({
|
||||
...this.settings,
|
||||
@@ -617,7 +621,7 @@ export class OpenClawApp extends LitElement {
|
||||
exportChatMarkdown(this.chatMessages, this.assistantName);
|
||||
break;
|
||||
case "refresh-tools-effective": {
|
||||
void refreshVisibleToolsEffectiveForCurrentSessionInternal(this);
|
||||
await refreshVisibleToolsEffectiveForCurrentSessionInternal(this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user