feat(telegram): first-token = new message, delete Thinking on done, register / commands#37
Conversation
Three coordinated changes to the Telegram platform:
1. First-token streams as a new message (not an edit of "Thinking…").
The streaming task no longer seeds current_msg_id from the placeholder;
the first token always triggers a fresh send_message. This avoids the
placeholder visually morphing mid-response into the first chunk.
2. "Thinking…" placeholder is deleted once the response is delivered.
Cleanup happens in handle_message after stream_handle.await, regardless
of success/error path. Best-effort: failures are logged but do not block
reporting the actual outcome.
3. Slash commands are registered with Telegram at bot startup via
set_my_commands(), so clients display them in the "/" menu. The
existing manual command dispatch is unchanged. /query-rewrite was
renamed to /queryrewrite to satisfy Telegram's BotCommand naming
rules ([a-z0-9_]{1,32}); the legacy form remains accepted as an
alias for back-compat.
Adds 4 new unit tests (10 → 11 platform::telegram tests pass) covering
the placeholder handoff, the cleanup, and the command list invariants
(name regex + length + non-empty description).
Co-authored-by: chinkan.ai <chinkan@gmail.com>
|
Claude finished @chinkan's task in 1m 52s —— View job Code Review
Overall this is a clean, well-reasoned UX improvement. The three changes compose correctly and the sequencing through the async machinery is sound. One minor inefficiency and a couple of worth-noting observations below. Minor Issue:
|
|
Claude finished @chinkan's task in 1m 51s —— View job Code Review
This PR is in good shape. The new commit ( Previous issue: resolved ✅
let commands = supported_commands();
let count = commands.len();
match bot.set_my_commands(commands).await {
Ok(_) => info!("Registered {} Telegram commands", count),
Err(e) => warn!(error = %e, "Failed to register Telegram commands"),
}Exactly what was suggested. No second allocation. Change 1: First token delivered as a new messageLine 331 — let mut current_msg_id: Option<teloxide::types::MessageId> = None;The streaming task is entirely self-contained: it creates its own first message from the first token, never touching the placeholder. The ownership separation between the placeholder (owned by Subtle correctness point: On the error path, Change 2: Placeholder deletionLines 455–459 — deletion is best-effort, happens unconditionally after streaming completes: if let Some(placeholder_id) = placeholder_msg_id {
if let Err(e) = bot.delete_message(msg.chat.id, placeholder_id).await {
tracing::warn!(error = %e, "Failed to delete thinking placeholder");
}
}The error path sequence is sound: One minor thing worth knowing (no action required): the placeholder is visible for the full duration of streaming — there are two simultaneous bubbles (placeholder + growing response) until streaming finishes. The PR acknowledges this trade-off; it's intentional. Change 3: Slash command registration
if text == "/queryrewrite" || text == "/query-rewrite" {No issues here. The registration failure is correctly treated as non-fatal (network down at startup shouldn't stop the bot). TestsAll four new tests are sound:
SummaryApprove. The previous review's only actionable item is resolved, all three UX changes are correctly implemented, error paths are safe, and the tests are fit for purpose. Ready to merge. |
|
Claude finished @chinkan's task in 1m 28s —— View job Code Review
Previous Review Issue: Resolved ✓The double let commands = supported_commands();
let count = commands.len();
match bot.set_my_commands(commands).await {
Ok(_) => info!("Registered {} Telegram commands", count),
Err(e) => warn!(error = %e, "Failed to register Telegram commands"),
}No redundant allocation. Clean. Correctness & LogicError path sequencing ( The abort-then-await-then-cleanup sequence is sound: Err(e) => {
stream_handle.abort(); // signal cancellation
Err(e)
}
// ...
stream_handle.await.ok(); // swallow JoinError::Cancelled — correct
// delete placeholder
// send error reply
First-token-as-new-message (line 331)
Back-compat alias (line 225) if text == "/queryrewrite" || text == "/query-rewrite" {Both forms accepted. Only the canonical Minor Observations (no action required)Placeholder visible alongside streaming content For the entire duration of streaming there are two items visible: "⏳ Thinking..." and the growing response. The placeholder is deleted only after 500 ms throttle gap on first token The first streaming message is not sent until Empty-response silent-success case (pre-existing) If the LLM produces zero tokens and TestsThe four new tests are well-constructed:
One small note: SummaryApprove. The double |
Summary
Three small UX improvements to the Telegram platform:
First response token is delivered as a new message instead of editing the "⏳ Thinking..." placeholder. Previously the placeholder bubble was edited in place with the first tokens, causing it to visually morph mid-stream. Now
current_msg_idstarts asNonein the streaming task and the placeholder is owned byhandle_message."Thinking..." placeholder is deleted once the response is delivered. Cleanup happens in
handle_messageafterstream_handle.await, on both success and error paths. Best-effort: failures are logged but do not block reporting the actual outcome.Slash commands are published to Telegram at startup via
bot.set_my_commands(...), so they appear in the "/" menu in Telegram clients. Existing manual command dispatch is unchanged. The legacy/query-rewritewas renamed to/queryrewriteto satisfy Telegram'sBotCommandnaming rules ([a-z0-9_]{1,32}— no hyphens). The old form remains accepted as an alias for back-compat.Files changed
src/platform/telegram.rs— only file touched (+134 / -9)Tests
11/11
platform::telegramtests pass (4 new):test_first_token_does_not_inherit_placeholder_msg_id— source-inspection guard against re-introducing the placeholder seeding.test_placeholder_is_deleted_after_streaming— confirms the cleanup branch is present.test_supported_commands_lists_user_visible_commands— validates each registered command name matches Telegram's regex ([a-z0-9_]{1,32}), has non-empty description, and the canonical set of 6 commands is present.test_stream_handle_does_not_require_placeholder_sendstill passes.)Verification
cargo fmt --all -- --check— clean.cargo test— 214 passed, 0 failed.cargo clippy -- -D warnings(matches CI) — clean.Notes
stream_handle.abort()runs first, thenstream_handle.await.ok()swallows the cancellation error, thendelete_messageruns.cargo clippy --all-targets -- -D warningsflags 3 pre-existing unrelated test-only warnings onmain(useless_vecinagent.rstest,unused_imports: super::*in two memory tests). Out of scope for this PR.