From 1d998889c85c1204b5b21329213d19e6837c184e Mon Sep 17 00:00:00 2001 From: tony-chinchill Date: Thu, 14 May 2026 17:54:32 -0700 Subject: [PATCH 1/2] fix(gchat): use full markdown parser for card text rendering The _markdown_to_gchat helper only converted **bold** to *bold* via a naive regex, leaving markdown bullet lists and italic syntax as raw literals in textParagraph widgets. Google Chat does not render markdown list syntax (- item, * item), so agent responses with bullet lists appeared with literal * characters instead of formatted bullets. Replace _markdown_to_gchat with _render_markdown_as_gchat which round-trips content through parse_markdown + GoogleChatFormatConverter, the same path used for plain text messages. This correctly converts: - markdown lists -> bullet characters - **bold** -> *bold* (GChat bold) - *italic* -> _italic_ (GChat italic) Apply the fix to both _convert_text_to_widget and _convert_fields_to_widgets. Update the test that asserted old pass-through behavior for *italic* to reflect the correct output. Co-Authored-By: Claude Sonnet 4.6 --- src/chat_sdk/adapters/google_chat/cards.py | 31 +++++++++------------- tests/test_gchat_cards.py | 16 ++++++++--- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/chat_sdk/adapters/google_chat/cards.py b/src/chat_sdk/adapters/google_chat/cards.py index c8205cf..a836a64 100644 --- a/src/chat_sdk/adapters/google_chat/cards.py +++ b/src/chat_sdk/adapters/google_chat/cards.py @@ -10,6 +10,7 @@ from typing import Any, cast +from chat_sdk.adapters.google_chat.format_converter import GoogleChatFormatConverter from chat_sdk.cards import ( CardChild, CardElement, @@ -18,10 +19,18 @@ ) from chat_sdk.shared import card_to_fallback_text as shared_card_to_fallback_text from chat_sdk.shared import create_emoji_converter +from chat_sdk.shared.base_format_converter import parse_markdown # Convert emoji placeholders in text to GChat format (Unicode). convert_emoji = create_emoji_converter("gchat") +_gchat_converter = GoogleChatFormatConverter() + + +def _render_markdown_as_gchat(text: str) -> str: + """Parse standard markdown and render as Google Chat formatted text.""" + return _gchat_converter.from_ast(parse_markdown(text)) + def card_to_google_card( card: CardElement, @@ -148,26 +157,12 @@ def _convert_child_to_widgets( return [] -def _markdown_to_gchat(text: str) -> str: - """Convert standard Markdown formatting to Google Chat formatting. - - **bold** -> *bold* - """ - import re - - return re.sub(r"\*\*(.+?)\*\*", r"*\1*", text) - - def _convert_text_to_widget(element: dict[str, Any]) -> dict[str, Any]: """Convert a text element to a widget.""" - text = _markdown_to_gchat(convert_emoji(element.get("content", ""))) + text = _render_markdown_as_gchat(convert_emoji(element.get("content", ""))) - style = element.get("style") - if style == "bold": + if element.get("style") == "bold": text = f"*{text}*" - elif style == "muted": - # GChat doesn't have muted, use regular text - text = convert_emoji(element.get("content", "")) return {"textParagraph": {"text": text}} @@ -302,8 +297,8 @@ def _convert_fields_to_widgets( return [ { "decoratedText": { - "topLabel": _markdown_to_gchat(convert_emoji(field.get("label", ""))), - "text": _markdown_to_gchat(convert_emoji(field.get("value", ""))), + "topLabel": _render_markdown_as_gchat(convert_emoji(field.get("label", ""))), + "text": _render_markdown_as_gchat(convert_emoji(field.get("value", ""))), }, } for field in element.get("children", []) diff --git a/tests/test_gchat_cards.py b/tests/test_gchat_cards.py index e1a4dc7..ceebceb 100644 --- a/tests/test_gchat_cards.py +++ b/tests/test_gchat_cards.py @@ -304,11 +304,13 @@ def test_converts_multiple_bold_segments(self): widgets = gchat_card["card"]["sections"][0]["widgets"] assert widgets[0]["textParagraph"]["text"] == "*Project*: my-app, *Status*: active" - def test_preserves_single_asterisk(self): - card = Card(children=[CardText("Already *bold* in GChat format")]) + def test_single_asterisk_markdown_italic_becomes_gchat_italic(self): + # *text* is markdown italic; the full converter renders it as _text_ (GChat italic), + # not *text* (GChat bold). Card content is always treated as markdown. + card = Card(children=[CardText("The *italic* word")]) gchat_card = card_to_google_card(card) widgets = gchat_card["card"]["sections"][0]["widgets"] - assert widgets[0]["textParagraph"]["text"] == "Already *bold* in GChat format" + assert widgets[0]["textParagraph"]["text"] == "The _italic_ word" def test_converts_bold_in_field_values(self): card = Card(children=[Fields([Field(label="Status", value="**Active**")])]) @@ -330,6 +332,14 @@ def test_plain_text_no_change(self): widgets = gchat_card["card"]["sections"][0]["widgets"] assert widgets[0]["textParagraph"]["text"] == "Plain text" + def test_markdown_bullet_list_with_bold_labels_renders_without_raw_markers(self): + # Agent responses use markdown lists with bold labels. The naive **→* regex + # left list markers and asterisks literal in textParagraph, rendering as "* *Jira:*". + card = Card(children=[CardText("- **Jira:** create issues\n- **Zendesk:** manage tickets")]) + text = card_to_google_card(card)["card"]["sections"][0]["widgets"][0]["textParagraph"]["text"] + assert "• " in text, "markdown list items must render as • bullets, not raw '- item'" + assert "**" not in text, "markdown **bold** must be converted to GChat *bold*" + # --------------------------------------------------------------------------- # CardLink From 36a7e1319fdf887802302591a5088e4e521e298c Mon Sep 17 00:00:00 2001 From: tony-chinchill Date: Fri, 15 May 2026 10:17:12 -0700 Subject: [PATCH 2/2] fix(gchat): restore muted style handling dropped in markdown converter refactor Upstream TS convertTextToWidget explicitly reverts muted elements to plain emoji-only text, bypassing markdown conversion. The AST-converter refactor dropped the elif branch, causing muted card text to go through parse_markdown + GoogleChatFormatConverter.from_ast instead. Co-Authored-By: Claude Sonnet 4.6 --- src/chat_sdk/adapters/google_chat/cards.py | 2 ++ tests/test_gchat_cards.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/chat_sdk/adapters/google_chat/cards.py b/src/chat_sdk/adapters/google_chat/cards.py index a836a64..820d7df 100644 --- a/src/chat_sdk/adapters/google_chat/cards.py +++ b/src/chat_sdk/adapters/google_chat/cards.py @@ -163,6 +163,8 @@ def _convert_text_to_widget(element: dict[str, Any]) -> dict[str, Any]: if element.get("style") == "bold": text = f"*{text}*" + elif element.get("style") == "muted": + text = convert_emoji(element.get("content", "")) return {"textParagraph": {"text": text}} diff --git a/tests/test_gchat_cards.py b/tests/test_gchat_cards.py index ceebceb..a2cd11d 100644 --- a/tests/test_gchat_cards.py +++ b/tests/test_gchat_cards.py @@ -340,6 +340,13 @@ def test_markdown_bullet_list_with_bold_labels_renders_without_raw_markers(self) assert "• " in text, "markdown list items must render as • bullets, not raw '- item'" assert "**" not in text, "markdown **bold** must be converted to GChat *bold*" + def test_muted_style_skips_markdown_conversion(self): + # Upstream TS explicitly reverts muted elements to plain emoji-only text. + # The full markdown converter must not run on muted content. + card = Card(children=[CardText("**bold text**", style="muted")]) + widgets = card_to_google_card(card)["card"]["sections"][0]["widgets"] + assert widgets[0]["textParagraph"]["text"] == "**bold text**" + # --------------------------------------------------------------------------- # CardLink