mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(ui): surface slash command dispatch failures
## Summary - surface inline Control UI feedback when local slash-command dispatch is unavailable or throws - cover missing-client and unexpected-error paths for local slash commands - note the user-facing fix in the changelog Fixes #52105.
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Control UI/chat: show inline feedback when local slash-command dispatch is unavailable or fails unexpectedly instead of clearing the composer silently. Fixes #52105. Thanks @MooreQiao.
|
||||
- Agents/tools: return critical tool-loop circuit-breaker stops as blocked tool results instead of thrown tool failures, so models see the guardrail and stop retrying the same call. Thanks @rayraiser.
|
||||
- Model commands: clarify direct and inline `/model` acknowledgements for non-default selections as session-scoped. Thanks @addu2612.
|
||||
- TUI/chat: skip full provider model normalization during context-window warmup while preserving provider-owned context metadata, avoiding cold-start stalls with large model registries. Thanks @547895019.
|
||||
|
||||
@@ -8,9 +8,13 @@ import {
|
||||
registerChatAttachmentPayload,
|
||||
resetChatAttachmentPayloadStoreForTest,
|
||||
} from "./chat/attachment-payload-store.ts";
|
||||
import type { executeSlashCommand } from "./chat/slash-command-executor.ts";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "./types.ts";
|
||||
|
||||
const { setLastActiveSessionKeyMock } = vi.hoisted(() => ({
|
||||
type ExecuteSlashCommand = typeof executeSlashCommand;
|
||||
|
||||
const { executeSlashCommandMock, setLastActiveSessionKeyMock } = vi.hoisted(() => ({
|
||||
executeSlashCommandMock: vi.fn(),
|
||||
setLastActiveSessionKeyMock: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -18,6 +22,21 @@ vi.mock("./app-last-active-session.ts", () => ({
|
||||
setLastActiveSessionKey: (...args: unknown[]) => setLastActiveSessionKeyMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./chat/slash-command-executor.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./chat/slash-command-executor.ts")>();
|
||||
return {
|
||||
...actual,
|
||||
executeSlashCommand: (...args: Parameters<ExecuteSlashCommand>) => {
|
||||
const implementation = executeSlashCommandMock.getMockImplementation() as
|
||||
| ExecuteSlashCommand
|
||||
| undefined;
|
||||
return implementation
|
||||
? executeSlashCommandMock(...args)
|
||||
: actual.executeSlashCommand(...args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
let handleSendChat: typeof import("./app-chat.ts").handleSendChat;
|
||||
let steerQueuedChatMessage: typeof import("./app-chat.ts").steerQueuedChatMessage;
|
||||
let navigateChatInputHistory: typeof import("./app-chat.ts").navigateChatInputHistory;
|
||||
@@ -420,6 +439,7 @@ describe("handleSendChat", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
executeSlashCommandMock.mockReset();
|
||||
setLastActiveSessionKeyMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -619,6 +639,48 @@ describe("handleSendChat", () => {
|
||||
expect(onSlashAction).toHaveBeenCalledWith("refresh-tools-effective");
|
||||
});
|
||||
|
||||
it("shows local slash-command feedback when the gateway client is unavailable", async () => {
|
||||
const host = makeHost({
|
||||
client: null,
|
||||
chatMessage: "/think",
|
||||
connected: true,
|
||||
});
|
||||
|
||||
await handleSendChat(host);
|
||||
|
||||
expect(host.chatMessage).toBe("");
|
||||
expect(host.chatMessages).toEqual([
|
||||
expect.objectContaining({
|
||||
role: "system",
|
||||
content: "Cannot run `/think`: Control UI is not connected to the Gateway.",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows local slash-command feedback when dispatch fails unexpectedly", async () => {
|
||||
executeSlashCommandMock.mockRejectedValue(new Error("dispatch failed"));
|
||||
const request = vi.fn(async (method: string) => {
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const host = makeHost({
|
||||
client: { request } as unknown as ChatHost["client"],
|
||||
chatMessage: "/think",
|
||||
connected: true,
|
||||
});
|
||||
|
||||
await handleSendChat(host);
|
||||
|
||||
expect(executeSlashCommandMock).toHaveBeenCalledTimes(1);
|
||||
expect(host.chatMessage).toBe("");
|
||||
expect(host.lastError).toBe("Error: dispatch failed");
|
||||
expect(host.chatMessages).toEqual([
|
||||
expect.objectContaining({
|
||||
role: "system",
|
||||
content: "Command `/think` failed unexpectedly.",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends /btw immediately while a main run is active without queueing it", async () => {
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "chat.send") {
|
||||
|
||||
@@ -567,15 +567,29 @@ async function dispatchSlashCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!host.client) {
|
||||
if (!host.client || !host.connected) {
|
||||
host.lastError = "Gateway not connected";
|
||||
injectCommandResult(
|
||||
host,
|
||||
`Cannot run \`/${name}\`: Control UI is not connected to the Gateway.`,
|
||||
);
|
||||
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetSessionKey = host.sessionKey;
|
||||
const result = await executeSlashCommand(host.client, targetSessionKey, name, args, {
|
||||
chatModelCatalog: host.chatModelCatalog,
|
||||
sessionsResult: host.sessionsResult,
|
||||
});
|
||||
let result: Awaited<ReturnType<typeof executeSlashCommand>>;
|
||||
try {
|
||||
result = await executeSlashCommand(host.client, targetSessionKey, name, args, {
|
||||
chatModelCatalog: host.chatModelCatalog,
|
||||
sessionsResult: host.sessionsResult,
|
||||
});
|
||||
} catch (err) {
|
||||
host.lastError = String(err);
|
||||
injectCommandResult(host, `Command \`/${name}\` failed unexpectedly.`);
|
||||
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.content) {
|
||||
injectCommandResult(host, result.content);
|
||||
|
||||
Reference in New Issue
Block a user