fix(webchat): set command flag on slash command responses (#2030)
Webchat slash commands (/status, /help, /new) were not working because command responses were missing the message.command flag. This caused the Control UI to not recognize them as command responses. - Always set message.command=true when agent wasn't started - Fix missing agentCommand import in E2E test - Add webchat commands unit tests
This commit is contained in:
parent
bc7ba73a34
commit
cd0aea0f8c
@ -506,12 +506,13 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!agentRunStarted) {
|
if (!agentRunStarted) {
|
||||||
|
// No agent was started, meaning this was handled as a command
|
||||||
const combinedReply = finalReplyParts
|
const combinedReply = finalReplyParts
|
||||||
.map((part) => part.trim())
|
.map((part) => part.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
.trim();
|
.trim();
|
||||||
let message: Record<string, unknown> | undefined;
|
let message: Record<string, unknown> = { command: true };
|
||||||
if (combinedReply) {
|
if (combinedReply) {
|
||||||
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(
|
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(
|
||||||
p.sessionKey,
|
p.sessionKey,
|
||||||
@ -525,7 +526,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
createIfMissing: true,
|
createIfMissing: true,
|
||||||
});
|
});
|
||||||
if (appended.ok) {
|
if (appended.ok) {
|
||||||
message = appended.message;
|
message = { ...appended.message, command: true };
|
||||||
} else {
|
} else {
|
||||||
context.logGateway.warn(
|
context.logGateway.warn(
|
||||||
`webchat transcript append failed: ${appended.error ?? "unknown error"}`,
|
`webchat transcript append failed: ${appended.error ?? "unknown error"}`,
|
||||||
@ -537,6 +538,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
timestamp: now,
|
timestamp: now,
|
||||||
stopReason: "injected",
|
stopReason: "injected",
|
||||||
usage: { input: 0, output: 0, totalTokens: 0 },
|
usage: { input: 0, output: 0, totalTokens: 0 },
|
||||||
|
command: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { WebSocket } from "ws";
|
|||||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||||
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
||||||
import {
|
import {
|
||||||
|
agentCommand,
|
||||||
connectOk,
|
connectOk,
|
||||||
getReplyFromConfig,
|
getReplyFromConfig,
|
||||||
installGatewayTestHooks,
|
installGatewayTestHooks,
|
||||||
|
|||||||
123
src/gateway/server.chat.webchat-commands.test.ts
Normal file
123
src/gateway/server.chat.webchat-commands.test.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import type { MsgContext } from "../auto-reply/templating.js";
|
||||||
|
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
||||||
|
import { buildCommandContext, handleCommands } from "../auto-reply/reply/commands.js";
|
||||||
|
import { parseInlineDirectives } from "../auto-reply/reply/directive-handling.js";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
|
||||||
|
// Test that webchat commands work with CommandAuthorized: true
|
||||||
|
describe("webchat slash commands", () => {
|
||||||
|
const workspaceDir = "/tmp/clawdbot-test";
|
||||||
|
|
||||||
|
function buildWebchatParams(commandBody: string, cfg: ClawdbotConfig) {
|
||||||
|
const ctx = {
|
||||||
|
Body: commandBody,
|
||||||
|
BodyForAgent: commandBody,
|
||||||
|
BodyForCommands: commandBody,
|
||||||
|
RawBody: commandBody,
|
||||||
|
CommandBody: commandBody,
|
||||||
|
SessionKey: "agent:main:webchat:test",
|
||||||
|
Provider: INTERNAL_MESSAGE_CHANNEL,
|
||||||
|
Surface: INTERNAL_MESSAGE_CHANNEL,
|
||||||
|
OriginatingChannel: INTERNAL_MESSAGE_CHANNEL,
|
||||||
|
ChatType: "direct",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
CommandSource: undefined,
|
||||||
|
} as MsgContext;
|
||||||
|
|
||||||
|
const command = buildCommandContext({
|
||||||
|
ctx,
|
||||||
|
cfg,
|
||||||
|
isGroup: false,
|
||||||
|
triggerBodyNormalized: commandBody.trim(),
|
||||||
|
commandAuthorized: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ctx,
|
||||||
|
cfg,
|
||||||
|
command,
|
||||||
|
directives: parseInlineDirectives(commandBody),
|
||||||
|
elevated: { enabled: true, allowed: true, failures: [] },
|
||||||
|
sessionKey: "agent:main:webchat:test",
|
||||||
|
workspaceDir,
|
||||||
|
defaultGroupActivation: () => "mention" as const,
|
||||||
|
resolvedVerboseLevel: "off" as const,
|
||||||
|
resolvedReasoningLevel: "off" as const,
|
||||||
|
resolveDefaultThinkingLevel: async () => undefined,
|
||||||
|
provider: "webchat",
|
||||||
|
model: "test-model",
|
||||||
|
contextTokens: 0,
|
||||||
|
isGroup: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/status returns a reply and does not continue to agent", async () => {
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const params = buildWebchatParams("/status", cfg);
|
||||||
|
|
||||||
|
console.log("Command context:", {
|
||||||
|
commandBodyNormalized: params.command.commandBodyNormalized,
|
||||||
|
isAuthorizedSender: params.command.isAuthorizedSender,
|
||||||
|
surface: params.command.surface,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handleCommands(params);
|
||||||
|
|
||||||
|
console.log("Result:", {
|
||||||
|
shouldContinue: result.shouldContinue,
|
||||||
|
hasReply: Boolean(result.reply),
|
||||||
|
replyPreview: result.reply?.text?.slice(0, 100),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.shouldContinue).toBe(false);
|
||||||
|
expect(result.reply).toBeDefined();
|
||||||
|
expect(result.reply?.text).toContain("Clawdbot");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/help returns a reply and does not continue to agent", async () => {
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const params = buildWebchatParams("/help", cfg);
|
||||||
|
|
||||||
|
const result = await handleCommands(params);
|
||||||
|
|
||||||
|
expect(result.shouldContinue).toBe(false);
|
||||||
|
expect(result.reply).toBeDefined();
|
||||||
|
expect(result.reply?.text).toContain("Help");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/new continues to agent (session reset)", async () => {
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const params = buildWebchatParams("/new", cfg);
|
||||||
|
|
||||||
|
const result = await handleCommands(params);
|
||||||
|
|
||||||
|
// /new triggers session reset but continues to agent for greeting
|
||||||
|
expect(result.shouldContinue).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("commands work with commands.text: false (webchat is not native)", async () => {
|
||||||
|
const cfg = { commands: { text: false } } as ClawdbotConfig;
|
||||||
|
const params = buildWebchatParams("/status", cfg);
|
||||||
|
|
||||||
|
const result = await handleCommands(params);
|
||||||
|
|
||||||
|
// Even with commands.text: false, webchat should still handle commands
|
||||||
|
// because webchat doesn't have native command support
|
||||||
|
expect(result.shouldContinue).toBe(false);
|
||||||
|
expect(result.reply).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verifies isAuthorizedSender is true for webchat", async () => {
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const params = buildWebchatParams("/status", cfg);
|
||||||
|
|
||||||
|
expect(params.command.isAuthorizedSender).toBe(true);
|
||||||
|
expect(params.command.surface).toBe(INTERNAL_MESSAGE_CHANNEL);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user