Skip to content

Google Chat adapter emits redundant <mailto:foo|foo> / <tel:n|n> tokens for autolinked emails and phone numbers #516

@taslim

Description

@taslim

Bug Description

packages/adapter-gchat/src/markdown.ts (nodeToGChat, lines 108–116) collapses link nodes to a bare URL when the link's display text equals the URL — but only for plain http(s):// URLs. For mailto: and tel: links, the URL carries a scheme prefix (mailto:foo@bar.com) that the visible text doesn't (foo@bar.com), so the equality check linkText === node.url never fires, and the adapter emits the verbose <scheme:url|display> form even though display text adds no information.

This is most visible when an agent or upstream message contains plain email addresses. remark-gfm's autolink-literal extension converts bare emails to link nodes [foo@bar.com](mailto:foo@bar.com), and the adapter then ships them to Google Chat as <mailto:foo@bar.com|foo@bar.com>. In some Google Chat clients these render correctly as a clickable email; in others (and consistently when the message text is round-tripped via quotedMessageSnapshot.text, copied to clipboard, or read back by spaces.messages.get), the raw token surfaces in the user-facing text.

The current code at markdown.ts:108–116:

if (isLinkNode(node)) {
  const linkText = getNodeChildren(node)
    .map((child) => this.nodeToGChat(child))
    .join('');
  if (linkText === node.url) {
    return node.url;
  }
  return `<${node.url}|${linkText}>`;
}

Steps to Reproduce

import { createGoogleChatAdapter } from '@chat-adapter/gchat';

const adapter = createGoogleChatAdapter({ /* … */ });
await adapter.postMessage('gchat:spaces/X:thread', {
  markdown: 'invite sent to hello@example.com',
});

Expected Behavior

The wire-level message GChat receives should be:

invite sent to hello@example.com

(GChat's own renderer auto-links bare email addresses at display time — no <mailto:…|…> wrapping needed when display text == URL tail.)

Actual Behavior

The wire-level message is:

invite sent to <mailto:hello@example.com|hello@example.com>

Same applies for <tel:+15551234|+15551234> when the agent writes a bare phone number that GFM autolinks. The nodeToGChat bare-URL optimization at line 113 should also fire for these schemes.

Suggested Fix

Extend the bare-URL collapse to recognise that for mailto: and tel: scheme URLs, the "bare display" form is the URL with its scheme prefix stripped:

if (isLinkNode(node)) {
  const linkText = getNodeChildren(node)
    .map((child) => this.nodeToGChat(child))
    .join('');
  if (linkText === node.url) return node.url;
  // mailto:/tel: autolinks: GChat renders the bare address as a clickable link.
  // The display-text form is the URL minus the scheme.
  const schemeMatch = node.url.match(/^(mailto|tel):(.+)$/);
  if (schemeMatch && schemeMatch[2] === linkText) return linkText;
  return `<${node.url}|${linkText}>`;
}

Happy to send a PR with the change + a test in markdown.test.ts if the direction looks right.

Chat SDK Version

4.26.0

Node.js Version

23.10

Platform Adapter

Google Chat

Operating System

macOS

Additional Context

Related precedent: #392 (Slack adapter mention regex mangles emails) — same general class of "plain email addresses in agent text need careful handling in adapter rendering."

Currently working around this in our codebase by monkey-patching formatConverter.renderPostable to post-process the rendered output and collapse <mailto:X|X> / <tel:X|X> / <https?://X|X> tokens where the display equals the URL tail. Happy to upstream the equivalent logic via the nodeToGChat change above so the workaround can be removed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions