From 2945d50d7ddfa7adac90906e38a775d84679d1fb Mon Sep 17 00:00:00 2001 From: Amir Mamaghani Date: Wed, 4 Mar 2026 18:54:29 +0100 Subject: [PATCH] fix(telegram): delete placeholder message when streaming delivers response When streaming was active, the "Thinking..." placeholder message stayed in the chat because preSend only deleted the tracking entry without removing the actual Telegram message. Now preSend deletes the placeholder via the new MessageDeleter interface when streamActive is set. --- pkg/channels/interfaces.go | 5 +++++ pkg/channels/manager.go | 14 +++++++++++--- pkg/channels/telegram/telegram.go | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pkg/channels/interfaces.go b/pkg/channels/interfaces.go index 580157ce0..99e977079 100644 --- a/pkg/channels/interfaces.go +++ b/pkg/channels/interfaces.go @@ -15,6 +15,11 @@ type MessageEditor interface { EditMessage(ctx context.Context, chatID string, messageID string, content string) error } +// MessageDeleter — channels that can delete a message by ID. +type MessageDeleter interface { + DeleteMessage(ctx context.Context, chatID string, messageID string) error +} + // ReactionCapable — channels that can add a reaction (e.g. 👀) to an inbound message. // ReactToMessage adds a reaction and returns an undo function to remove it. // The undo function MUST be idempotent and safe to call multiple times. diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index ba56bd789..c259e439a 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -133,10 +133,18 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess } } - // 3. If a stream already finalized this message, skip both placeholder and send + // 3. If a stream already finalized this message, delete the placeholder and skip send if _, loaded := m.streamActive.LoadAndDelete(key); loaded { - // Also clean up any stale placeholder - m.placeholders.Delete(key) + if v, loaded := m.placeholders.LoadAndDelete(key); loaded { + if entry, ok := v.(placeholderEntry); ok && entry.id != "" { + // Prefer deleting the placeholder (cleaner UX than editing to same content) + if deleter, ok := ch.(MessageDeleter); ok { + deleter.DeleteMessage(ctx, msg.ChatID, entry.id) // best effort + } else if editor, ok := ch.(MessageEditor); ok { + editor.EditMessage(ctx, msg.ChatID, entry.id, msg.Content) // fallback + } + } + } return true } diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index a7df1ebc5..4daace52a 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -301,6 +301,22 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag return err } +// DeleteMessage implements channels.MessageDeleter. +func (c *TelegramChannel) DeleteMessage(ctx context.Context, chatID string, messageID string) error { + cid, err := parseChatID(chatID) + if err != nil { + return err + } + mid, err := strconv.Atoi(messageID) + if err != nil { + return err + } + return c.bot.DeleteMessage(ctx, &telego.DeleteMessageParams{ + ChatID: tu.ID(cid), + MessageID: mid, + }) +} + // SendPlaceholder implements channels.PlaceholderCapable. // It sends a placeholder message (e.g. "Thinking... 💭") that will later be // edited to the actual response via EditMessage (channels.MessageEditor).