Skip to content

fix(telegram): render markdown as html#1667

Merged
zerob13 merged 1 commit into
devfrom
fix/telegram-markdown-render
May 25, 2026
Merged

fix(telegram): render markdown as html#1667
zerob13 merged 1 commit into
devfrom
fix/telegram-markdown-render

Conversation

@zhangmo8
Copy link
Copy Markdown
Collaborator

@zhangmo8 zhangmo8 commented May 25, 2026

Summary

  • Add convertMarkdownToTelegramHtml (mirrors the existing feishuMarkdown.ts pattern) that maps the Markdown subset our AI replies emit into Telegram's HTML subset (<b>, <i>, <s>, <code>, <pre>, <a>, <blockquote>).
  • Thread parseMode through TelegramClient.sendMessage / editMessageText / sendPhoto, and route every outbound chunk in telegramPoller through the converter with parse_mode: 'HTML'.
  • Auto-close dangling fenced blocks at chunk boundaries so 4096-char splits stay parseable; fall back to escaped plain text on any internal error.

Closes #1665

Test plan

  • pnpm test test/main/presenter/remoteControlPresenter/ — 224/224 pass (incl. 12 new telegramMarkdown cases + parseMode forwarding tests on client/poller).
  • pnpm run typecheck clean.
  • pnpm run lint 0 warnings.
  • pnpm run format / pnpm run i18n clean.
  • Manual: send a Telegram message containing **bold**, headings, a fenced ```ts code block, lists, and a link — verify rendering on a real Telegram client.

Summary by CodeRabbit

  • New Features

    • Telegram messages now support formatted text via Markdown-to-HTML conversion, including bold, italic, strikethrough, code blocks, links, lists, and blockquotes.
    • Added optional HTML rendering mode for message sending and editing in the remote control presenter.
  • Tests

    • Added comprehensive test coverage for Markdown-to-HTML conversion and message formatting functionality.

Review Change Stack

AI replies arrived as Markdown and were sent verbatim, so Telegram
clients showed raw `**bold**`, `# heading`, and fenced code blocks.

Add a local converter that maps the Markdown subset we emit to
Telegram's HTML subset (`<b>`, `<i>`, `<s>`, `<code>`, `<pre>`, `<a>`,
`<blockquote>`), thread `parseMode` through TelegramClient, and route
every outbound chunk in telegramPoller through the converter with
`parse_mode: 'HTML'`. Dangling fenced blocks at chunk boundaries are
auto-closed so 4096-char splits stay parseable.

Closes #1665
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

This PR implements Telegram markdown-to-HTML rendering for the remote control presenter. It extends TelegramClient with an optional parseMode parameter, adds a Markdown-to-Telegram-HTML converter, integrates the converter into TelegramPoller's message delivery flow, and updates all tests to verify parseMode propagation and conversion behavior.

Changes

Telegram Markdown-to-HTML Rendering Feature

Layer / File(s) Summary
Planning and specification documents
docs/issues/telegram-message-markdown-render/plan.md, spec.md, tasks.md
Documentation establishing requirements, design approach (behavior parity with Feishu pattern), and task tracking for Telegram markdown rendering.
TelegramClient API extension with parseMode
src/main/presenter/remoteControlPresenter/telegram/telegramClient.ts, test/main/presenter/remoteControlPresenter/telegramClient.test.ts
Introduces TelegramParseMode type ('HTML' | 'MarkdownV2'), extends sendMessage and sendPhoto with optional options.parseMode, and editMessageText with optional parseMode parameter. Requests forward parse_mode to Telegram API. Tests verify parseMode is correctly included in request payloads.
Markdown-to-Telegram-HTML converter implementation
src/main/presenter/remoteControlPresenter/telegram/telegramMarkdown.ts, test/main/presenter/remoteControlPresenter/telegramMarkdown.test.ts
Implements convertMarkdownToTelegramHtml(input) to transform markdown text into Telegram-compatible HTML subset. Handles HTML escaping, fenced and inline code blocks with optional language classes, markdown formatting (bold/italic/strikethrough), headings, links, lists, blockquotes, and auto-closes dangling code fences at chunk boundaries. Comprehensive tests cover all conversion patterns and error handling.
TelegramPoller message sending with converted HTML
src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts, test/main/presenter/remoteControlPresenter/telegramPoller.test.ts
Introduces sendChunk helper that applies convertMarkdownToTelegramHtml and sets parseMode: 'HTML' for all text sends. Updates syncDeliverySegment for streaming chunks, sendChunkedMessage for long-text splitting, and editMessageText for message edits. All test expectations updated to verify parseMode: 'HTML' is passed through the full message delivery and editing flow across multiple conversation patterns (streaming, chunking, pending interactions, callback queries, tool outputs).

Sequence Diagram(s)

sequenceDiagram
  participant Poller as TelegramPoller
  participant SendChunk as sendChunk helper
  participant Converter as convertMarkdownToTelegramHtml
  participant Client as TelegramClient
  participant API as Telegram API
  Poller->>SendChunk: sendChunk(markdownText, replyMarkup?)
  SendChunk->>Converter: convert markdown
  Converter-->>SendChunk: HTML text
  SendChunk->>Client: sendMessage(html, undefined, {parseMode: 'HTML'})
  Client->>API: POST /sendMessage with parse_mode: 'HTML'
  API-->>Client: {ok: true, result: {message_id}}
  Client-->>SendChunk: messageId
  SendChunk-->>Poller: messageId
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ThinkInAIXYZ/deepchat#1404: Modifies TelegramPoller pending-interaction message sending and editing paths, which overlap with the parseMode wiring added here.

  • ThinkInAIXYZ/deepchat#1435: Restructures TelegramPoller's process/answer/terminal segment delivery, which is affected by the new parseMode and converter wiring in this PR.

Poem

🐰 A telegram renderer fair,
Transforms markdown with careful care,
Bold, italic, code blocks shine,
HTML tags in every line,
Chunks stay safe at boundaries,
No fences break—hooray for thee!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "fix(telegram): render markdown as html" directly and concisely summarizes the main change: enabling Markdown rendering in Telegram messages via HTML conversion.
Linked Issues check ✅ Passed The PR implementation fully addresses issue #1665 requirements: converts Markdown to Telegram HTML, threads parseMode through TelegramClient methods, and applies conversion in TelegramPoller for all outbound messages.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #1665 objectives: Markdown-to-HTML conversion, parseMode threading, and TelegramPoller integration with supporting tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/telegram-markdown-render

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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
Contributor

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/issues/telegram-message-markdown-render/plan.md (1)

1-20: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move documentation to docs/features/ folder.

According to the coding guidelines, feature documentation should be in docs/features/<goal>/ while docs/issues/<goal>/ is reserved for bug fixes. Since issue #1665 is marked as "[FEATURE]" and adds new Markdown rendering capability, this documentation set should be located at:

  • docs/features/telegram-message-markdown-render/plan.md
  • docs/features/telegram-message-markdown-render/spec.md
  • docs/features/telegram-message-markdown-render/tasks.md

As per coding guidelines: Create specification-driven development documentation in kebab-case folders: docs/features/<goal>/ for new features, docs/issues/<goal>/ for bug fixes, docs/architecture/<goal>/ for refactors

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/issues/telegram-message-markdown-render/plan.md` around lines 1 - 20,
Move the feature docs out of docs/issues and into the feature docs folder:
create docs/features/telegram-message-markdown-render/ and relocate plan.md (and
any related spec.md and tasks.md) there, update the front-matter/links if any
refer to docs/issues paths, and ensure filenames and folder name use kebab-case
as described in the guidelines; keep the content and headings the same but
remove the docs/issues/telegram-message-markdown-render/* files to avoid
duplication and comply with the docs/features/<goal>/ convention for feature
work (issue `#1665`).
src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts (1)

648-654: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Chunk Telegram messages after Markdown→HTML conversion to respect Telegram’s 4096-char limit

syncDeliverySegment splits with chunkTelegramText using TELEGRAM_OUTBOUND_TEXT_LIMIT = 4096 on raw markdown (~648-654), but sendChunk then converts each chunk via convertMarkdownToTelegramHtml and sends with parseMode: 'HTML' (~727-744). The HTML conversion can expand content (escaping + inserted tags like <pre>, <code>, <a>, <blockquote>, etc.) and there’s no post-conversion length guard in telegramMarkdown.ts, so a “safe” raw chunk can become oversized and trigger Telegram 400 send/edit failures. Fix by chunking based on the final HTML string length (or using an HTML-aware conservative limit) before calling sendMessage/editMessageText.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts` around
lines 648 - 654, syncDeliverySegment currently chunks raw markdown via
chunkTelegramText (using TELEGRAM_OUTBOUND_TEXT_LIMIT) before calling sendChunk
which runs convertMarkdownToTelegramHtml, but HTML conversion can expand length
and exceed Telegram's 4096-char limit; update the flow so chunking happens on
the final HTML-safe string (or apply a conservative HTML-aware limit) before
calling sendChunk/sendMessage/editMessageText: convert each candidate chunk with
convertMarkdownToTelegramHtml and then re-chunk by measuring the resulting HTML
length (or reduce raw chunk size by a safe multiplier) to ensure each HTML
string is <= TELEGRAM_OUTBOUND_TEXT_LIMIT, keeping logic around
syncDeliverySegment, chunkTelegramText, sendChunk,
convertMarkdownToTelegramHtml, and the sendMessage/editMessageText calls
consistent.
🧹 Nitpick comments (1)
test/main/presenter/remoteControlPresenter/telegramClient.test.ts (1)

66-108: ⚡ Quick win

Add parse_mode coverage for sendPhoto as well.

sendPhoto was updated to forward parse_mode, but this suite only verifies sendMessage and editMessageText. Please add one multipart-form assertion for sendPhoto(..., { parseMode: 'HTML' }) to prevent regressions on that path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/remoteControlPresenter/telegramClient.test.ts` around
lines 66 - 108, Add a new test that calls TelegramClient.sendPhoto(..., {
parseMode: 'HTML' }) and asserts the mocked fetch call hits the '/sendPhoto'
endpoint and the multipart form body includes the caption and parse_mode fields;
locate tests near sendMessage/editMessageText in telegramClient.test.ts, create
a TelegramClient('token'), invoke sendPhoto with a caption containing HTML and
parseMode: 'HTML', then inspect vi.mocked(fetch).mock.calls[0] and verify the
body contains the multipart parts for name="caption" with the caption text and
name="parse_mode" with value "HTML" so parse_mode forwarding is covered for
sendPhoto.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/main/presenter/remoteControlPresenter/telegramMarkdown.test.ts`:
- Around line 65-67: The test name is misleading: it claims to verify the
fallback branch when conversion throws but the input 'plain <tag>' doesn't cause
an exception; either rename the test to reflect normal escaping behavior (e.g.,
change the it description to "returns escaped text for plain input" while
keeping the assertion against convertMarkdownToTelegramHtml('plain <tag>')), or
modify the test to force an internal throw by mocking the underlying converter
used by convertMarkdownToTelegramHtml (stub the converter to throw and then
assert the function returns the escaped fallback 'plain &lt;tag&gt;'); reference
convertMarkdownToTelegramHtml and the test file telegramMarkdown.test.ts when
making the change.

---

Outside diff comments:
In `@docs/issues/telegram-message-markdown-render/plan.md`:
- Around line 1-20: Move the feature docs out of docs/issues and into the
feature docs folder: create docs/features/telegram-message-markdown-render/ and
relocate plan.md (and any related spec.md and tasks.md) there, update the
front-matter/links if any refer to docs/issues paths, and ensure filenames and
folder name use kebab-case as described in the guidelines; keep the content and
headings the same but remove the docs/issues/telegram-message-markdown-render/*
files to avoid duplication and comply with the docs/features/<goal>/ convention
for feature work (issue `#1665`).

In `@src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts`:
- Around line 648-654: syncDeliverySegment currently chunks raw markdown via
chunkTelegramText (using TELEGRAM_OUTBOUND_TEXT_LIMIT) before calling sendChunk
which runs convertMarkdownToTelegramHtml, but HTML conversion can expand length
and exceed Telegram's 4096-char limit; update the flow so chunking happens on
the final HTML-safe string (or apply a conservative HTML-aware limit) before
calling sendChunk/sendMessage/editMessageText: convert each candidate chunk with
convertMarkdownToTelegramHtml and then re-chunk by measuring the resulting HTML
length (or reduce raw chunk size by a safe multiplier) to ensure each HTML
string is <= TELEGRAM_OUTBOUND_TEXT_LIMIT, keeping logic around
syncDeliverySegment, chunkTelegramText, sendChunk,
convertMarkdownToTelegramHtml, and the sendMessage/editMessageText calls
consistent.

---

Nitpick comments:
In `@test/main/presenter/remoteControlPresenter/telegramClient.test.ts`:
- Around line 66-108: Add a new test that calls TelegramClient.sendPhoto(..., {
parseMode: 'HTML' }) and asserts the mocked fetch call hits the '/sendPhoto'
endpoint and the multipart form body includes the caption and parse_mode fields;
locate tests near sendMessage/editMessageText in telegramClient.test.ts, create
a TelegramClient('token'), invoke sendPhoto with a caption containing HTML and
parseMode: 'HTML', then inspect vi.mocked(fetch).mock.calls[0] and verify the
body contains the multipart parts for name="caption" with the caption text and
name="parse_mode" with value "HTML" so parse_mode forwarding is covered for
sendPhoto.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e4cc9a1-88ff-4b4c-b698-ece2e4b2605d

📥 Commits

Reviewing files that changed from the base of the PR and between 0cd0821 and c7f1e06.

📒 Files selected for processing (9)
  • docs/issues/telegram-message-markdown-render/plan.md
  • docs/issues/telegram-message-markdown-render/spec.md
  • docs/issues/telegram-message-markdown-render/tasks.md
  • src/main/presenter/remoteControlPresenter/telegram/telegramClient.ts
  • src/main/presenter/remoteControlPresenter/telegram/telegramMarkdown.ts
  • src/main/presenter/remoteControlPresenter/telegram/telegramPoller.ts
  • test/main/presenter/remoteControlPresenter/telegramClient.test.ts
  • test/main/presenter/remoteControlPresenter/telegramMarkdown.test.ts
  • test/main/presenter/remoteControlPresenter/telegramPoller.test.ts

Comment on lines +65 to +67
it('returns escaped text when conversion throws', () => {
expect(convertMarkdownToTelegramHtml('plain <tag>')).toBe('plain &lt;tag&gt;')
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test name doesn’t match what it verifies.

The case does not force convertMarkdownToTelegramHtml to throw, so it doesn’t exercise the catch/fallback branch. Either rename it to plain escaping behavior or mock an internal throw path and assert fallback output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/remoteControlPresenter/telegramMarkdown.test.ts` around
lines 65 - 67, The test name is misleading: it claims to verify the fallback
branch when conversion throws but the input 'plain <tag>' doesn't cause an
exception; either rename the test to reflect normal escaping behavior (e.g.,
change the it description to "returns escaped text for plain input" while
keeping the assertion against convertMarkdownToTelegramHtml('plain <tag>')), or
modify the test to force an internal throw by mocking the underlying converter
used by convertMarkdownToTelegramHtml (stub the converter to throw and then
assert the function returns the escaped fallback 'plain &lt;tag&gt;'); reference
convertMarkdownToTelegramHtml and the test file telegramMarkdown.test.ts when
making the change.

@zerob13 zerob13 merged commit d5bae13 into dev May 25, 2026
3 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.

[FEATURE] Telegram 消息支持 Markdown 渲染

2 participants