import { createReplyPrefixContext, createTypingCallbacks, logTypingFailure, type ClawdbotConfig, type RuntimeEnv, type ReplyPayload, } from "openclaw/plugin-sdk"; import { getPopoRuntime } from "./runtime.js"; import { sendMessagePopo, sendRichTextPopo, textToRichTextContent } from "./send.js"; import type { PopoConfig } from "./types.js"; export type CreatePopoReplyDispatcherParams = { cfg: ClawdbotConfig; agentId: string; runtime: RuntimeEnv; sessionId: string; }; export function createPopoReplyDispatcher(params: CreatePopoReplyDispatcherParams) { const core = getPopoRuntime(); const { cfg, agentId, sessionId } = params; const prefixContext = createReplyPrefixContext({ cfg, agentId, }); // POPO doesn't have a native typing indicator API // We could potentially use emoji reactions but skip for now const typingCallbacks = createTypingCallbacks({ start: async () => { // No-op for POPO }, stop: async () => { // No-op for POPO }, onStartError: (err) => { logTypingFailure({ log: (message) => params.runtime.log?.(message), channel: "popo", action: "start", error: err, }); }, onStopError: (err) => { logTypingFailure({ log: (message) => params.runtime.log?.(message), channel: "popo", action: "stop", error: err, }); }, }); const textChunkLimit = core.channel.text.resolveTextChunkLimit({ cfg, channel: "popo", defaultLimit: 4000, }); const chunkMode = core.channel.text.resolveChunkMode(cfg, "popo"); const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ responsePrefix: prefixContext.responsePrefix, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId), onReplyStart: typingCallbacks.onReplyStart, deliver: async (payload: ReplyPayload) => { params.runtime.log?.(`popo deliver called: text=${payload.text?.slice(0, 100)}`); const text = payload.text ?? ""; if (!text.trim()) { params.runtime.log?.(`popo deliver: empty text, skipping`); return; } // Check render mode: raw (default) or rich_text const popoCfg = cfg.channels?.popo as PopoConfig | undefined; const renderMode = popoCfg?.renderMode ?? "raw"; const chunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode); params.runtime.log?.(`popo deliver: sending ${chunks.length} chunks to ${sessionId}`); for (const chunk of chunks) { if (renderMode === "rich_text") { // Rich text mode const content = textToRichTextContent(chunk); await sendRichTextPopo({ cfg, to: sessionId, content, }); } else { // Raw text mode (default) await sendMessagePopo({ cfg, to: sessionId, text: chunk, }); } } }, onError: (err, info) => { params.runtime.error?.(`popo ${info.kind} reply failed: ${String(err)}`); typingCallbacks.onIdle?.(); }, onIdle: typingCallbacks.onIdle, }); return { dispatcher, replyOptions: { ...replyOptions, onModelSelected: prefixContext.onModelSelected, }, markDispatchIdle, }; }