feat: add ack reaction defaults

This commit is contained in:
Peter Steinberger
2026-01-06 03:28:35 +00:00
parent 58186aa56e
commit 1a4f7d3388
16 changed files with 318 additions and 25 deletions

View File

@@ -87,6 +87,57 @@ describe("config identity defaults", () => {
});
});
it("defaults ackReaction to identity emoji", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".clawdbot");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "clawdbot.json"),
JSON.stringify(
{
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" },
messages: {},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
expect(cfg.messages?.ackReaction).toBe("🦥");
expect(cfg.messages?.ackReactionScope).toBe("group-mentions");
});
});
it("defaults ackReaction to 👀 when identity is missing", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".clawdbot");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "clawdbot.json"),
JSON.stringify(
{
messages: {},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
expect(cfg.messages?.ackReaction).toBe("👀");
expect(cfg.messages?.ackReactionScope).toBe("group-mentions");
});
});
it("does not override explicit values", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".clawdbot");

View File

@@ -54,6 +54,32 @@ export function applyIdentityDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
return mutated ? next : cfg;
}
export function applyMessageDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
const messages = cfg.messages;
const hasAckReaction = messages?.ackReaction !== undefined;
const hasAckScope = messages?.ackReactionScope !== undefined;
if (hasAckReaction && hasAckScope) return cfg;
const fallbackEmoji = cfg.identity?.emoji?.trim() || "👀";
const nextMessages = { ...(messages ?? {}) };
let mutated = false;
if (!hasAckReaction) {
nextMessages.ackReaction = fallbackEmoji;
mutated = true;
}
if (!hasAckScope) {
nextMessages.ackReactionScope = "group-mentions";
mutated = true;
}
if (!mutated) return cfg;
return {
...cfg,
messages: nextMessages,
};
}
export function applySessionDefaults(
cfg: ClawdbotConfig,
options: SessionDefaultsOptions = {},

View File

@@ -11,6 +11,7 @@ import {
import {
applyIdentityDefaults,
applyLoggingDefaults,
applyMessageDefaults,
applyModelDefaults,
applySessionDefaults,
applyTalkApiKey,
@@ -117,7 +118,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
const cfg = applyModelDefaults(
applySessionDefaults(
applyLoggingDefaults(
applyIdentityDefaults(validated.data as ClawdbotConfig),
applyMessageDefaults(
applyIdentityDefaults(validated.data as ClawdbotConfig),
),
),
),
);
@@ -148,7 +151,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
const exists = deps.fs.existsSync(configPath);
if (!exists) {
const config = applyTalkApiKey(
applyModelDefaults(applySessionDefaults({})),
applyModelDefaults(applySessionDefaults(applyMessageDefaults({}))),
);
const legacyIssues: LegacyConfigIssue[] = [];
return {
@@ -205,7 +208,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
valid: true,
config: applyTalkApiKey(
applyModelDefaults(
applySessionDefaults(applyLoggingDefaults(validated.config)),
applySessionDefaults(
applyLoggingDefaults(applyMessageDefaults(validated.config)),
),
),
),
issues: [],

View File

@@ -97,6 +97,8 @@ const FIELD_LABELS: Record<string, string> = {
"ui.seamColor": "Accent Color",
"browser.controlUrl": "Browser Control URL",
"session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns",
"messages.ackReaction": "Ack Reaction Emoji",
"messages.ackReactionScope": "Ack Reaction Scope",
"talk.apiKey": "Talk API Key",
"telegram.botToken": "Telegram Bot Token",
"discord.token": "Discord Bot Token",
@@ -131,6 +133,10 @@ const FIELD_HELP: Record<string, string> = {
"Ordered fallback image models (provider/model).",
"session.agentToAgent.maxPingPongTurns":
"Max reply-back turns between requester and target (05).",
"messages.ackReaction":
"Emoji reaction used to acknowledge inbound messages (empty disables).",
"messages.ackReactionScope":
'When to send ack reactions ("group-mentions", "group-all", "direct", "all").',
};
const FIELD_PLACEHOLDERS: Record<string, string> = {

View File

@@ -449,6 +449,10 @@ export type RoutingConfig = {
export type MessagesConfig = {
messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdbot]" if no allowFrom, else "")
responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞")
/** Emoji reaction used to acknowledge inbound messages (empty disables). */
ackReaction?: string;
/** When to send ack reactions. Default: "group-mentions". */
ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all";
};
export type BridgeBindMode = "auto" | "lan" | "tailnet" | "loopback";

View File

@@ -150,6 +150,10 @@ const MessagesSchema = z
.object({
messagePrefix: z.string().optional(),
responsePrefix: z.string().optional(),
ackReaction: z.string().optional(),
ackReactionScope: z
.enum(["group-mentions", "group-all", "direct", "all"])
.optional(),
})
.optional();