mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
fix(ui): confirm button-triggered new session resets (#73361)
This commit is contained in:
@@ -427,6 +427,110 @@ describe("handleSendChat", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("cancels button-triggered /new resets when confirmation is declined", async () => {
|
||||
const confirm = vi.fn(() => false);
|
||||
vi.stubGlobal("confirm", confirm);
|
||||
const request = vi.fn(async (method: string) => {
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatMessage: "keep this draft",
|
||||
sessionKey: "agent:main",
|
||||
});
|
||||
|
||||
await handleSendChat(host, "/new", { confirmReset: true, restoreDraft: true });
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith("Start a new session? This will reset the current chat.");
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expect(host.chatMessage).toBe("keep this draft");
|
||||
expect(host.chatMessages).toEqual([]);
|
||||
expect(host.chatRunId).toBeNull();
|
||||
expect(host.refreshSessionsAfterChat.size).toBe(0);
|
||||
});
|
||||
|
||||
it("cancels button-triggered /new resets when confirmation is unavailable", async () => {
|
||||
vi.stubGlobal("confirm", undefined);
|
||||
const request = vi.fn(async (method: string) => {
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatMessage: "keep this draft",
|
||||
sessionKey: "agent:main",
|
||||
});
|
||||
|
||||
await handleSendChat(host, "/new", { confirmReset: true, restoreDraft: true });
|
||||
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expect(host.chatMessage).toBe("keep this draft");
|
||||
expect(host.chatMessages).toEqual([]);
|
||||
expect(host.chatRunId).toBeNull();
|
||||
expect(host.refreshSessionsAfterChat.size).toBe(0);
|
||||
});
|
||||
|
||||
it("sends button-triggered /new resets after confirmation", async () => {
|
||||
const confirm = vi.fn(() => true);
|
||||
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: "restore me",
|
||||
sessionKey: "agent:main",
|
||||
});
|
||||
|
||||
await handleSendChat(host, "/new", { confirmReset: true, restoreDraft: true });
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"chat.send",
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main",
|
||||
message: "/new",
|
||||
deliver: false,
|
||||
idempotencyKey: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(host.chatMessage).toBe("restore me");
|
||||
expect(host.refreshSessionsAfterChat).toContain(host.chatRunId);
|
||||
});
|
||||
|
||||
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",
|
||||
|
||||
@@ -72,6 +72,11 @@ export type ChatHost = ChatInputHistoryState & {
|
||||
onSlashAction?: (action: string) => void;
|
||||
};
|
||||
|
||||
export type ChatSendOptions = {
|
||||
confirmReset?: boolean;
|
||||
restoreDraft?: boolean;
|
||||
};
|
||||
|
||||
export const CHAT_SESSIONS_ACTIVE_MINUTES = 120;
|
||||
export {
|
||||
handleChatDraftChange,
|
||||
@@ -115,6 +120,16 @@ function isChatResetCommand(text: string) {
|
||||
return normalized.startsWith("/new ") || normalized.startsWith("/reset ");
|
||||
}
|
||||
|
||||
function confirmChatResetCommand(text: string) {
|
||||
if (!isChatResetCommand(text)) {
|
||||
return true;
|
||||
}
|
||||
if (typeof globalThis.confirm !== "function") {
|
||||
return false;
|
||||
}
|
||||
return globalThis.confirm("Start a new session? This will reset the current chat.");
|
||||
}
|
||||
|
||||
function isBtwCommand(text: string) {
|
||||
return /^\/btw(?::|\s|$)/i.test(text.trim());
|
||||
}
|
||||
@@ -408,7 +423,7 @@ export function clearPendingQueueItemsForRun(host: ChatHost, runId: string | und
|
||||
export async function handleSendChat(
|
||||
host: ChatHost,
|
||||
messageOverride?: string,
|
||||
opts?: { restoreDraft?: boolean },
|
||||
opts?: ChatSendOptions,
|
||||
) {
|
||||
if (!host.connected) {
|
||||
return;
|
||||
@@ -423,6 +438,10 @@ export async function handleSendChat(
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageOverride != null && opts?.confirmReset && !confirmChatResetCommand(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isChatStopCommand(message)) {
|
||||
if (messageOverride == null) {
|
||||
recordNonTranscriptInputHistory(host, message);
|
||||
|
||||
@@ -2357,7 +2357,8 @@ export function renderApp(state: AppViewState) {
|
||||
onDismissSideResult: () => {
|
||||
state.chatSideResult = null;
|
||||
},
|
||||
onNewSession: () => state.handleSendChat("/new", { restoreDraft: true }),
|
||||
onNewSession: () =>
|
||||
state.handleSendChat("/new", { confirmReset: true, restoreDraft: true }),
|
||||
onClearHistory: async () => {
|
||||
if (!state.client || !state.connected) {
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { EventLogEntry } from "./app-events.ts";
|
||||
import type { ChatSendOptions } from "./app-chat.ts";
|
||||
import type { CompactionStatus, FallbackStatus } from "./app-tool-stream.ts";
|
||||
import type { ChatInputHistoryKeyInput, ChatInputHistoryKeyResult } from "./chat/input-history.ts";
|
||||
import type { RealtimeTalkStatus } from "./chat/realtime-talk.ts";
|
||||
@@ -460,7 +461,7 @@ export type AppViewState = {
|
||||
handleChatDraftChange: (next: string) => void;
|
||||
handleChatInputHistoryKey: (input: ChatInputHistoryKeyInput) => ChatInputHistoryKeyResult;
|
||||
resetChatInputHistoryNavigation: () => void;
|
||||
handleSendChat: (messageOverride?: string, opts?: { restoreDraft?: boolean }) => Promise<void>;
|
||||
handleSendChat: (messageOverride?: string, opts?: ChatSendOptions) => Promise<void>;
|
||||
toggleRealtimeTalk: () => Promise<void>;
|
||||
steerQueuedChatMessage: (id: string) => Promise<void>;
|
||||
handleAbortChat: () => Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user