mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(ui): confirm button-triggered new session resets (#73361)
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Anthropic: cancel stalled Anthropic Messages SSE body reads when abort signals fire, so active-memory timeouts release transport resources instead of leaving hidden recall runs parked on `reader.read()`. Refs #72965 and #73120. Thanks @wdeveloper16.
|
||||
- Control UI/WebChat: keep pending run and typing state attached to the active client run, so unowned inject/announce/side-result finals no longer unlock unrelated active runs while completed owned runs still clear promptly. Fixes #57795; carries forward the narrow diagnosis from #57887. Thanks @haoyu-haoyu.
|
||||
- Sandbox/Docker: stop satisfying a missing default sandbox image by tagging plain Debian as `openclaw-sandbox:bookworm-slim`, preserving the Python tooling required by sandbox write/edit helpers and directing users to build the default image. Fixes #51185; refs #45108, #51099, #51609, and #57713. Thanks @dpalis, @Tin55FoilDev, @jbcohen2-coder, @macminihal-cyber, and @PraxoOnline.
|
||||
- Control UI/WebChat: confirm toolbar New Session button resets before dispatching `/new` while leaving typed `/new` and `/reset` commands immediate. Fixes #45800; refs #27065, #56611, #54499, and #27110. Thanks @aethnova, @kosta228-huli, @adambezemek, and @xss925175263 (xianshishan).
|
||||
- Agents/models: keep per-agent primary models strict when `fallbacks` is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.
|
||||
- Gateway/models: add `models.pricing.enabled` so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.
|
||||
- Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale `OPENCLAW_GATEWAY_TOKEN` or `OPENCLAW_GATEWAY_PASSWORD` values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.
|
||||
|
||||
@@ -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