From 087edec93f7be8c3210d06b2430ff08037f43b45 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 16 Feb 2026 16:07:00 -0500 Subject: [PATCH] feat(slack): add draft preview cleanup lifecycle --- src/slack/draft-stream.test.ts | 50 +++++++++++++++++++ src/slack/draft-stream.ts | 31 +++++++++++- src/slack/monitor/message-handler/dispatch.ts | 4 +- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/slack/draft-stream.test.ts b/src/slack/draft-stream.test.ts index bcb1488ec..4563950e7 100644 --- a/src/slack/draft-stream.test.ts +++ b/src/slack/draft-stream.test.ts @@ -103,4 +103,54 @@ describe("createSlackDraftStream", () => { expect(edit).not.toHaveBeenCalled(); expect(warn).toHaveBeenCalledTimes(1); }); + + it("clear removes preview message when one exists", async () => { + const send = vi.fn(async () => ({ + channelId: "C123", + messageId: "111.222", + })); + const edit = vi.fn(async () => {}); + const remove = vi.fn(async () => {}); + const stream = createSlackDraftStream({ + target: "channel:C123", + token: "xoxb-test", + throttleMs: 250, + send, + edit, + remove, + }); + + stream.update("hello"); + await stream.flush(); + await stream.clear(); + + expect(remove).toHaveBeenCalledTimes(1); + expect(remove).toHaveBeenCalledWith("C123", "111.222", { + token: "xoxb-test", + accountId: undefined, + }); + expect(stream.messageId()).toBeUndefined(); + expect(stream.channelId()).toBeUndefined(); + }); + + it("clear is a no-op when no preview message exists", async () => { + const send = vi.fn(async () => ({ + channelId: "C123", + messageId: "111.222", + })); + const edit = vi.fn(async () => {}); + const remove = vi.fn(async () => {}); + const stream = createSlackDraftStream({ + target: "channel:C123", + token: "xoxb-test", + throttleMs: 250, + send, + edit, + remove, + }); + + await stream.clear(); + + expect(remove).not.toHaveBeenCalled(); + }); }); diff --git a/src/slack/draft-stream.ts b/src/slack/draft-stream.ts index 3e918c2e6..3e79a6e00 100644 --- a/src/slack/draft-stream.ts +++ b/src/slack/draft-stream.ts @@ -1,4 +1,4 @@ -import { editSlackMessage } from "./actions.js"; +import { deleteSlackMessage, editSlackMessage } from "./actions.js"; import { sendMessageSlack } from "./send.js"; const SLACK_STREAM_MAX_CHARS = 4000; @@ -7,6 +7,7 @@ const DEFAULT_THROTTLE_MS = 1000; export type SlackDraftStream = { update: (text: string) => void; flush: () => Promise; + clear: () => Promise; stop: () => void; forceNewMessage: () => void; messageId: () => string | undefined; @@ -25,11 +26,13 @@ export function createSlackDraftStream(params: { 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; @@ -152,6 +155,31 @@ export function createSlackDraftStream(params: { } }; + const clear = async () => { + stop(); + if (inFlightPromise) { + await inFlightPromise; + } + 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; @@ -164,6 +192,7 @@ export function createSlackDraftStream(params: { return { update, flush, + clear, stop, forceNewMessage, messageId: () => streamMessageId, diff --git a/src/slack/monitor/message-handler/dispatch.ts b/src/slack/monitor/message-handler/dispatch.ts index bebb34ad8..8022c4dbf 100644 --- a/src/slack/monitor/message-handler/dispatch.ts +++ b/src/slack/monitor/message-handler/dispatch.ts @@ -135,7 +135,8 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag ); } } else if (mediaCount > 0) { - draftStream?.stop(); + await draftStream?.clear(); + hasStreamedMessage = false; } const replyThreadTs = replyPlan.nextThreadTs(); @@ -215,6 +216,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag const anyReplyDelivered = queuedFinal || (counts.block ?? 0) > 0 || (counts.final ?? 0) > 0; if (!anyReplyDelivered) { + await draftStream.clear(); if (prepared.isRoomish) { clearHistoryEntriesIfEnabled({ historyMap: ctx.channelHistories,