Skip to content

feat(mobile): preview first-message cloud-agent attachments - #5412

Merged
iscekic merged 10 commits into
mainfrom
md-attachment-preview-b30f
Aug 21, 2026
Merged

feat(mobile): preview first-message cloud-agent attachments#5412
iscekic merged 10 commits into
mainfrom
md-attachment-preview-b30f

Conversation

@iscekic

@iscekic iscekic commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Tapping a Markdown file that a cloud agent sends now opens the preview with the file's content.
  • Tapping an image file that a cloud agent sends now shows the image inline and opens the full-screen viewer.
  • Both previews still work after leaving and reopening the session.
  • A file that fails to load shows an error with a Retry button that fetches a fresh link.
  • An empty file shows "This file is empty."
  • Small inline images sent as file parts now render in the live stream.

The live stream no longer blanks every file-part URL. It now keeps file:// sandbox paths, http(s) URLs, and data: URLs up to 512 KiB on top-level file parts, and it still strips larger data: URLs and the source text. The new MAX_INLINE_FILE_URL_LENGTH limit is half the ingest-frame byte budget, so one inline file cannot alone overflow a frame.

Files
  • services/cloud-agent-next/src/shared/trim-payload.ts — adds MAX_INLINE_FILE_URL_LENGTH; stripFilePartFields keeps non-data: URLs and small top-level data: URLs, strips large data: URLs, and still blanks source.text.value.

A new parser turns the wrapper's sandbox attachment URL into a CloudAgentAttachmentRef with the message UUID and filename. The pattern accepts only the exact sandbox path shape, one UUID segment followed by one filename segment, so unrelated file URLs do not match. History keeps this URL form forever, so the parser must handle both live events and replays.

Files
  • apps/mobile/src/components/agents/file-part-preview.ts — adds the CloudAgentAttachmentRef type and parseCloudAgentAttachmentUrl.

The cache entry contract changes: url becomes optional, and attachmentRef plus resolveFailed join the shape. A sandbox attachment URL now stores a reference-only entry with no URL key, and three new helpers overwrite the URL, mark a failed presign, and clear that mark. First write still wins, and data: URLs still write to disk.

Files
  • apps/mobile/src/components/agents/file-part-cache.ts — makes url optional, adds attachmentRef and resolveFailed, adds overwriteFilePartCacheEntry, markFilePartResolveFailed, and clearFilePartResolveFailed, and changes useFilePartCache to snapshot the entry object.

The new useResolvedFilePartUrl hook owns URL resolution for a file part. It returns a captured http(s) or data: URL as-is and presigns a sandbox attachment through the existing getAttachmentDownloadUrl endpoint on demand. A module-level set dedupes in-flight presigns by part id, and refreshFilePartUrl re-presigns and returns false instead of throwing.

Files
  • apps/mobile/src/components/agents/file-part-url-resolver.ts — adds useResolvedFilePartUrl and refreshFilePartUrl; exposes ready, resolving, unavailable, and error states.

The renderer now consumes the resolver's state instead of reading the cache directly. Image parts get loading, error, and retry rows, the preview modal gets an onRetry prop that re-presigns before re-reading, and the chip shows a busy spinner while resolution runs.

Files
  • apps/mobile/src/components/agents/file-part-renderer.tsx — drops the resolveUsableUrl helper, uses useResolvedFilePartUrl, adds the image states, and passes onRetry to the modal.

Tests: 5 files changed — apps/mobile/src/components/agents/file-part-cache.test.ts, apps/mobile/src/components/agents/file-part-preview.test.ts, apps/mobile/src/components/agents/file-part-renderer.mounted.test.tsx, services/cloud-agent-next/src/shared/ingest-frame.test.ts, services/cloud-agent-next/src/shared/trim-payload.test.ts.

Generated: none.


Verification

On iOS, four scenarios ran across three verify rounds and the g4 takeover round.

Case What it proves Platform Result
E1 The live markdown tap opens the preview modal and renders the file text. ios passed after the fix in round g4 takeover
E2 The live image tap opens the full-screen viewer. ios passed after the fix in round g4 takeover
E3 After re-entering the session, the markdown tap still opens the preview modal. ios passed after the fix in round g4 takeover
E4 After re-entering the session, the image tap still opens the full-screen viewer. ios passed after the fix in round g4 takeover

Defects reproduced on the unfixed build:

  • Live markdown tap showed Preview unavailable, no modal (repro R1). Fixed; no longer reproduces.
  • Live image tap showed Image unavailable, no viewer (repro R2). Fixed; no longer reproduces.
  • After reopen, markdown showed Preview unavailable (repro R3). Fixed; no longer reproduces.

Recordings: repro-r0-ios.mp4, repro-r1-ios.mp4.

Visual Changes

Markdown preview modal. Tapping the markdown chip opens a modal that renders the file text, not the Preview unavailable toast. The Repro brief heading and body line sit under the title bar; the Done button is top right.
e2e-takeover-E1-md-modal.png

Full-screen image viewer. Tapping the inline image opens a full-screen viewer that shows the PNG, not the Image unavailable row. The red PNG spans the width on a black backdrop; the close button is top left.
e2e-takeover-E2-image-viewer.png

Reviewer Notes

Human steps: none known.

Notes: none.

Parse the wrapper's sandbox file:// URL into a messageUuid+filename
reference and store it as a ref-only cache entry, so a later slice can
presign a fresh R2 download URL on demand. Add overwrite and
resolve-failed helpers with store-driven failure state.
Add an on-demand resolver that presigns a fresh R2 GET for captured
attachment refs and feeds the existing preview modal and image viewer.
Wire the renderer to show busy, error-with-retry, and unavailable states,
and re-presign on retry so 15-minute URL expiry recovers.
@iscekic iscekic self-assigned this Aug 21, 2026
@kilo-code-bot

kilo-code-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of markdown tap-during-presign failure toast, image retry coverage, and comment-only cache/parser notes; no high-confidence bugs.

Files Reviewed (4 files)
  • apps/mobile/src/components/agents/file-part-cache.ts
  • apps/mobile/src/components/agents/file-part-preview.ts
  • apps/mobile/src/components/agents/file-part-renderer.mounted.test.tsx
  • apps/mobile/src/components/agents/file-part-renderer.tsx
Previous Review Summaries (6 snapshots, latest commit 1aaf16b)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 1aaf16b)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of markdown tap-during-presign failure toast, image retry coverage, and comment-only cache/parser notes; no high-confidence bugs.

Files Reviewed (4 files)
  • apps/mobile/src/components/agents/file-part-cache.ts
  • apps/mobile/src/components/agents/file-part-preview.ts
  • apps/mobile/src/components/agents/file-part-renderer.mounted.test.tsx
  • apps/mobile/src/components/agents/file-part-renderer.tsx

Previous review (commit 5952f22)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of remount URL reuse, image-presign retry coverage, pressable feedback, and ingest-frame trim-contract tests; no high-confidence bugs.

Files Reviewed (3 files)
  • apps/mobile/src/components/agents/file-part-renderer.tsx
  • apps/mobile/src/components/agents/file-part-renderer.mounted.test.tsx
  • services/cloud-agent-next/src/shared/ingest-frame.test.ts

Previous review (commit 2c8b8cf)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of live-stream trimPayload small-data: URL preservation (512 KiB cap); no high-confidence bugs on keeping small inline images while still stripping oversized and tool-attachment data: URLs.

Files Reviewed (2 files)
  • services/cloud-agent-next/src/shared/trim-payload.ts
  • services/cloud-agent-next/src/shared/trim-payload.test.ts

Previous review (commit 0501000)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of live-stream trimPayload file-part URL preservation; no high-confidence bugs on keeping file:// and http(s) while still stripping data: URLs.

Files Reviewed (2 files)
  • services/cloud-agent-next/src/shared/trim-payload.ts
  • services/cloud-agent-next/src/shared/trim-payload.test.ts

Previous review (commit c3d1614)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of the cache useSyncExternalStore snapshot and markdown tap-during-presign path; no high-confidence bugs on the new pending-preview behavior.

Files Reviewed (3 files)
  • apps/mobile/src/components/agents/file-part-cache.ts
  • apps/mobile/src/components/agents/file-part-renderer.tsx
  • apps/mobile/src/components/agents/file-part-renderer.mounted.test.tsx

Previous review (commit 1078afd)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Reviewed the mobile file-part cache, sandbox URL parser, on-demand R2 presign resolver, and renderer retry states; no high-confidence bugs, security issues, or leaks on the first-message attachment path.

Files Reviewed (7 files)
  • apps/mobile/src/components/agents/file-part-cache.ts
  • apps/mobile/src/components/agents/file-part-cache.test.ts
  • apps/mobile/src/components/agents/file-part-preview.ts
  • apps/mobile/src/components/agents/file-part-preview.test.ts
  • apps/mobile/src/components/agents/file-part-url-resolver.ts
  • apps/mobile/src/components/agents/file-part-renderer.tsx
  • apps/mobile/src/components/agents/file-part-renderer.mounted.test.tsx

Reviewed by grok-4.6 · Input: 125.5K · Output: 6.1K · Cached: 257.2K

Review guidance: REVIEW.md from base branch main

The cache hook snapshotted a version counter and ignored it, so a
post-mount overwrite (the on-demand presign) did not reliably re-render
subscribers and the chip stayed busy. Snapshot the entry object instead,
and open the markdown modal once the URL lands when the chip is tapped
while the presign is in flight.
The wrapper's live-stream trim stripped every FilePart url to '', so the
mobile onFilePart sink never saw the raw file:// sandbox path on the live
path. The attachment reference was only captured from the history replay,
which races the async part ingestion. Keep non-data: urls and strip only
data: urls (inline base64) plus source.text.value.
The wrapper's live-stream trim stripped every data: url to '', but the Kilo CLI inlines image file parts as data:, so the live first-message image never reached the mobile client and showed 'Image unavailable'. Preserve small data: urls (<=512KiB) on top-level file parts; tool attachments and larger data: urls still strip for size.
The live-stream trim now preserves small inline data: URLs on top-level file parts. Update the ingest-frame test to assert the new contract: a small data: URL is preserved, a large one is stripped, and source.text.value is always stripped.
Add a remount test proving the module-level cache survives unmount/remount without a second presign, and an image presign-failure test proving the retry row re-presigns and renders the inline image.
The two image retry Pressables lacked active opacity feedback. Add active:opacity-80 to match the chip Pressable.
- Show the retry toast and reset preview when a markdown tap during the
  presign is followed by a presign failure, instead of a silent no-op.
- Add mounted tests for the imageFailed retry press with and without an
  attachment ref, and for the markdown presign-failure leg.
- Correct the attachment path comment (sessionId, not agentId) and the
  resolveCacheUrl failure-mode comment (undefined return, not rejection).
clearFilePartResolveFailed writes a new entry and emits, so the effect
re-runs via the cached dep; the attempt counter is a dead second trigger.
@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 21, 2026
@iscekic
iscekic enabled auto-merge (squash) August 21, 2026 10:42
messageUuid: attachmentRef.messageUuid,
filename: attachmentRef.filename,
});
overwriteFilePartCacheEntry(part.id, {

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.

bot: Presigning an attachment from an uncached part.url drops the reference needed for later retries.

Suggested fix: Carry attachmentRef into the cache write (for example, allow overwriteFilePartCacheEntry to accept it, or seed a ref-only entry before the mutation). Today the overwrite preserves a reference only if an entry already existed. On the uncached fallback path, the first image/text preview works, but after its signed URL expires or an image/text load fails, refreshFilePartUrl(part.id) sees an entry without attachmentRef and returns false. Add a regression test that mounts a sandbox part.url without calling cacheFilePart, resolves it, then verifies a later refresh re-presigns successfully.

(!options?.preserveSmallDataUrls || url.length > MAX_INLINE_FILE_URL_LENGTH);
const out: Record<string, unknown> = {
...part,
...(stripDataUrl ? { 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.

bot: The new preservation rule forwards every non-data: URL scheme, not just the intended sandbox file:// and HTTP(S) URLs.

Suggested fix: Replace the data-only stripping condition with an explicit allowlist: preserve only the validated sandbox attachment file:// shape, http:///https://, and eligible small data: URLs; blank all other schemes. Add tests for unsupported values such as ftp: and case-variant/other schemes. As written, arbitrary non-data URLs are included in the ingest payload, widening the prior behavior that always removed file-part URLs and exceeding the stated retention contract.

@iscekic
iscekic merged commit 07d1a74 into main Aug 21, 2026
18 checks passed
@iscekic
iscekic deleted the md-attachment-preview-b30f branch August 21, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants