Skip to content

feat(ios): tap-to-expand link previews in chat transcript#101198

Merged
steipete merged 1 commit into
mainfrom
feat/ios-link-previews-nash
Jul 6, 2026
Merged

feat(ios): tap-to-expand link previews in chat transcript#101198
steipete merged 1 commit into
mainfrom
feat/ios-link-previews-nash

Conversation

@steipete

@steipete steipete commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Related: #100699

What Problem This Solves

Bare URLs in the iOS/macOS chat transcript render as plain tappable links with no way to see what's behind them before leaving the app. Android gained tap-to-expand previews in #100898; this brings the same experience to the Apple clients.

Why This Change Was Made

Mirrors the Android product contract exactly: no ambient fetches, ever — a collapsed domain chip appears under messages containing a safe web link, metadata is fetched only on tap, and the expanded text card (title + description) opens the URL on tap like the link itself. LinkPresentation/LPLinkMetadataProvider was deliberately not used: it fetches ambiently and can't honor our policy. The custom URLSession fetcher enforces the same constraints as Android: http/https only; non-public hosts rejected (loopback, RFC1918, link-local, CGNAT, ULA, localhost/*.local); manual redirect approval per hop (max 3, scheme + host policy re-checked); ephemeral configuration with no cookies or credential storage; 6s total deadline; 512KB body cap; text/html only. Metadata is sanitized plain text (entities decoded, control chars stripped, capped); results including failures live in a bounded in-memory LRU only. Card is text-only, matching Android.

Rebinding disclosure: URLSession has no DNS hook, so DNS-name validation uses task transaction metrics after the response: absent metrics or any non-public remote address discards the result (fail closed). A rebound host can receive the initial request before the result is discarded — narrower protection than Android's Dns-level pinning; literal-IP and localhost cases are still rejected before any request. Documented at the site.

User Impact

iOS and macOS users can peek at a link's title and description before opening it, with zero background network traffic from message content and failed lookups showing a plain unavailable state.

Evidence

  • swift test --package-path apps/shared/OpenClawKit — full suite green (542 Swift Testing + 21 XCTest); focused link-preview suites 15 tests / 6 suites: extraction (code exclusion, scheme filtering), OG parsing (fallbacks, entity handling, caps), host policy (IPv4/IPv6 literals incl. CGNAT/ULA/link-local, localhost, *.local), redirect approval, deadline, body cap, fetch-on-tap-only, cache-hit behavior via URLProtocol stubs.
  • iOS xcodebuild build-for-testing green; swiftformat --lint clean; new source registered in the iOS filelist.
  • corepack pnpm native:i18n:sync run for the new strings.
  • Structured review (codex): one finding rejected with direct proof — the reviewer predicted the native-i18n drift gate would fail because the inventory lacks entries for the new file; running the actual gate (corepack pnpm native:i18n:check, the same command CI runs) passes on this exact tree, so the inventory is precisely what the scanner generates. Whether the scanner should extract these particular string constructs is an i18n-tooling scan-pattern question, not staleness in this PR.

@openclaw-barnacle openclaw-barnacle Bot added scripts Repository scripts size: XL maintainer Maintainer-authored PR labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 868f354ded

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +614 to +616
.task(id: self.url) {
guard self.result == nil else { return }
self.result = await chatLinkPreviewStore.get(self.url)

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.

P2 Badge Reset preview state when the URL changes

When a message row keeps the same SwiftUI identity but its first URL changes after expansion, this guard leaves the old result in place because it only checks for nil. That can happen when cached or optimistic transcript rows are later replaced/reconciled with live history while preserving the message ID, so the card can show metadata or a failure for the previous URL while the domain and tap target are for the new URL. Key the state by URL or clear/ignore in-flight results when url changes.

Useful? React with 👍 / 👎.


guard
let response,
let html = String(bytes: response.data, encoding: .utf8)

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.

P2 Badge Honor the response charset when decoding HTML

For any accepted text/html response served with a non-UTF-8 charset, such as text/html; charset=iso-8859-1 or Windows-1252, this hard-coded UTF-8 decode returns nil as soon as the page contains non-UTF-8 bytes, so a valid preview fails before parsing metadata. URLResponse exposes the server-provided text encoding (textEncodingName), so capture it with the response and decode using that charset with a UTF-8 fallback.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 6, 2026, 5:57 PM ET / 21:57 UTC.

Summary
The PR adds a Swift tap-to-expand chat link-preview parser, fetcher, store, and SwiftUI card, wires it into Apple chat message bodies, registers the file for iOS builds, and refreshes native i18n line inventory.

PR surface: Other +999. Total +999 across 5 files.

Reproducibility: yes. from source for the review findings: the PR head counts image-only metadata as loaded, keeps preview result state across URL changes, and decodes accepted HTML only as UTF-8. I did not run a native Apple UI reproduction in this read-only review.

Review metrics: 1 noteworthy metric.

  • New chat URL fetch surface: 1 Swift fetcher added. Chat content can now trigger URL metadata requests after a user tap, so privacy and security proof matters before merge.

Stored data model
Persistent data-model change detected: vector/embedding metadata: apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatLinkPreviewTests.swift. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🦪 silver shellfish
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof from a real setup is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Fix image-only metadata handling, charset-aware decoding, and URL-keyed preview state.
  • [P1] Add redacted iOS or macOS app proof showing a transcript link collapsed, tapped, expanded, and opened or safely unavailable.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body reports Swift tests, build-for-testing, lint, and i18n commands, but no real iOS/macOS screenshot, recording, live output, linked artifact, or redacted app log showing the changed tap-to-expand behavior; redact IPs, API keys, phone numbers, endpoints, and other private details before posting proof. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] This adds a security-sensitive user-initiated URL metadata fetch path from chat content; source mitigations are present, but green CI alone does not prove the native Apple gesture path or network boundary in a real app.
  • [P1] The branch is behind current main, so a final merge verdict should be refreshed after rebasing or updating the exact PR head.

Maintainer options:

  1. Fix and prove Apple link previews (recommended)
    Repair the review findings, keep the preview fetch gesture-gated, and add redacted native app proof before merge.
  2. Accept source-only network proof
    A maintainer could intentionally accept tests and source inspection for the network boundary, but the user-visible Apple flow would remain unproven.
  3. Pause this platform slice
    If the Apple fetch boundary needs more design review, keep the umbrella issue open and pause this PR until the product/security contract is settled.

Next step before merge

  • [P1] The PR has narrow code blockers, but external real behavior proof is missing and cannot be supplied by ClawSweeper automation, so contributor or maintainer follow-up is required before merge.

Security
Cleared: No concrete supply-chain issue was found, but the new URL metadata fetch path is security-sensitive and remains merge-risk work until the review findings and real app proof are addressed.

Review findings

  • [P2] Treat image-only metadata as unavailable — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatLinkPreview.swift:119-126
  • [P2] Honor the response charset when decoding HTML — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatLinkPreview.swift:407
  • [P2] Reset preview state when the URL changes — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatLinkPreview.swift:614-616
Review details

Best possible solution:

Land the Apple link-preview slice only after the parser/UI, charset, and URL-keyed state issues are fixed and a redacted iOS or macOS app run proves the tap-to-expand path without ambient fetches.

Do we have a high-confidence way to reproduce the issue?

Yes from source for the review findings: the PR head counts image-only metadata as loaded, keeps preview result state across URL changes, and decodes accepted HTML only as UTF-8. I did not run a native Apple UI reproduction in this read-only review.

Is this the best way to solve the issue?

No: the feature direction matches the merged Android sibling, but the current patch is not the best merge path until it requires visible metadata or renders images, resets state by URL, honors response charset, and includes real native proof.

Full review comments:

  • [P2] Treat image-only metadata as unavailable — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatLinkPreview.swift:119-126
    When a page provides only og:image, this guard returns .loaded, but the expanded card renders only title and description. That leaves users with a domain-only card instead of the unavailable state; either require title/description for .loaded or render the image before counting it as useful metadata.
    Confidence: 0.94
  • [P2] Honor the response charset when decoding HTML — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatLinkPreview.swift:407
    The fetcher accepts any text/html response but always decodes bytes as UTF-8. Valid pages served as text/html; charset=iso-8859-1 or Windows-1252 will fail before parsing metadata as soon as they contain non-UTF-8 bytes; carry the response encoding through from URLResponse.textEncodingName and fall back to UTF-8.
    Confidence: 0.9
  • [P2] Reset preview state when the URL changes — apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatLinkPreview.swift:614-616
    .task(id: self.url) reruns for a new URL, but this guard keeps the previous result once it is non-nil. If a cached or optimistic row is reconciled with live history under the same SwiftUI row identity, the expanded card can show metadata or failure for the old URL while the domain and tap target are for the new URL; key or clear the result by URL.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.89

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against e80e8a2b6730.

Label changes

Label changes:

  • add P3: This is a mobile chat polish feature with limited blast radius rather than a broken core runtime or blocked setup path.
  • add merge-risk: 🚨 security-boundary: The diff adds a new user-initiated URL metadata fetch path from chat content, which needs more than green CI to prove the Apple app privacy boundary.
  • add rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body reports Swift tests, build-for-testing, lint, and i18n commands, but no real iOS/macOS screenshot, recording, live output, linked artifact, or redacted app log showing the changed tap-to-expand behavior; redact IPs, API keys, phone numbers, endpoints, and other private details before posting proof. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Label justifications:

  • P3: This is a mobile chat polish feature with limited blast radius rather than a broken core runtime or blocked setup path.
  • merge-risk: 🚨 security-boundary: The diff adds a new user-initiated URL metadata fetch path from chat content, which needs more than green CI to prove the Apple app privacy boundary.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🦪 silver shellfish and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body reports Swift tests, build-for-testing, lint, and i18n commands, but no real iOS/macOS screenshot, recording, live output, linked artifact, or redacted app log showing the changed tap-to-expand behavior; redact IPs, API keys, phone numbers, endpoints, and other private details before posting proof. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Other +999. Total +999 across 5 files.

View PR surface stats
Area Files Added Removed Net
Source 0 0 0 0
Tests 0 0 0 0
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 5 1019 20 +999
Total 5 1019 20 +999

Acceptance criteria:

  • [P1] swift test --package-path apps/shared/OpenClawKit.
  • [P1] xcodebuild build-for-testing for the iOS target.
  • [P1] corepack pnpm native:i18n:check.

What I checked:

Likely related people:

  • steipete: Authored the linked mobile polish issue, the merged Android sibling, this PR head, and a recent merged iOS/shared chat PR touching ChatMessageViews.swift and adjacent chat UI files. (role: recent area contributor and likely feature owner; confidence: high; commits: 868f354ded3d, cd25de98b1b9, 60ce50671229; files: apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatLinkPreview.swift, apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift, apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatLinkPreview.kt)
  • zhangguiping-xydt: Git blame on the current shared ChatMessageBody region points to xingzhou's recent merged baseline commit, making them adjacent history context but not the likely owner of this feature direction. (role: recent adjacent contributor; confidence: low; commits: f805bb4e0255; files: apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P3 Low-priority cleanup, docs, polish, ergonomics, or speculative work. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jul 6, 2026
@steipete
steipete merged commit 66c764b into main Jul 6, 2026
137 of 144 checks passed
@steipete
steipete deleted the feat/ios-link-previews-nash branch July 6, 2026 22:47
@steipete

steipete commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Merged via squash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer Maintainer-authored PR merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P3 Low-priority cleanup, docs, polish, ergonomics, or speculative work. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. scripts Repository scripts size: XL status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant