Skip to content

feat(rendering): render richlink content type#2

Open
Zane Wang (zelinewang) wants to merge 6 commits into
photon-hq:mainfrom
zelinewang:spike/richlink-render
Open

feat(rendering): render richlink content type#2
Zane Wang (zelinewang) wants to merge 6 commits into
photon-hq:mainfrom
zelinewang:spike/richlink-render

Conversation

@zelinewang
Copy link
Copy Markdown

@zelinewang Zane Wang (zelinewang) commented May 1, 2026

Status: ready for review — companion to photon-hq/spectrum-ts#42.

Tracks #1: makes tuichat render the richlink content type that spectrum-ts#42 sends over the terminal protocol. Without this, a richlink message round-trips cleanly through the wire (no -32099, returns SendResult{id, timestamp}) but main renders a blank line.

What changed

File Change
PROTOCOL.md Document richlink in the public Content union spec
internal/protocol/types.go Add Url / Title / Summary / Cover to Content
internal/ui/messagelog.go Render richlink label, summary, and click-to-preview hint when cover image is present
internal/ui/entry_preview.go Reuse the existing attachment preview path for richlink cover images
internal/ui/mouse.go / internal/ui/view.go Mark richlinks with image covers as preview-click zones, same as attachments
internal/ui/select.go Render richlink in reply quote
internal/plain/plain.go Render richlink in non-TTY/plain mode
Tests Add focused stdlib tests for plain formatting, richlink cover unmarshal, and richlink cover preview decoding

Design question

Pattern A (this PR) vs Pattern B (Raw passthrough)?

This PR uses Pattern A: explicit richlink fields on Content. I considered Pattern B (richlink fields ride under c.Raw, parsed in renderer) because it aligns with the existing comment at internal/protocol/types.go:20-22:

"Contact fields deliberately passed through via Raw on the client side when needed. We don't model contact/voice extensively in Go today — they fall through as custom-like payloads."

Happy to switch to Pattern B if you prefer. For the current spectrum-ts#42 wire shape, Pattern A is the direct match: cover stays nested under the richlink payload and no spectrum-ts-side change is needed.

Cover preview

Implemented after Ryan's question on spectrum-ts#42. If a richlink has cover.bytes and a supported image MIME type, tuichat now shows the same (click to preview) affordance as attachments and opens the existing floating preview panel. This reuses the current Kitty/Ghostty image path and mosaic fallback; no new hover system or protocol shape is introduced.

Non-TTY/plain mode remains text-only: [link] Title — Summary.

Verification

  • go build ./... clean
  • go vet ./... clean
  • go test ./... -race clean
  • internal/plain: 5 richlink formatting cases pass
  • internal/protocol: nested cover.{mimeType, bytes} unmarshal test passes
  • internal/ui: richlink cover preview decode/reject tests pass
  • Backward compat: JSON field additions are wire-compatible by Go semantics
  • TCP-level E2E against spectrum-ts#42 with this branch's binary: richlink with cover returns SendResult, no -32099, stdout renders [link] E2E Test Article — Round-trip verification...
  • Controlled comparison from earlier iteration: same spectrum-ts#42 terminal provider against main returned message id but rendered blank, confirming the rendering bug is real

Caveats

  • Pattern B remains an open maintainer preference question
  • Cover preview requires cover.bytes and supported image MIME; unsupported/malformed cover payloads still render the text richlink

Cross-link: photon-hq/spectrum-ts#42 (wire-side companion), #1 (issue tracking this work)

Summary by CodeRabbit

  • New Features

    • Added richlink content type supporting URLs with titles, summaries, and optional cover images
    • Richlink previews (cover images) now display on hover or click
    • Richlinks can be quoted in message replies
  • Tests

    • Added comprehensive test coverage for richlink rendering, preview handling, and protocol unmarshalling

Companion to photon-hq/spectrum-ts#42. Adds Url / Title / Summary
fields to protocol.Content and case branches in the four renderer
switches (messagelog.go main + quoteBody, select.go reply-quote,
plain.go). Cover field deferred — kitty-protocol image rendering
is a separate design call.

Without these renderer cases, richlink messages round-trip cleanly
through the wire layer (SendResult{id, timestamp}, no -32099) but
display as blank in the TUI.

Tracks photon-hq#1.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Warning

Rate limit exceeded

@zelinewang has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 34 minutes and 38 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5fad0244-1e82-48d4-b5b0-1aca68ee461a

📥 Commits

Reviewing files that changed from the base of the PR and between 6c876b4 and 4f9ae8e.

📒 Files selected for processing (2)
  • internal/ui/entry_preview.go
  • internal/ui/entry_preview_test.go
📝 Walkthrough

Walkthrough

This PR introduces support for a new "richlink" content type across the protocol, plain-text rendering, and UI layers. It extends the Content type with URL, title, summary, and optional cover image fields, adds plain-text and UI rendering for richlinks, implements preview infrastructure to base64-decode and validate cover images, and integrates preview hints and interactions throughout the message log and reply quoting flows.

Changes

Richlink Content Type Support

Layer / File(s) Summary
Protocol & Types
PROTOCOL.md, internal/protocol/types.go
New richlink variant with url (required), title, summary, and optional cover (with mimeType and base64 bytes). New Cover struct defined.
Plain Text Rendering
internal/plain/plain.go
New case formats richlinks as "[link] <label>" or "[link] <label> — <summary>", with label derived from title or URL.
Preview Infrastructure
internal/ui/entry_preview.go
New helpers: richlinkLabel() extracts label, entrySupportsPreview() checks if richlink has valid cover with supported MIME, previewForEntry() base64-decodes cover bytes and constructs cache-keyed preview.
Message Log & Reply Rendering
internal/ui/messagelog.go, internal/ui/select.go, internal/ui/view.go
Updated to call unified entrySupportsPreview() instead of direct kitty checks; richlink rendering includes preview hint and optional summary line; reply quoting adds richlink case formatting as "[link] <label>".
Mouse Interaction
internal/ui/mouse.go
Refactored to use abstracted previewForEntry() for both attachments and richlinks; removed direct kitty dependency.
Tests
internal/plain/plain_test.go, internal/protocol/types_test.go, internal/ui/entry_preview_test.go
Added table-driven tests for plain rendering, JSON unmarshaling with cover, and preview construction (valid cases and rejection of missing/invalid covers).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • Issue #1: This PR directly implements richlink content type support with URL, title, summary, and cover fields, addressing the missing protocol definitions and UI rendering gaps described in that issue.

🐰 A rich link hops into view,
With covers that decode just right,
No preview left behind,
Base64 bytes take flight,
The UI dances with delight! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(rendering): render richlink content type' accurately and concisely summarizes the main feature addition: implementing rendering support for the richlink content type across the UI, plain-text, and protocol layers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 0/1 reviews remaining, refill in 34 minutes and 38 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Adds richlink to the public Content union spec. The wire shape mirrors
what spectrum-ts emits: required url, optional title/summary, optional
cover with mimeType + base64 bytes. Renderer support for cover is
implementation-defined and may be deferred (this PR defers cover
rendering — see PR description).

Closes documentation gap noted during PR photon-hq#2 review.
Removes the 'spike: explicit modeling vs Raw passthrough TBD' inline
comment. Design tradeoffs belong in the PR discussion, not in the
struct. Field names and JSON tags are self-documenting; PROTOCOL.md
is the canonical wire reference.
Adds the first test file in the repo: a stdlib table-driven test for
plainFormat's richlink branch. Covers:
- url only -> '[link] {url}'
- title without summary -> '[link] {title}'
- title with summary -> '[link] {title} — {summary}'
- summary without title falls back to url label
- missing url and title renders '[link] ' (cosmetic edge case)

Stdlib only, no third-party deps. Same-package test so plainFormat
stays unexported.
@zelinewang
Copy link
Copy Markdown
Author

Iteration since initial draft (3 atomic commits on top of the spike):

  • 13d7c56 docs(protocol): add richlink to public Content union spec — fixes wire/contract doc gap
  • a23af6c refactor(protocol): drop the inline (spike: ... TBD) marker comment
  • 19b62c6 test(plain): add internal/plain/plain_test.go — first test file in the repo, stdlib only, 5 table-driven cases scoped to the richlink branch

PR description updated to reflect current state. Still DRAFT pending direction on Pattern A vs B and cover rendering. go test ./... -race passes; controlled comparison vs main binary confirms the rendering bug is real (not cosmetic).

@zelinewang
Copy link
Copy Markdown
Author

Re-ran E2E against spectrum-ts#42 — patched binary renders [link] Title — Summary for the synthetic test and [link] Example Domain for a real https://example.com/ builder. Same wire against main binary: message id returned (no -32099) but stdout stays blank. Controlled comparison confirms the rendering bug is real, not cosmetic.

@zelinewang Zane Wang (zelinewang) marked this pull request as ready for review May 2, 2026 00:31
Copy link
Copy Markdown

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/ui/entry_preview.go`:
- Around line 40-44: The current CacheKey for the HoveredPreview uses
e.Content.Name which is not unique; change the CacheKey in the return of the
function creating &store.HoveredPreview to a stable unique identifier (for
example use e.AttachmentPath or a combination like e.AttachmentPath + ":" +
e.Content.Name or a content ID if available) so previews for different files
with the same filename do not collide when dispatchClick compares
preview.CacheKey; update the CacheKey field only (leave Name/Path as-is) and
ensure any downstream comparisons still use the new unique key.
- Around line 21-32: entrySupportsPreview currently advertises previews for
richlink covers based only on non-empty Bytes and MIME type, but previewForEntry
may reject malformed base64; update entrySupportsPreview (function
entrySupportsPreview, type store.LogEntry) to validate the cover payload by
attempting to base64-decode e.Content.Cover.Bytes and ensuring the decode
succeeds and yields non-zero-length data before returning true (in addition to
checking kitty.SupportedMimeType on e.Content.Cover.MimeType); this keeps the UI
affordance consistent with previewForEntry's behavior.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c10bd0f5-5ee7-4849-93cd-176187ce0b3e

📥 Commits

Reviewing files that changed from the base of the PR and between 89f2e89 and 6c876b4.

📒 Files selected for processing (11)
  • PROTOCOL.md
  • internal/plain/plain.go
  • internal/plain/plain_test.go
  • internal/protocol/types.go
  • internal/protocol/types_test.go
  • internal/ui/entry_preview.go
  • internal/ui/entry_preview_test.go
  • internal/ui/messagelog.go
  • internal/ui/mouse.go
  • internal/ui/select.go
  • internal/ui/view.go
📜 Review details
🔇 Additional comments (11)
internal/protocol/types.go (1)

23-31: Richlink payload fields are wired in cleanly.

The new Url/Title/Summary fields and nested Cover type line up with the protocol contract without introducing any obvious schema mismatches.

internal/protocol/types_test.go (1)

8-30: Good coverage for the nested cover shape.

This test verifies the new richlink.cover unmarshalling path and locks in the mimeType/bytes mapping added by the protocol change.

internal/plain/plain.go (1)

159-167: Richlink plain-mode formatting looks correct.

The new branch keeps the output predictable by preferring Title, falling back to Url, and appending Summary only when present.

internal/ui/view.go (1)

313-314: Preview zone gating now follows the shared predicate.

Using entrySupportsPreview(e) here keeps the view in sync with the richer preview rules, including the new richlink cover path.

internal/ui/select.go (1)

95-100: Richlink reply quoting is wired up consistently.

The new branch uses the same label fallback as the rest of the rendering code, so reply banners won’t drop richlink targets on the floor.

internal/plain/plain_test.go (1)

9-48: Nice, focused coverage for the new richlink formatting.

The table-driven cases exercise the fallback and summary combinations that matter for the new plain-mode branch.

internal/ui/messagelog.go (2)

53-82: Render richlinks and preview hints through the shared predicate.

This keeps the attachment hinting logic and the new richlink branch aligned with the shared preview support instead of duplicating MIME checks.


203-221: Richlink reply quotes stay consistent with the new label helper.

The quoteBody branch now emits the same link label shape used elsewhere, so reply quoting remains predictable for richlink entries.

PROTOCOL.md (1)

55-63: Protocol docs now match the new richlink wire shape.

The added richlink union member and nested cover object line up with the implementation and make the contract clear for adapters.

internal/ui/mouse.go (1)

188-203: Shared preview plumbing looks good.

Routing attachment/preview clicks through previewForEntry keeps the hover-preview behavior centralized and removes the old MIME-specific branching from mouse handling.

internal/ui/entry_preview_test.go (1)

12-85: Good coverage for the new richlink preview path.

The happy-path and rejection cases cover the important MIME/base64 branches, which should make regressions in previewForEntry easier to catch.

Comment thread internal/ui/entry_preview.go
Comment thread internal/ui/entry_preview.go
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