Skip to content

fix(telegram): file round-trip + clickable permission prompts (#84)#85

Merged
chauncygu merged 1 commit intomainfrom
feature/tab-accept-ghost-suggestion
May 5, 2026
Merged

fix(telegram): file round-trip + clickable permission prompts (#84)#85
chauncygu merged 1 commit intomainfrom
feature/tab-accept-ghost-suggestion

Conversation

@chauncygu
Copy link
Copy Markdown
Contributor

Two missing code paths in bridges/telegram.py produced both halves of issue #84: the bridge had only _tg_send (text via sendMessage), so (1) the agent claimed to "send a file" but only emitted text — even with --accept-all there was no sendDocument helper, no inbound document handler, and no path for the agent to emit a file; and (2) permission prompts arrived as text containing [y/N/a(ccept-all)] that looked like buttons but weren't, because the poll loop only listened for message updates and there was no inline-keyboard render.

bridges/telegram.py

  • _tg_send_document(token, chat_id, file_path, caption=None) — manual multipart/form-data upload, 49 MB cap (Telegram hard limit is 50 MB; headroom for encoding overhead). Six failure modes report specific errors to chat: missing, stat failure, empty, oversize, network exception, API ok=false.
  • Inbound document handler in tg_poll_loop — getFile download, filename sanitized to [A-Za-z0-9.-]_, saved to /workspace if mounted else tempfile.gettempdir(), path echoed to chat, then a path-aware prompt submitted to the agent (caption-overridable).
  • !sendfile <absolute_path> user command — explicit on-demand send in a daemon thread so the poll loop does not block.
  • Auto-send on Write — _on_tool_start records the in-flight file_path; _on_tool_end mails it (FIFO-paired). Skipped on Error: / Denied: results, de-duplicated per turn via _sent_files.
  • _tg_send_keyboard(...) — sendMessage with reply_markup inline_keyboard. Three-layer fallback: Markdown -> no parse_mode same keyboard -> plain text no keyboard.
  • _handle_callback_query(...) — extracted for testability. Auth check (chat_id), answerCallbackQuery to clear click spinner, prompt_id validation (stale clicks dropped silently), editMessageText appends "✓ Selected: " for scroll-back, fires tg_input_event with the value to unblock the agent.
  • Poll loop allowed_updates widened to ["message", "callback_query"].

tools/interaction.py

  • ask_input_interactive grows options=[(label, value), ...] parameter. Telegram branch renders inline_keyboard when options non-empty; Slack / WeChat / terminal ignore options and keep using the text path. Event + prompt_id are registered before the keyboard send so a fast click cannot race in.

cheetahclaws.py

  • ask_permission_interactive passes [("✅ Approve","y"), ("❌ Reject","n"), ("✅✅ Accept all","a")].

runtime.py

  • RuntimeContext adds tg_callback_prompt_id: str and tg_callback_message_id: int.

tests/test_telegram_bridge.py

  • 27 new pytest cases. urllib.request.urlopen and _tg_api are mocked; threading.Thread is monkeypatched to a synchronous stub for the auto-send hook; an end-to-end test drives ask_input_interactive(options=...) from a worker thread, simulates a click via _handle_callback_query, and asserts the worker returns the clicked value.

docs/guides/bridges.md, docs/news.md, README.md, docs/README.CN.MD

Tests: 696 passed in 43s (was 669 before this change).

Two missing code paths in bridges/telegram.py produced both halves of
issue #84: the bridge had only `_tg_send` (text via sendMessage), so
(1) the agent claimed to "send a file" but only emitted text — even
with --accept-all there was no sendDocument helper, no inbound
`document` handler, and no path for the agent to emit a file; and
(2) permission prompts arrived as text containing `[y/N/a(ccept-all)]`
that looked like buttons but weren't, because the poll loop only
listened for `message` updates and there was no inline-keyboard render.

bridges/telegram.py
  - _tg_send_document(token, chat_id, file_path, caption=None) —
    manual multipart/form-data upload, 49 MB cap (Telegram hard
    limit is 50 MB; headroom for encoding overhead). Six failure
    modes report specific errors to chat: missing, stat failure,
    empty, oversize, network exception, API ok=false.
  - Inbound `document` handler in _tg_poll_loop — getFile download,
    filename sanitized to [A-Za-z0-9._-]_, saved to /workspace if
    mounted else tempfile.gettempdir(), path echoed to chat, then a
    path-aware prompt submitted to the agent (caption-overridable).
  - !sendfile <absolute_path> user command — explicit on-demand
    send in a daemon thread so the poll loop does not block.
  - Auto-send on Write — _on_tool_start records the in-flight
    file_path; _on_tool_end mails it (FIFO-paired). Skipped on
    Error: / Denied: results, de-duplicated per turn via _sent_files.
  - _tg_send_keyboard(...) — sendMessage with reply_markup
    inline_keyboard. Three-layer fallback: Markdown -> no parse_mode
    same keyboard -> plain text no keyboard.
  - _handle_callback_query(...) — extracted for testability. Auth
    check (chat_id), answerCallbackQuery to clear click spinner,
    prompt_id validation (stale clicks dropped silently),
    editMessageText appends "✓ Selected: <value>" for scroll-back,
    fires tg_input_event with the value to unblock the agent.
  - Poll loop allowed_updates widened to
    ["message", "callback_query"].

tools/interaction.py
  - ask_input_interactive grows options=[(label, value), ...]
    parameter. Telegram branch renders inline_keyboard when options
    non-empty; Slack / WeChat / terminal ignore options and keep
    using the text path. Event + prompt_id are registered before
    the keyboard send so a fast click cannot race in.

cheetahclaws.py
  - ask_permission_interactive passes
    [("✅ Approve","y"), ("❌ Reject","n"), ("✅✅ Accept all","a")].

runtime.py
  - RuntimeContext adds tg_callback_prompt_id: str and
    tg_callback_message_id: int.

tests/test_telegram_bridge.py
  - 27 new pytest cases. urllib.request.urlopen and _tg_api are
    mocked; threading.Thread is monkeypatched to a synchronous stub
    for the auto-send hook; an end-to-end test drives
    ask_input_interactive(options=...) from a worker thread,
    simulates a click via _handle_callback_query, and asserts the
    worker returns the clicked value.

docs/guides/bridges.md, docs/news.md, README.md, docs/README.CN.MD
  - New "Permission prompts (clickable buttons)" and "File support"
    sections; news entry covering both halves of #84.

Tests: 696 passed in 43s (was 669 before this change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chauncygu chauncygu merged commit 1433b0e into main May 5, 2026
6 checks passed
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.

1 participant