import { createDraftStreamLoop } from "../channels/draft-stream-loop.js"; import { deleteSlackMessage, editSlackMessage } from "./actions.js"; import { sendMessageSlack } from "./send.js"; const SLACK_STREAM_MAX_CHARS = 4000; const DEFAULT_THROTTLE_MS = 1000; export type SlackDraftStream = { update: (text: string) => void; flush: () => Promise; clear: () => Promise; stop: () => void; forceNewMessage: () => void; messageId: () => string | undefined; channelId: () => string | undefined; }; export function createSlackDraftStream(params: { target: string; token: string; accountId?: string; maxChars?: number; throttleMs?: number; resolveThreadTs?: () => string | undefined; onMessageSent?: () => void; log?: (message: string) => void; warn?: (message: string) => void; send?: typeof sendMessageSlack; edit?: typeof editSlackMessage; remove?: typeof deleteSlackMessage; }): SlackDraftStream { const maxChars = Math.min(params.maxChars ?? SLACK_STREAM_MAX_CHARS, SLACK_STREAM_MAX_CHARS); const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS); const send = params.send ?? sendMessageSlack; const edit = params.edit ?? editSlackMessage; const remove = params.remove ?? deleteSlackMessage; let streamMessageId: string | undefined; let streamChannelId: string | undefined; let lastSentText = ""; let stopped = false; const sendOrEditStreamMessage = async (text: string) => { if (stopped) { return; } const trimmed = text.trimEnd(); if (!trimmed) { return; } if (trimmed.length > maxChars) { stopped = true; params.warn?.(`slack stream preview stopped (text length ${trimmed.length} > ${maxChars})`); return; } if (trimmed === lastSentText) { return; } lastSentText = trimmed; try { if (streamChannelId && streamMessageId) { await edit(streamChannelId, streamMessageId, trimmed, { token: params.token, accountId: params.accountId, }); return; } const sent = await send(params.target, trimmed, { token: params.token, accountId: params.accountId, threadTs: params.resolveThreadTs?.(), }); streamChannelId = sent.channelId || streamChannelId; streamMessageId = sent.messageId || streamMessageId; if (!streamChannelId || !streamMessageId) { stopped = true; params.warn?.("slack stream preview stopped (missing identifiers from sendMessage)"); return; } params.onMessageSent?.(); } catch (err) { stopped = true; params.warn?.( `slack stream preview failed: ${err instanceof Error ? err.message : String(err)}`, ); } }; const loop = createDraftStreamLoop({ throttleMs, isStopped: () => stopped, sendOrEditStreamMessage, }); const stop = () => { stopped = true; loop.stop(); }; const clear = async () => { stop(); await loop.waitForInFlight(); const channelId = streamChannelId; const messageId = streamMessageId; streamChannelId = undefined; streamMessageId = undefined; lastSentText = ""; if (!channelId || !messageId) { return; } try { await remove(channelId, messageId, { token: params.token, accountId: params.accountId, }); } catch (err) { params.warn?.( `slack stream preview cleanup failed: ${err instanceof Error ? err.message : String(err)}`, ); } }; const forceNewMessage = () => { streamMessageId = undefined; streamChannelId = undefined; lastSentText = ""; loop.resetPending(); }; params.log?.(`slack stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`); return { update: loop.update, flush: loop.flush, clear, stop, forceNewMessage, messageId: () => streamMessageId, channelId: () => streamChannelId, }; }