Skip to content

feat(telegram): add reaction indicator for message processing#433

Merged
qhkm merged 2 commits intoqhkm:mainfrom
stuartbowness:telegram-reaction-indicator-v2
Mar 26, 2026
Merged

feat(telegram): add reaction indicator for message processing#433
qhkm merged 2 commits intoqhkm:mainfrom
stuartbowness:telegram-reaction-indicator-v2

Conversation

@stuartbowness
Copy link
Copy Markdown
Contributor

@stuartbowness stuartbowness commented Mar 25, 2026

Summary

  • Add 👀 eyes reaction to inbound messages when processing begins
  • Replace with ✅ checkmark reaction when the response is sent
  • Provides visual feedback in Telegram that the bot has seen and is working on the message

Depends on #432. Replaces #430 (rebased to resolve conflicts from merged history).

Test plan

  • Existing telegram tests pass
  • Send a message — eyes reaction should appear immediately, then switch to checkmark when response arrives
  • Verify reactions work in both direct chats and forum topics

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Visual processing indicators on Telegram: 👀 appears while the bot is handling a message and is replaced with ✅ when the reply is sent.
    • Replies are now threaded back to the originating Telegram message so conversations stay linked.
  • Chores

    • Added a configurable option to enable/disable Telegram reaction indicators (enabled by default).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0bdf66c2-140a-4fef-9b35-6e7f326fb3f6

📥 Commits

Reviewing files that changed from the base of the PR and between 3692f96 and 430bdd8.

📒 Files selected for processing (3)
  • src/agent/loop.rs
  • src/channels/telegram.rs
  • src/config/types.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/agent/loop.rs

📝 Walkthrough

Walkthrough

Propagates telegram_message_id from inbound to outbound routing metadata; adds optional Telegram processing reactions (👀 on receipt, ✅ on send) and improves reply-threading by using reply_to or telegram_message_id when sending replies.

Changes

Cohort / File(s) Summary
Routing metadata propagation
src/agent/loop.rs
Copy inbound.metadata["telegram_message_id"] into outbound metadata for output.for_user paths.
Telegram channel: reactions & reply threading
src/channels/telegram.rs
New reactions flag injection; set 👀 reaction on inbound when enabled; after sending reply, set ✅ on original message (best-effort); build ReplyParameters from msg.reply_to or fallback telegram_message_id metadata.
Config: Telegram reactions flag
src/config/types.rs
Add pub reactions: bool to TelegramConfig with #[serde(default = "default_telegram_reactions")] and helper default_telegram_reactions() returning true; update Default impl.

Sequence Diagram

sequenceDiagram
    actor User as User/Telegram
    participant TG as Telegram API
    participant Ch as TelegramChannel
    participant Agent as Agent Loop

    User->>TG: Send message
    TG->>Ch: Deliver inbound message (includes telegram_message_id)
    Ch->>Ch: Optionally set 👀 reaction (if enabled)
    Ch->>Agent: Publish InboundMessage (with telegram_message_id metadata)
    Agent->>Agent: Process and propagate telegram_message_id into OutboundMessage
    Agent->>Ch: Send outbound response
    Ch->>TG: Post reply (use ReplyParameters: reply_to or telegram_message_id)
    Ch->>TG: Optionally set ✅ reaction on original message
    TG->>User: Deliver threaded reply and reactions
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped in quick with watchful eyes (👀),
Carried your message through looping skies,
I threaded replies back to their seed,
And left a tidy checkmark once done — indeed! ✅

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding reaction indicators (eyes and checkmark) for Telegram message processing. It is concise, clear, and directly reflects the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/agent/loop.rs`:
- Around line 240-244: The outbound metadata propagation currently copies only
"telegram_thread_id" in the direct tool publish paths, so ensure you also copy
"telegram_message_id" from inbound to outbound there; specifically, locate the
places that insert "telegram_thread_id" into outbound.metadata (the same scope
where inbound.metadata.get("telegram_thread_id") is read) and add the same
pattern used at the final/error/timeout path—check
inbound.metadata.get("telegram_message_id") and, if Some(mid), call
outbound.metadata.insert("telegram_message_id".to_string(), mid.clone()) so that
for_user tool messages preserve the Telegram message id for reply-thread
fallback and reaction updates.

In `@src/channels/telegram.rs`:
- Around line 1222-1232: The code sets the completion reaction (✅) immediately
after reading telegram_message_id, which can mark a message as processed even if
the outbound reply fails; move the bot.set_message_reaction call to run only
after the outgoing send succeeds—i.e., after the successful await of the send
function (the call around send_message at lines near where send_message is
awaited), so wrap or relocate the logic that checks
msg.metadata.get("telegram_message_id") and calls
bot.set_message_reaction(ChatId(chat_id), MessageId(mid)).reaction(...) to
execute only when the send_message future returns Ok; keep the same parsing of
mid_str.parse::<i32>() and error handling but perform it post-send to ensure the
✅ reaction is only applied on successful send.
- Around line 781-790: The reaction is being set unconditionally with
bot.set_message_reaction(...) (the 👀 emoji) before the early-return branches
(/model and /persona short-circuits and the if let Some(text) gate), which
leaves many inbound messages stuck at 👀; move the reaction setup so it only
runs when the message will reach the completion path (i.e., after the /model and
/persona short-circuits and after the if let Some(text) branch), or refactor by
calling the same completion helper used by the inline bot.send_message(...)
paths (the paths that currently add ✅ in send()) so both inline and async
completion flows reuse the same reaction logic instead of setting 👀
unconditionally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2173bad7-79c2-4a9c-bb56-431a48069768

📥 Commits

Reviewing files that changed from the base of the PR and between 10e2ad6 and 3692f96.

📒 Files selected for processing (2)
  • src/agent/loop.rs
  • src/channels/telegram.rs

Comment thread src/agent/loop.rs
Comment thread src/channels/telegram.rs Outdated
Comment on lines +781 to +790
// React with 👀 to acknowledge receipt before processing.
{
use teloxide::types::ReactionType;
let _ = bot
.set_message_reaction(msg.chat.id, msg.id)
.reaction(vec![ReactionType::Emoji {
emoji: "\u{1F440}".to_string(),
}])
.await;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Only add 👀 on paths that will hit the completion hook.

is only set in send(), but this block runs before the /model and /persona short-circuits and before the if let Some(text) gate. Messages handled inline at Lines 853-930, 934-1014, and 1084-1089 — plus unsupported updates that never enter Lines 800-803 — will leave the inbound message stuck at 👀.

Either move the reaction setup below the early-return branches or reuse the same completion helper from those inline bot.send_message(...) paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/channels/telegram.rs` around lines 781 - 790, The reaction is being set
unconditionally with bot.set_message_reaction(...) (the 👀 emoji) before the
early-return branches (/model and /persona short-circuits and the if let
Some(text) gate), which leaves many inbound messages stuck at 👀; move the
reaction setup so it only runs when the message will reach the completion path
(i.e., after the /model and /persona short-circuits and after the if let
Some(text) branch), or refactor by calling the same completion helper used by
the inline bot.send_message(...) paths (the paths that currently add ✅ in
send()) so both inline and async completion flows reuse the same reaction logic
instead of setting 👀 unconditionally.

Comment thread src/channels/telegram.rs Outdated
Comment on lines +1222 to +1232
// Clear the processing reaction (👀) set when the message was received.
if let Some(mid_str) = msg.metadata.get("telegram_message_id") {
if let Ok(mid) = mid_str.parse::<i32>() {
let _ = bot
.set_message_reaction(ChatId(chat_id), MessageId(mid))
.reaction(vec![ReactionType::Emoji {
emoji: "\u{2705}".to_string(),
}])
.await;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Set only after Telegram accepts the outbound message.

This marks the inbound message as complete before Line 1262 actually sends anything. If send_message fails (for example, bad HTML or a transport error), users will still see even though no reply was delivered.

🔧 Move the completion reaction after a successful send
-        // Clear the processing reaction (👀) set when the message was received.
-        if let Some(mid_str) = msg.metadata.get("telegram_message_id") {
-            if let Ok(mid) = mid_str.parse::<i32>() {
-                let _ = bot
-                    .set_message_reaction(ChatId(chat_id), MessageId(mid))
-                    .reaction(vec![ReactionType::Emoji {
-                        emoji: "\u{2705}".to_string(),
-                    }])
-                    .await;
-            }
-        }
-
         let rendered = render_telegram_html(&msg.content);
         let mut req = bot
             .send_message(ChatId(chat_id), rendered)
             .parse_mode(ParseMode::Html);
@@
         req.await
             .map_err(|e| ZeptoError::Channel(format!("Failed to send Telegram message: {}", e)))?;
+
+        if let Some(mid_str) = msg.metadata.get("telegram_message_id") {
+            if let Ok(mid) = mid_str.parse::<i32>() {
+                let _ = bot
+                    .set_message_reaction(ChatId(chat_id), MessageId(mid))
+                    .reaction(vec![ReactionType::Emoji {
+                        emoji: "\u{2705}".to_string(),
+                    }])
+                    .await;
+            }
+        }

Also applies to: 1262-1263

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/channels/telegram.rs` around lines 1222 - 1232, The code sets the
completion reaction (✅) immediately after reading telegram_message_id, which can
mark a message as processed even if the outbound reply fails; move the
bot.set_message_reaction call to run only after the outgoing send succeeds—i.e.,
after the successful await of the send function (the call around send_message at
lines near where send_message is awaited), so wrap or relocate the logic that
checks msg.metadata.get("telegram_message_id") and calls
bot.set_message_reaction(ChatId(chat_id), MessageId(mid)).reaction(...) to
execute only when the send_message future returns Ok; keep the same parsing of
mid_str.parse::<i32>() and error handling but perform it post-send to ensure the
✅ reaction is only applied on successful send.

@qhkm
Copy link
Copy Markdown
Owner

qhkm commented Mar 25, 2026

Thanks for this! #432 has been merged, so this needs a rebase.

A few things to address before merge:

  1. Rebase onto mainfeat(telegram): reply-to threading for outbound messages #432's commits are included here; after rebase the diff should only show the reaction logic
  2. Config gate — the 👀/✅ reactions fire on every message with no opt-out. Please add a config flag (e.g. telegram.reactions: true) so users can disable this
  3. Missing log line — the info!("Telegram: Sending message to chat {}", chat_id) log before bot init looks like it was accidentally removed
  4. Silent reaction failureslet _ = bot.set_message_reaction(...) is fine, but a debug! log on failure would help troubleshooting

stuartbowness and others added 2 commits March 25, 2026 11:16
React with eyes emoji when a message is received to give instant visual
feedback, then replace with a checkmark when the response is sent.
Reactions are best-effort — errors are silently ignored since the bot
may lack reaction permissions in some groups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `telegram.reactions` config flag (default: true) for opt-out
- Move 👀 after /model and /persona early returns so commands don't
  get stuck with eyes reaction
- Move ✅ after successful send_message so failed sends aren't marked
  as complete
- Restore removed log line before bot initialization
- Add debug logging on reaction failures instead of silent ignore
- Propagate telegram_message_id in tool for_user outbound paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@stuartbowness
Copy link
Copy Markdown
Contributor Author

All four items addressed:

  1. Rebase onto main — Branch is up to date with main (no duplicate feat(telegram): reply-to threading for outbound messages #432 commits). Diff is 57 lines across 3 files — reaction logic only.
  2. Config gatetelegram.reactions: bool added to TelegramConfig with #[serde(default = "default_telegram_reactions")] defaulting to true. Users can set "reactions": false to disable.
  3. Missing log lineinfo!("Telegram: Sending message to chat {}", chat_id) is present.
  4. Silent reaction failures — Both 👀 and ✅ reactions use if let Err(e) with debug! logging on failure.

CI is green (build, clippy, fmt, tests all pass). Ready for re-review.

@qhkm qhkm merged commit 00a0cb7 into qhkm:main Mar 26, 2026
14 checks passed
@qhkm
Copy link
Copy Markdown
Owner

qhkm commented Mar 26, 2026

Merged! Great turnaround @stuartbowness — all four review items addressed cleanly. The config gate and debug logging are exactly right. Nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants