Files
clawdbot/src/auto-reply/reply/typing-mode.ts
Tyler Yust b073deee20 feat: implement short ID mapping for BlueBubbles messages and enhance reply context caching
- Added functionality to resolve short message IDs to full UUIDs and vice versa, optimizing token usage.
- Introduced a reply cache to store message context for replies when metadata is omitted in webhook payloads.
- Updated message handling to utilize short IDs for outbound messages and replies, improving efficiency.
- Enhanced error messages to clarify required parameters for actions like react, edit, and unsend.
- Added tests to ensure correct behavior of new features and maintain existing functionality.
2026-01-21 00:45:01 -08:00

121 lines
3.3 KiB
TypeScript

import type { TypingMode } from "../../config/types.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
import type { TypingController } from "./typing.js";
export type TypingModeContext = {
configured?: TypingMode;
isGroupChat: boolean;
wasMentioned: boolean;
isHeartbeat: boolean;
};
export const DEFAULT_GROUP_TYPING_MODE: TypingMode = "message";
export function resolveTypingMode({
configured,
isGroupChat,
wasMentioned,
isHeartbeat,
}: TypingModeContext): TypingMode {
if (isHeartbeat) return "never";
if (configured) return configured;
if (!isGroupChat || wasMentioned) return "instant";
return DEFAULT_GROUP_TYPING_MODE;
}
export type TypingSignaler = {
mode: TypingMode;
shouldStartImmediately: boolean;
shouldStartOnMessageStart: boolean;
shouldStartOnText: boolean;
shouldStartOnReasoning: boolean;
signalRunStart: () => Promise<void>;
signalMessageStart: () => Promise<void>;
signalTextDelta: (text?: string) => Promise<void>;
signalReasoningDelta: () => Promise<void>;
signalToolStart: () => Promise<void>;
};
export function createTypingSignaler(params: {
typing: TypingController;
mode: TypingMode;
isHeartbeat: boolean;
}): TypingSignaler {
const { typing, mode, isHeartbeat } = params;
const shouldStartImmediately = mode === "instant";
const shouldStartOnMessageStart = mode === "message";
const shouldStartOnText = mode === "message" || mode === "instant";
const shouldStartOnReasoning = mode === "thinking";
const disabled = isHeartbeat || mode === "never";
let hasRenderableText = false;
const isRenderableText = (text?: string): boolean => {
const trimmed = text?.trim();
if (!trimmed) return false;
return !isSilentReplyText(trimmed, SILENT_REPLY_TOKEN);
};
const signalRunStart = async () => {
if (disabled || !shouldStartImmediately) return;
await typing.startTypingLoop();
};
const signalMessageStart = async () => {
if (disabled || !shouldStartOnMessageStart) return;
if (!hasRenderableText) return;
await typing.startTypingLoop();
};
const signalTextDelta = async (text?: string) => {
if (disabled) return;
const renderable = isRenderableText(text);
if (renderable) {
hasRenderableText = true;
} else if (text?.trim()) {
return;
}
if (shouldStartOnText) {
await typing.startTypingOnText(text);
return;
}
if (shouldStartOnReasoning) {
if (!typing.isActive()) {
await typing.startTypingLoop();
}
typing.refreshTypingTtl();
}
};
const signalReasoningDelta = async () => {
if (disabled || !shouldStartOnReasoning) return;
if (!hasRenderableText) return;
await typing.startTypingLoop();
typing.refreshTypingTtl();
};
const signalToolStart = async () => {
if (disabled) return;
// Start typing as soon as tools begin executing, even before the first text delta.
if (!typing.isActive()) {
await typing.startTypingLoop();
typing.refreshTypingTtl();
return;
}
// Keep typing indicator alive during tool execution.
typing.refreshTypingTtl();
};
return {
mode,
shouldStartImmediately,
shouldStartOnMessageStart,
shouldStartOnText,
shouldStartOnReasoning,
signalRunStart,
signalMessageStart,
signalTextDelta,
signalReasoningDelta,
signalToolStart,
};
}