Skip to content

docs cat drops internal link targets on text runs (link.tabId / bookmarkId / headingId) in all output modes; readable only via --raw #921

Description

@neo-wanderer

Summary

docs cat renders a text run that carries an internal link (TextRun.TextStyle.Link.TabId, and equivalently .BookmarkId / .HeadingId) as bare text, silently dropping the link target in every non---raw mode (default, --plain, --chips, --json). The destination is only recoverable from --raw.

This is a follow-up in addition to #917 / #920. That work covers external URL links on text runs (Link.Url). Internal links are a distinct, still-open case: #920 handles Link.Url only and explicitly scopes out internal links, so this gap remains after it merges.

Why this is a consistency gap, not a new feature

A rich-link smart chip that points at a document tab already renders as a deep link today:

See details → [Details section](https://docs.google.com/document/d/DOC_ID/edit?tab=t.bbb)

But an ordinary text run pointing at the same tab via Link.TabId renders as plain text with no target:

See details

Same destination, two authoring styles, two different outputs. The deep-link rendering the chip path already produces can be reused for internal text-run links.

Minimal repro (synthetic documents.get shape)

A document DOC_ID with two tabs, Overview (t.aaa) and Details (t.bbb). A paragraph on the Overview tab contains one text-run internal link and one rich-link chip, both targeting t.bbb:

{ "paragraph": { "elements": [
  { "startIndex": 1, "endIndex": 12, "textRun": {
      "content": "See details",
      "textStyle": { "link": { "tabId": "t.bbb" } } } },
  { "startIndex": 12, "endIndex": 13, "textRun": { "content": " " } },
  { "startIndex": 13, "endIndex": 14, "richLink": {
      "richLinkProperties": {
        "title": "Details section",
        "uri": "https://docs.google.com/document/d/DOC_ID/edit?tab=t.bbb" } } },
  { "startIndex": 14, "endIndex": 15, "textRun": { "content": "\n" } }
] } }

Current output (docs cat --chips, v0.34.0; unchanged by #920)

See details [Details section](https://docs.google.com/document/d/DOC_ID/edit?tab=t.bbb)

The text-run link (See detailst.bbb) is dropped; only the chip renders.

Expected

[See details](https://docs.google.com/document/d/DOC_ID/edit?tab=t.bbb) [Details section](https://docs.google.com/document/d/DOC_ID/edit?tab=t.bbb)

(under the opt-in --chips rendered path, matching #920's behavior for external links; default/--plain text unchanged).

Root cause

internal/cmd/docs_read.go, function appendDocsElementRenderedText (paragraph/text-run branch, ~L425–427 on main) appends p.TextRun.Content without inspecting p.TextRun.TextStyle.Link. #920 adds a Link.Url check here but not Link.TabId / Link.BookmarkId / Link.HeadingId. The deep-link markdown format is already constructed in the same file's renderDocsSmartChip, case p.RichLink != nil block (~L519–541), building [title](uri) from RichLinkProperties.Uri.

Proposed fix

Note that, unlike rich links (where the API supplies a full Uri), internal links expose only an ID, so gog must synthesize the deep link from the document ID plus the id:

  • Primary — Link.TabId: render [<text>](https://docs.google.com/document/d/<docId>/edit?tab=<tabId>), reusing the existing chip deep-link format. This is unambiguous and mirrors what the rich-link path already emits.
  • Secondary — Link.BookmarkId / Link.HeadingId: fuzzier (need anchor/heading resolution); can be handled separately or, at minimum, surfaced as structured metadata in --json so the target is not silently lost.

Severity / notes

Lower priority than #917: this is not data loss — the tabId (etc.) is present in --raw, and a tab id resolves to a tab already listed in the same documents.get response. It is a rendering-parity gap in the opt-in path, tractable with the format the codebase already has.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:ux-frictionUser-facing flow adds avoidable confusion or support burden without fully blocking progress.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions