openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
299 lines (298 loc) • 8.29 kB
JavaScript
import { s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import "./identity-Qcvvo6SY.js";
import { i as TOOL_DISPLAY_CONFIG, r as resolveToolDisplay } from "./tool-display-CX5DV8AV.js";
//#region src/channels/status-reactions.ts
/** Default emoji set used by status reaction controllers. */
const DEFAULT_EMOJIS = {
queued: "👀",
thinking: "🧠",
tool: "🛠️",
coding: "💻",
web: "🌐",
deploy: "🛫",
build: "🏗️",
concierge: "💁",
done: "✅",
error: "❌",
stallSoft: "⏳",
stallHard: "⚠️",
compacting: "🗜️"
};
/** Default debounce, stall, and terminal hold timings for status reactions. */
const DEFAULT_TIMING = {
debounceMs: 700,
stallSoftMs: 1e4,
stallHardMs: 3e4,
doneHoldMs: 1500,
errorHoldMs: 2500
};
/** Tool-name tokens mapped to the coding status reaction. */
const CODING_TOOL_TOKENS = [
"exec",
"process",
"read",
"write",
"edit",
"session_status",
"bash"
];
/** Tool-name tokens mapped to the web status reaction. */
const WEB_TOOL_TOKENS = [
"web_search",
"web-search",
"web_fetch",
"web-fetch",
"browser"
];
/** Tool-name tokens mapped to the deploy status reaction. */
const DEPLOY_TOOL_TOKENS = [
"fastlane",
"deploy",
"upload",
"testflight",
"ship",
"release",
"publish",
"distribute"
];
/** Tool-name tokens mapped to the build status reaction. */
const BUILD_TOOL_TOKENS = [
"build",
"compile",
"xcode",
"swift",
"gradle",
"cargo",
"make",
"cmake",
"webpack",
"vite",
"tsc",
"lint"
];
/** Tool-name tokens mapped to the concierge/browser-control status reaction. */
const CONCIERGE_TOOL_TOKENS = [
"navigate",
"click",
"fill",
"screenshot",
"scroll",
"page",
"form",
"puppeteer",
"playwright",
"selenium",
"chromedp"
];
/** Resolves the appropriate emoji for a tool invocation. */
function resolveToolEmoji(toolName, emojis, emojiOverrides) {
const normalized = normalizeOptionalLowercaseString(toolName) ?? "";
if (!normalized) return emojis.tool;
const category = DEPLOY_TOOL_TOKENS.some((token) => normalized.includes(token)) ? "deploy" : BUILD_TOOL_TOKENS.some((token) => normalized.includes(token)) ? "build" : CONCIERGE_TOOL_TOKENS.some((token) => normalized.includes(token)) ? "concierge" : WEB_TOOL_TOKENS.some((token) => normalized.includes(token)) ? "web" : CODING_TOOL_TOKENS.some((token) => normalized.includes(token)) ? "coding" : "tool";
if (emojiOverrides?.[category] !== void 0) return emojis[category];
if (Object.hasOwn(TOOL_DISPLAY_CONFIG.tools, normalized)) return resolveToolDisplay({ name: toolName }).emoji;
if (category === "deploy") return emojis.deploy;
if (category === "build") return emojis.build;
if (category === "concierge") return emojis.concierge;
if (category === "web") return emojis.web;
if (category === "coding") return emojis.coding;
return emojis.tool;
}
/**
* Create a status reaction controller.
*
* Features:
* - Promise chain serialization (prevents concurrent API calls)
* - Debouncing (intermediate states debounce, terminal states are immediate)
* - Stall timers (soft/hard warnings on inactivity)
* - Terminal state protection (done/error mark finished, subsequent updates ignored)
* - Defers reaction removals until final cleanup to avoid visible flicker on
* platforms without atomic reaction replacement
*/
function createStatusReactionController(params) {
const { enabled, adapter, initialEmoji, onError } = params;
const emojis = {
...DEFAULT_EMOJIS,
queued: params.emojis?.queued ?? initialEmoji,
...params.emojis
};
const timing = {
...DEFAULT_TIMING,
...params.timing
};
let currentEmoji = "";
let pendingEmoji = "";
let debounceTimer = null;
let stallSoftTimer = null;
let stallHardTimer = null;
let finished = false;
let chainPromise = Promise.resolve();
const activeEmojis = /* @__PURE__ */ new Set();
function enqueue(fn) {
chainPromise = chainPromise.then(fn, fn);
return chainPromise;
}
function clearAllTimers() {
if (debounceTimer) {
clearTimeout(debounceTimer);
debounceTimer = null;
}
if (stallSoftTimer) {
clearTimeout(stallSoftTimer);
stallSoftTimer = null;
}
if (stallHardTimer) {
clearTimeout(stallHardTimer);
stallHardTimer = null;
}
}
function clearDebounceTimer() {
if (debounceTimer) {
clearTimeout(debounceTimer);
debounceTimer = null;
}
}
function resetStallTimers() {
if (stallSoftTimer) clearTimeout(stallSoftTimer);
if (stallHardTimer) clearTimeout(stallHardTimer);
stallSoftTimer = setTimeout(() => {
scheduleEmoji(emojis.stallSoft, {
immediate: true,
skipStallReset: true
});
}, timing.stallSoftMs);
stallHardTimer = setTimeout(() => {
scheduleEmoji(emojis.stallHard, {
immediate: true,
skipStallReset: true
});
}, timing.stallHardMs);
}
async function removeActiveEmojis(options = {}) {
if (!adapter.removeReaction) return;
for (const emoji of Array.from(activeEmojis)) {
if (emoji === options.keepEmoji) continue;
try {
await adapter.removeReaction(emoji);
} catch (err) {
if (onError) onError(err);
} finally {
activeEmojis.delete(emoji);
}
}
}
async function applyEmoji(newEmoji) {
if (!enabled) return;
try {
if (!adapter.removeReaction || !activeEmojis.has(newEmoji)) await adapter.setReaction(newEmoji);
activeEmojis.add(newEmoji);
currentEmoji = newEmoji;
} catch (err) {
if (onError) onError(err);
}
}
function scheduleEmoji(emoji, options = {}) {
if (!enabled || finished) return;
if (emoji === currentEmoji || emoji === pendingEmoji) {
if (!options.skipStallReset) resetStallTimers();
return;
}
pendingEmoji = emoji;
clearDebounceTimer();
if (options.immediate) enqueue(async () => {
await applyEmoji(emoji);
pendingEmoji = "";
});
else debounceTimer = setTimeout(() => {
debounceTimer = null;
enqueue(async () => {
await applyEmoji(emoji);
pendingEmoji = "";
});
}, timing.debounceMs);
if (!options.skipStallReset) resetStallTimers();
}
function setQueued() {
scheduleEmoji(emojis.queued, { immediate: true });
}
function setThinking() {
scheduleEmoji(emojis.thinking);
}
function setTool(toolName) {
scheduleEmoji(resolveToolEmoji(toolName, emojis, params.emojis));
}
function setCompacting() {
scheduleEmoji(emojis.compacting);
}
function cancelPending() {
clearDebounceTimer();
pendingEmoji = "";
}
function finishWithEmoji(emoji) {
if (!enabled) return Promise.resolve();
finished = true;
clearAllTimers();
return enqueue(async () => {
await applyEmoji(emoji);
await removeActiveEmojis({ keepEmoji: emoji });
pendingEmoji = "";
});
}
function setDone() {
return finishWithEmoji(emojis.done);
}
function setError() {
return finishWithEmoji(emojis.error);
}
async function clear() {
if (!enabled) return;
clearAllTimers();
finished = true;
await enqueue(async () => {
if (adapter.clearReaction) try {
await adapter.clearReaction();
} catch (err) {
if (onError) onError(err);
} finally {
activeEmojis.clear();
}
else if (adapter.removeReaction) await removeActiveEmojis();
currentEmoji = "";
pendingEmoji = "";
});
}
async function restoreInitial() {
if (!enabled) return;
const alreadyInitial = currentEmoji === initialEmoji;
const pendingBeforeClear = pendingEmoji;
const hadDebouncedPending = debounceTimer !== null;
const hasExtraActiveEmoji = Array.from(activeEmojis).some((emoji) => emoji !== initialEmoji);
clearAllTimers();
if (alreadyInitial && (!pendingBeforeClear || hadDebouncedPending) && !hasExtraActiveEmoji) {
pendingEmoji = "";
return;
}
if (pendingBeforeClear === initialEmoji && !hadDebouncedPending) {
await chainPromise;
return;
}
await enqueue(async () => {
await applyEmoji(initialEmoji);
await removeActiveEmojis({ keepEmoji: initialEmoji });
pendingEmoji = "";
});
}
return {
setQueued,
setThinking,
setTool,
setCompacting,
cancelPending,
setDone,
setError,
clear,
restoreInitial
};
}
//#endregion
export { DEFAULT_TIMING as a, createStatusReactionController as c, DEFAULT_EMOJIS as i, resolveToolEmoji as l, CODING_TOOL_TOKENS as n, DEPLOY_TOOL_TOKENS as o, CONCIERGE_TOOL_TOKENS as r, WEB_TOOL_TOKENS as s, BUILD_TOOL_TOKENS as t };