feat: show status reaction during context compaction (#35474)
Merged via squash. Prepared head SHA: 145a7b7c4e1939718c41a300899ae813bd9c511b Co-authored-by: Cypherm <28184436+Cypherm@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman
This commit is contained in:
parent
4e872521f0
commit
61d219cb39
@ -84,6 +84,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Agents/compaction safeguard: route missing-model and missing-API-key cancellation warnings through the shared subsystem logger so they land in structured and file logs. (#9974) Thanks @dinakars777.
|
- Agents/compaction safeguard: route missing-model and missing-API-key cancellation warnings through the shared subsystem logger so they land in structured and file logs. (#9974) Thanks @dinakars777.
|
||||||
- Cron/doctor: stop flagging canonical `agentTurn` and `systemEvent` payload kinds as legacy cron storage, while still normalizing whitespace-padded and non-canonical variants. (#44012) Thanks @shuicici.
|
- Cron/doctor: stop flagging canonical `agentTurn` and `systemEvent` payload kinds as legacy cron storage, while still normalizing whitespace-padded and non-canonical variants. (#44012) Thanks @shuicici.
|
||||||
- ACP/client final-message delivery: preserve terminal assistant text snapshots before resolving `end_turn`, so ACP clients no longer drop the last visible reply when the gateway sends the final message body on the terminal chat event. (#17615) Thanks @pjeby.
|
- ACP/client final-message delivery: preserve terminal assistant text snapshots before resolving `end_turn`, so ACP clients no longer drop the last visible reply when the gateway sends the final message body on the terminal chat event. (#17615) Thanks @pjeby.
|
||||||
|
- Telegram/Discord status reactions: show a temporary compacting reaction during auto-compaction pauses and restore thinking afterward so the bot no longer appears frozen while context is being compacted. (#35474) thanks @Cypherm.
|
||||||
|
|
||||||
## 2026.3.11
|
## 2026.3.11
|
||||||
|
|
||||||
|
|||||||
@ -393,11 +393,15 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
await params.opts?.onToolStart?.({ name, phase });
|
await params.opts?.onToolStart?.({ name, phase });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Track auto-compaction completion
|
// Track auto-compaction completion and notify UI layer
|
||||||
if (evt.stream === "compaction") {
|
if (evt.stream === "compaction") {
|
||||||
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
|
||||||
|
if (phase === "start") {
|
||||||
|
await params.opts?.onCompactionStart?.();
|
||||||
|
}
|
||||||
if (phase === "end") {
|
if (phase === "end") {
|
||||||
autoCompactionCompleted = true;
|
autoCompactionCompleted = true;
|
||||||
|
await params.opts?.onCompactionEnd?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -54,6 +54,10 @@ export type GetReplyOptions = {
|
|||||||
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
|
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
|
||||||
/** Called when a tool phase starts/updates, before summary payloads are emitted. */
|
/** Called when a tool phase starts/updates, before summary payloads are emitted. */
|
||||||
onToolStart?: (payload: { name?: string; phase?: string }) => Promise<void> | void;
|
onToolStart?: (payload: { name?: string; phase?: string }) => Promise<void> | void;
|
||||||
|
/** Called when context auto-compaction starts (allows UX feedback during the pause). */
|
||||||
|
onCompactionStart?: () => Promise<void> | void;
|
||||||
|
/** Called when context auto-compaction completes. */
|
||||||
|
onCompactionEnd?: () => Promise<void> | void;
|
||||||
/** Called when the actual model is selected (including after fallback).
|
/** Called when the actual model is selected (including after fallback).
|
||||||
* Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */
|
* Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */
|
||||||
onModelSelected?: (ctx: ModelSelectedContext) => void;
|
onModelSelected?: (ctx: ModelSelectedContext) => void;
|
||||||
|
|||||||
@ -148,6 +148,15 @@ describe("createStatusReactionController", () => {
|
|||||||
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.thinking });
|
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.thinking });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should debounce setCompacting and eventually call adapter", async () => {
|
||||||
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
|
void controller.setCompacting();
|
||||||
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
|
|
||||||
|
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.compacting });
|
||||||
|
});
|
||||||
|
|
||||||
it("should classify tool name and debounce", async () => {
|
it("should classify tool name and debounce", async () => {
|
||||||
const { calls, controller } = createEnabledController();
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
@ -245,6 +254,19 @@ describe("createStatusReactionController", () => {
|
|||||||
expect(calls.length).toBe(callsAfterFirst);
|
expect(calls.length).toBe(callsAfterFirst);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should cancel a pending compacting emoji before resuming thinking", async () => {
|
||||||
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
|
void controller.setCompacting();
|
||||||
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs - 1);
|
||||||
|
controller.cancelPending();
|
||||||
|
void controller.setThinking();
|
||||||
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
|
|
||||||
|
const setEmojis = calls.filter((call) => call.method === "set").map((call) => call.emoji);
|
||||||
|
expect(setEmojis).toEqual([DEFAULT_EMOJIS.thinking]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should call removeReaction when adapter supports it and emoji changes", async () => {
|
it("should call removeReaction when adapter supports it and emoji changes", async () => {
|
||||||
const { calls, controller } = createEnabledController();
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
@ -446,6 +468,7 @@ describe("constants", () => {
|
|||||||
const emojiKeys = [
|
const emojiKeys = [
|
||||||
"queued",
|
"queued",
|
||||||
"thinking",
|
"thinking",
|
||||||
|
"compacting",
|
||||||
"tool",
|
"tool",
|
||||||
"coding",
|
"coding",
|
||||||
"web",
|
"web",
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export type StatusReactionEmojis = {
|
|||||||
error?: string; // Default: "❌"
|
error?: string; // Default: "❌"
|
||||||
stallSoft?: string; // Default: "⏳"
|
stallSoft?: string; // Default: "⏳"
|
||||||
stallHard?: string; // Default: "⚠️"
|
stallHard?: string; // Default: "⚠️"
|
||||||
|
compacting?: string; // Default: "✍"
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatusReactionTiming = {
|
export type StatusReactionTiming = {
|
||||||
@ -38,6 +39,9 @@ export type StatusReactionController = {
|
|||||||
setQueued: () => Promise<void> | void;
|
setQueued: () => Promise<void> | void;
|
||||||
setThinking: () => Promise<void> | void;
|
setThinking: () => Promise<void> | void;
|
||||||
setTool: (toolName?: string) => Promise<void> | void;
|
setTool: (toolName?: string) => Promise<void> | void;
|
||||||
|
setCompacting: () => Promise<void> | void;
|
||||||
|
/** Cancel any pending debounced emoji (useful before forcing a state transition). */
|
||||||
|
cancelPending: () => void;
|
||||||
setDone: () => Promise<void>;
|
setDone: () => Promise<void>;
|
||||||
setError: () => Promise<void>;
|
setError: () => Promise<void>;
|
||||||
clear: () => Promise<void>;
|
clear: () => Promise<void>;
|
||||||
@ -58,6 +62,7 @@ export const DEFAULT_EMOJIS: Required<StatusReactionEmojis> = {
|
|||||||
error: "😱",
|
error: "😱",
|
||||||
stallSoft: "🥱",
|
stallSoft: "🥱",
|
||||||
stallHard: "😨",
|
stallHard: "😨",
|
||||||
|
compacting: "✍",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_TIMING: Required<StatusReactionTiming> = {
|
export const DEFAULT_TIMING: Required<StatusReactionTiming> = {
|
||||||
@ -162,6 +167,7 @@ export function createStatusReactionController(params: {
|
|||||||
emojis.error,
|
emojis.error,
|
||||||
emojis.stallSoft,
|
emojis.stallSoft,
|
||||||
emojis.stallHard,
|
emojis.stallHard,
|
||||||
|
emojis.compacting,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -306,6 +312,15 @@ export function createStatusReactionController(params: {
|
|||||||
scheduleEmoji(emoji);
|
scheduleEmoji(emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCompacting(): void {
|
||||||
|
scheduleEmoji(emojis.compacting);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelPending(): void {
|
||||||
|
clearDebounceTimer();
|
||||||
|
pendingEmoji = "";
|
||||||
|
}
|
||||||
|
|
||||||
function finishWithEmoji(emoji: string): Promise<void> {
|
function finishWithEmoji(emoji: string): Promise<void> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@ -375,6 +390,8 @@ export function createStatusReactionController(params: {
|
|||||||
setQueued,
|
setQueued,
|
||||||
setThinking,
|
setThinking,
|
||||||
setTool,
|
setTool,
|
||||||
|
setCompacting,
|
||||||
|
cancelPending,
|
||||||
setDone,
|
setDone,
|
||||||
setError,
|
setError,
|
||||||
clear,
|
clear,
|
||||||
|
|||||||
@ -1481,7 +1481,7 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
"messages.statusReactions.enabled":
|
"messages.statusReactions.enabled":
|
||||||
"Enable lifecycle status reactions for Telegram. When enabled, the ack reaction becomes the initial 'queued' state and progresses through thinking, tool, done/error automatically. Default: false.",
|
"Enable lifecycle status reactions for Telegram. When enabled, the ack reaction becomes the initial 'queued' state and progresses through thinking, tool, done/error automatically. Default: false.",
|
||||||
"messages.statusReactions.emojis":
|
"messages.statusReactions.emojis":
|
||||||
"Override default status reaction emojis. Keys: thinking, tool, coding, web, done, error, stallSoft, stallHard. Must be valid Telegram reaction emojis.",
|
"Override default status reaction emojis. Keys: thinking, compacting, tool, coding, web, done, error, stallSoft, stallHard. Must be valid Telegram reaction emojis.",
|
||||||
"messages.statusReactions.timing":
|
"messages.statusReactions.timing":
|
||||||
"Override default timing. Keys: debounceMs (700), stallSoftMs (25000), stallHardMs (60000), doneHoldMs (1500), errorHoldMs (2500).",
|
"Override default timing. Keys: debounceMs (700), stallSoftMs (25000), stallHardMs (60000), doneHoldMs (1500), errorHoldMs (2500).",
|
||||||
"messages.inbound.debounceMs":
|
"messages.inbound.debounceMs":
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export type StatusReactionsEmojiConfig = {
|
|||||||
error?: string;
|
error?: string;
|
||||||
stallSoft?: string;
|
stallSoft?: string;
|
||||||
stallHard?: string;
|
stallHard?: string;
|
||||||
|
compacting?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatusReactionsTimingConfig = {
|
export type StatusReactionsTimingConfig = {
|
||||||
|
|||||||
@ -169,6 +169,7 @@ export const MessagesSchema = z
|
|||||||
error: z.string().optional(),
|
error: z.string().optional(),
|
||||||
stallSoft: z.string().optional(),
|
stallSoft: z.string().optional(),
|
||||||
stallHard: z.string().optional(),
|
stallHard: z.string().optional(),
|
||||||
|
compacting: z.string().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@ -47,15 +47,19 @@ type DispatchInboundParams = {
|
|||||||
onReasoningStream?: () => Promise<void> | void;
|
onReasoningStream?: () => Promise<void> | void;
|
||||||
onReasoningEnd?: () => Promise<void> | void;
|
onReasoningEnd?: () => Promise<void> | void;
|
||||||
onToolStart?: (payload: { name?: string }) => Promise<void> | void;
|
onToolStart?: (payload: { name?: string }) => Promise<void> | void;
|
||||||
|
onCompactionStart?: () => Promise<void> | void;
|
||||||
|
onCompactionEnd?: () => Promise<void> | void;
|
||||||
onPartialReply?: (payload: { text?: string }) => Promise<void> | void;
|
onPartialReply?: (payload: { text?: string }) => Promise<void> | void;
|
||||||
onAssistantMessageStart?: () => Promise<void> | void;
|
onAssistantMessageStart?: () => Promise<void> | void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const dispatchInboundMessage = vi.fn(async (_params?: DispatchInboundParams) => ({
|
const dispatchInboundMessage = vi.hoisted(() =>
|
||||||
queuedFinal: false,
|
vi.fn(async (_params?: DispatchInboundParams) => ({
|
||||||
counts: { final: 0, tool: 0, block: 0 },
|
queuedFinal: false,
|
||||||
}));
|
counts: { final: 0, tool: 0, block: 0 },
|
||||||
const recordInboundSession = vi.fn(async () => {});
|
})),
|
||||||
|
);
|
||||||
|
const recordInboundSession = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
const configSessionsMocks = vi.hoisted(() => ({
|
const configSessionsMocks = vi.hoisted(() => ({
|
||||||
readSessionUpdatedAt: vi.fn(() => undefined),
|
readSessionUpdatedAt: vi.fn(() => undefined),
|
||||||
resolveStorePath: vi.fn(() => "/tmp/openclaw-discord-process-test-sessions.json"),
|
resolveStorePath: vi.fn(() => "/tmp/openclaw-discord-process-test-sessions.json"),
|
||||||
@ -346,6 +350,39 @@ describe("processDiscordMessage ack reactions", () => {
|
|||||||
expect(emojis).toContain("🏁");
|
expect(emojis).toContain("🏁");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows compacting reaction during auto-compaction and resumes thinking", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||||
|
await params?.replyOptions?.onCompactionStart?.();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
||||||
|
await params?.replyOptions?.onCompactionEnd?.();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
||||||
|
return createNoQueuedDispatchResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = await createBaseContext({
|
||||||
|
cfg: {
|
||||||
|
messages: {
|
||||||
|
ackReaction: "👀",
|
||||||
|
statusReactions: {
|
||||||
|
timing: { debounceMs: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: { store: "/tmp/openclaw-discord-process-test-sessions.json" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
|
const runPromise = processDiscordMessage(ctx as any);
|
||||||
|
await vi.advanceTimersByTimeAsync(2_500);
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
await runPromise;
|
||||||
|
|
||||||
|
const emojis = getReactionEmojis();
|
||||||
|
expect(emojis).toContain(DEFAULT_EMOJIS.compacting);
|
||||||
|
expect(emojis).toContain(DEFAULT_EMOJIS.thinking);
|
||||||
|
});
|
||||||
|
|
||||||
it("clears status reactions when dispatch aborts and removeAckAfterReply is enabled", async () => {
|
it("clears status reactions when dispatch aborts and removeAckAfterReply is enabled", async () => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
dispatchInboundMessage.mockImplementationOnce(async () => {
|
dispatchInboundMessage.mockImplementationOnce(async () => {
|
||||||
|
|||||||
@ -769,6 +769,19 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
}
|
}
|
||||||
await statusReactions.setTool(payload.name);
|
await statusReactions.setTool(payload.name);
|
||||||
},
|
},
|
||||||
|
onCompactionStart: async () => {
|
||||||
|
if (isProcessAborted(abortSignal)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await statusReactions.setCompacting();
|
||||||
|
},
|
||||||
|
onCompactionEnd: async () => {
|
||||||
|
if (isProcessAborted(abortSignal)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusReactions.cancelPending();
|
||||||
|
await statusReactions.setThinking();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (isProcessAborted(abortSignal)) {
|
if (isProcessAborted(abortSignal)) {
|
||||||
|
|||||||
@ -75,6 +75,8 @@ describe("resolveProxyFetchFromEnv", () => {
|
|||||||
it("returns proxy fetch using EnvHttpProxyAgent when HTTPS_PROXY is set", async () => {
|
it("returns proxy fetch using EnvHttpProxyAgent when HTTPS_PROXY is set", async () => {
|
||||||
vi.stubEnv("HTTP_PROXY", "");
|
vi.stubEnv("HTTP_PROXY", "");
|
||||||
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");
|
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");
|
||||||
|
delete process.env.https_proxy;
|
||||||
|
delete process.env.http_proxy;
|
||||||
undiciFetch.mockResolvedValue({ ok: true });
|
undiciFetch.mockResolvedValue({ ok: true });
|
||||||
|
|
||||||
const fetchFn = resolveProxyFetchFromEnv();
|
const fetchFn = resolveProxyFetchFromEnv();
|
||||||
@ -91,6 +93,8 @@ describe("resolveProxyFetchFromEnv", () => {
|
|||||||
it("returns proxy fetch when HTTP_PROXY is set", () => {
|
it("returns proxy fetch when HTTP_PROXY is set", () => {
|
||||||
vi.stubEnv("HTTPS_PROXY", "");
|
vi.stubEnv("HTTPS_PROXY", "");
|
||||||
vi.stubEnv("HTTP_PROXY", "http://fallback.test:3128");
|
vi.stubEnv("HTTP_PROXY", "http://fallback.test:3128");
|
||||||
|
delete process.env.https_proxy;
|
||||||
|
delete process.env.http_proxy;
|
||||||
|
|
||||||
const fetchFn = resolveProxyFetchFromEnv();
|
const fetchFn = resolveProxyFetchFromEnv();
|
||||||
expect(fetchFn).toBeDefined();
|
expect(fetchFn).toBeDefined();
|
||||||
|
|||||||
@ -2182,4 +2182,41 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
);
|
);
|
||||||
expect(finalTextSentViaDeliverReplies).toBe(true);
|
expect(finalTextSentViaDeliverReplies).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows compacting reaction during auto-compaction and resumes thinking", async () => {
|
||||||
|
const statusReactionController = {
|
||||||
|
setThinking: vi.fn(async () => {}),
|
||||||
|
setCompacting: vi.fn(async () => {}),
|
||||||
|
setTool: vi.fn(async () => {}),
|
||||||
|
setDone: vi.fn(async () => {}),
|
||||||
|
setError: vi.fn(async () => {}),
|
||||||
|
setQueued: vi.fn(async () => {}),
|
||||||
|
cancelPending: vi.fn(() => {}),
|
||||||
|
clear: vi.fn(async () => {}),
|
||||||
|
restoreInitial: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
|
||||||
|
await replyOptions?.onCompactionStart?.();
|
||||||
|
await replyOptions?.onCompactionEnd?.();
|
||||||
|
return { queuedFinal: true };
|
||||||
|
});
|
||||||
|
deliverReplies.mockResolvedValue({ delivered: true });
|
||||||
|
|
||||||
|
await dispatchWithContext({
|
||||||
|
context: createContext({
|
||||||
|
statusReactionController: statusReactionController as never,
|
||||||
|
}),
|
||||||
|
streamMode: "off",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(statusReactionController.setCompacting).toHaveBeenCalledTimes(1);
|
||||||
|
expect(statusReactionController.cancelPending).toHaveBeenCalledTimes(1);
|
||||||
|
expect(statusReactionController.setThinking).toHaveBeenCalledTimes(2);
|
||||||
|
expect(statusReactionController.setCompacting.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
statusReactionController.cancelPending.mock.invocationCallOrder[0],
|
||||||
|
);
|
||||||
|
expect(statusReactionController.cancelPending.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
statusReactionController.setThinking.mock.invocationCallOrder[1],
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -713,6 +713,15 @@ export const dispatchTelegramMessage = async ({
|
|||||||
await statusReactionController.setTool(payload.name);
|
await statusReactionController.setTool(payload.name);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
onCompactionStart: statusReactionController
|
||||||
|
? () => statusReactionController.setCompacting()
|
||||||
|
: undefined,
|
||||||
|
onCompactionEnd: statusReactionController
|
||||||
|
? async () => {
|
||||||
|
statusReactionController.cancelPending();
|
||||||
|
await statusReactionController.setThinking();
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
onModelSelected,
|
onModelSelected,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -90,6 +90,7 @@ export const TELEGRAM_STATUS_REACTION_VARIANTS: Record<StatusReactionEmojiKey, s
|
|||||||
error: ["😱", "😨", "🤯"],
|
error: ["😱", "😨", "🤯"],
|
||||||
stallSoft: ["🥱", "😴", "🤔"],
|
stallSoft: ["🥱", "😴", "🤔"],
|
||||||
stallHard: ["😨", "😱", "⚡"],
|
stallHard: ["😨", "😱", "⚡"],
|
||||||
|
compacting: ["✍", "🤔", "🤯"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_REACTION_EMOJI_KEYS: StatusReactionEmojiKey[] = [
|
const STATUS_REACTION_EMOJI_KEYS: StatusReactionEmojiKey[] = [
|
||||||
@ -102,6 +103,7 @@ const STATUS_REACTION_EMOJI_KEYS: StatusReactionEmojiKey[] = [
|
|||||||
"error",
|
"error",
|
||||||
"stallSoft",
|
"stallSoft",
|
||||||
"stallHard",
|
"stallHard",
|
||||||
|
"compacting",
|
||||||
];
|
];
|
||||||
|
|
||||||
function normalizeEmoji(value: string | undefined): string | undefined {
|
function normalizeEmoji(value: string | undefined): string | undefined {
|
||||||
@ -129,6 +131,7 @@ export function resolveTelegramStatusReactionEmojis(params: {
|
|||||||
error: normalizeEmoji(overrides?.error) ?? DEFAULT_EMOJIS.error,
|
error: normalizeEmoji(overrides?.error) ?? DEFAULT_EMOJIS.error,
|
||||||
stallSoft: normalizeEmoji(overrides?.stallSoft) ?? DEFAULT_EMOJIS.stallSoft,
|
stallSoft: normalizeEmoji(overrides?.stallSoft) ?? DEFAULT_EMOJIS.stallSoft,
|
||||||
stallHard: normalizeEmoji(overrides?.stallHard) ?? DEFAULT_EMOJIS.stallHard,
|
stallHard: normalizeEmoji(overrides?.stallHard) ?? DEFAULT_EMOJIS.stallHard,
|
||||||
|
compacting: normalizeEmoji(overrides?.compacting) ?? DEFAULT_EMOJIS.compacting,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -115,9 +115,14 @@ describe("agentLogoUrl", () => {
|
|||||||
describe("resolveAgentAvatarUrl", () => {
|
describe("resolveAgentAvatarUrl", () => {
|
||||||
it("prefers a runtime avatar URL over non-URL identity avatars", () => {
|
it("prefers a runtime avatar URL over non-URL identity avatars", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveAgentAvatarUrl({ identity: { avatar: "A", avatarUrl: "/avatar/main" } }, {
|
resolveAgentAvatarUrl(
|
||||||
avatar: "A",
|
{ identity: { avatar: "A", avatarUrl: "/avatar/main" } },
|
||||||
} as { avatar: string }),
|
{
|
||||||
|
agentId: "main",
|
||||||
|
avatar: "A",
|
||||||
|
name: "Main",
|
||||||
|
},
|
||||||
|
),
|
||||||
).toBe("/avatar/main");
|
).toBe("/avatar/main");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user