Skip to content

feat(mobile): preview images and markdown read by the model - #4835

Merged
iscekic merged 15 commits into
mainfrom
file-preview-2975
Jul 29, 2026
Merged

feat(mobile): preview images and markdown read by the model#4835
iscekic merged 15 commits into
mainfrom
file-preview-2975

Conversation

@iscekic

@iscekic iscekic commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

What: Mobile agent tool cards now preview the files the model read — rendered markdown for .md reads (with a full-screen reader) and inline images for image reads and MCP screenshots (with a pinch-zoom full-screen viewer).

Why: A .md read rendered as raw line-numbered mono text, and an image read rendered nothing at all: the client SDK blanks attachment bytes (stripPartContentIfFile) before the store ever sees them, on both the live and history paths.

How (four slices):

  1. Markdown preview + reader (2f10f9dbc) — read tool cards for .md/.mdx parse state.metadata.display (falling back to the legacy state.output wrapper for parts that predate the field), render a capped inline markdown preview with a partial-read footer (lines X–Y of N / (truncated)), and open a full-screen markdown reader modal. Non-markdown reads are byte-identical to main.
  2. onImageAttachment sink + FS cache (91d2ab1b7) — createChatProcessor accepts an optional sink called with (partId, mime, dataUrl) for each image/* attachment just before the existing strip runs; threaded through all three processor sites (live session.ts, hydration + history replay in session-manager.ts). The strip itself is byte-identical to main — the store retains nothing new. Mobile's sink writes the bytes once to the expo-file-system cache (in-memory dedupe + existence check) and exposes a file:// URI via jotai; expo-image then gets native caching/downsampling. Web never passes the sink and is byte-for-byte unaffected.
  3. Shared pinch-zoom viewer (497207f34) — kilo-chat's image preview modal moved to ImageViewerModal with pinch/pan/double-tap-reset gestures (own GestureHandlerRootView inside the RN Modal). kilo-chat keeps its share action unchanged; the tool-card viewer omits share (data URLs have no presigned URL/attachmentId).
  4. Tool-card image attachments (b18b2fb8c) — ToolCardShell takes an optional part; ReadToolCard and GenericToolCard (covers maestro_take_screenshot and any future MCP image tool) render cached images inline at a clamped aspect ratio with a skeleton, a terminal "unavailable" row when no cached bytes exist (container sessions strip server-side), and a static "Image unavailable" row on decode failure. Image reads suppress the mono output line.

Scope note: the packages/cloud-agent-sdk change is a sanctioned scope break — the requesting human approved it with a performance constraint ("yes, but performant") and specified this sink + FS-cache design himself. The originally planned in-store retention cap was not shipped; MAX_RETAINED_IMAGE_URL_CHARS does not exist.

Verification

  • packages/cloud-agent-sdk: pnpm typecheck + pnpm test (28 suites, 1059 tests) — sink fires per site, stored part still stripped, non-image/blank-url/sink-absent cases covered; the pre-existing strip test passes unmodified
  • apps/mobile: pnpm format && pnpm typecheck && pnpm lint && pnpm check:unused && pnpm test (280 files, 2386 tests) — markdown parser golden fixtures, attachment filter, aspect-ratio clamp, FS-cache dedupe/failure paths
  • Repository root: pnpm typecheck; git diff --check clean; scripts/lint-all.sh clean
  • Two independent cumulative reviews (integration seams): Slice 1↔4 card composition, Slice 2↔4 cache contract, Slice 3↔4 viewer props, SDK surface — all clean
  • E2E (seeded agent session, iOS 26.5 simulator, Maestro) — all 8 seeded cases verified on device by an independent verifier:
    • image read renders inline in the expanded card, opens the full-screen viewer on tap, no mono output line (D10), no share button (D7)
    • maestro_take_screenshot renders the same way through the generic tool card
    • complete .md renders markdown (heading/list/fence), no footer
    • windowed .md renders markdown + footer lines 1–200 of 1,450, unterminated fence closed
    • legacy part (no metadata.display) renders markdown via the state.output fallback
    • blank-url image renders the terminal Image preview unavailable in this session. row, no broken image
    • .ts read unchanged from main (line-numbered mono in the wrapper)
    • application/pdf attachment renders nothing new (and no spurious unavailable row)
    • viewer dismiss preserves conversation scroll offset (pixel-diff evidence)
  • E2E limitation 1 (stated, not proxied): pinch-zoom-in and double-tap-reset-after-zoom are not expressible by Maestro's iOS driver. Verified programmatically: viewer opens on tap; double-tap is stable; scroll offset preserved. Two-finger pinch itself is a driver limitation — reported, never proxy-passed. The gesture code is the plan-prescribed RNGH structure and is shared with kilo-chat.
  • E2E limitation 2 (criterion 10 not verified on device — accepted by the requesting human): kilo-chat attachment share could not be exercised locally. The composer's Attach file button is gated on botStatus.capabilities.includes('attachments') (conversation-screen.tsx:72); a dev:seed ki_fake_* sandbox never advertises it; and a forced, correctly authenticated bot-status POST is overwritten within ~15s of the conversation opening, because the app's own request-bot-status poll escalates through kiloclaw's deliverChatWebhook, which throws has no sandboxId for fake sandboxes → kilo-chat republishes online:false and the monotonic DO upsert NULLs capabilities (mechanism proven on device; learning committed in b2578064f as mobile-kilochat-attachments-capability-gate-blocks-e2e.md). A fix needs a product-side dev seed/fake-bot harness, which is out of scope. What covers it: slice 3's fresh impl-reviewer round (impl-reviewer-slice3-r1) returned No findings., and kilo-chat still supplies onShare at message-attachment.tsx:135 — the share path is unchanged by code inspection (slice 3's risk to kilo-chat is limited to an import move + tag rename + in-modal gestures; message-attachment.tsx is otherwise unchanged). Per the approved plan this is reported as unverified — never proxied with the tool-card viewer, never marked passed — and the requesting human has explicitly accepted shipping with criterion 10 unverified on device.

Visual Changes

E2E screenshots (13) were captured by the verifier and are described here per card; they are
not attached because images cannot be uploaded via the CLI. They are preserved on the requesting
human's machine at
/private/tmp/claude-501/-Users-igor-Projects-cloud/cc2ef1f9-7c13-4287-80f8-f33266da28f8/scratchpad/e2e-shots/
(01-shot-png-expanded.png13-docpdf-expanded.png). The most informative: 07-big-expanded.png
(partial markdown with the lines 1–200 of 1,450 footer and the mid-block code fence
auto-balanced), 01/02 (image inline, then the full-screen viewer),
04-maestro-screenshot-expanded.png (maestro_take_screenshot rendering identically to read,
proving the shared ToolCardShell placement), and 11-blank-unavailable.png (blank url
degrading to a muted message).

Card (before → after) After (verified on device)
read of a PNG (mono Image read successfully → inline preview) Image inline at clamped aspect ratio; tap opens pinch-zoom viewer; no share button
maestro_take_screenshot (mono output → inline preview) Same, via the generic tool card
read of .md (line-numbered mono → rendered markdown) Heading/list/code fence rendered; partial-read footer when windowed; full-screen reader modal on tap
legacy .md part without metadata.display Renders markdown via the state.output fallback
blank-url image (nothing → terminal state) Muted Image preview unavailable in this session. row, no broken image
read of .ts Unchanged from main
kilo-chat image preview modal Shared ImageViewerModal with pinch-zoom; share button retained (not reachable in local E2E — see Verification)

Reviewer Notes

  • Reviewer focus: the sink threading in packages/cloud-agent-sdk/src/session-manager.ts + session.ts (three call sites, sink errors contained) and the FS-cache contract in apps/mobile/src/components/agents/tool-card-image-cache.ts (dedupe Set cleared on failure, file.exists skip).
  • The branch also carries three workflow-only commits under .kilo_workflow/ (learnings + monitor-behavior fixes: 04df12f32, fed917841, b2578064f) — no product code.
  • Follow-ups (not fixed here, by design):
    1. services/session-ingest integration tests are broken repo-wide (pg imported eagerly under miniflare; no CI runs them).
    2. Server-side session-store image retention is unbounded (1,412 image parts / 538MB observed in one real store; over-limit items already offload to R2).
    3. apps/web/src/components/cloud-agent-next/types.ts still duplicates the SDK strip logic (live on the app-builder path). With the sink design the two copies no longer diverge in behavior, but the duplication itself is untouched.
    4. Client-side session-store part accumulation has no eviction (pre-existing on main; the FS cache is OS-evictable and does not add to it).
  • E2E limitations are stated in the Verification section above; none were proxied.

iscekic added 8 commits July 28, 2026 16:16
Add read-tool markdown parser, inline ChatMarkdownText preview,
full-screen reader modal, and ReadToolCard branch for .md/.mdx.
…S cache

Thread an optional image-attachment sink through the chat processor and
all three processor creation sites (live session, child hydration,
history applyPage). The store strip is unchanged — zero added store RAM
and no web behaviour change (web never passes the sink). Mobile caches
image/* data URLs under Paths.cache/tool-card-images, deduped by part
id, for Slice 4's file:// tool-card rendering.
…vening

Records the planner-intervention incident as a learning and states the
negative space in Planner Monitor Mode: never kill a live dispatch on a
judgment call, never edit the worktree, never forge a dispatch-log
marker; verify from the orchestrator's scrollback, scratch and git log
first.
Move message-image-preview-modal to components/image-viewer-modal, add
RNGH pinch/pan/double-tap zoom (own GestureHandlerRootView inside the
Modal), and make onShare optional so tool cards can adopt the viewer.
kilo-chat behaviour is unchanged: same share flow, same a11y labels,
same inline share-error block.
@iscekic iscekic self-assigned this Jul 28, 2026
Comment thread packages/cloud-agent-sdk/src/chat-processor.ts
Comment thread apps/mobile/src/components/agents/tool-card-image-cache.ts
Comment thread apps/mobile/src/components/agents/read-tool-markdown.ts Outdated
Comment thread apps/mobile/src/components/image-viewer-modal.tsx
@kilo-code-bot

kilo-code-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The only product-code change since the previous review (54ff57ddb, read-tool-markdown.ts) removes the redundant sawMatch flag and resolves the previously flagged unreachable-fallback SUGGESTION with no behavioral change; no new issues found.

Files Reviewed (1 file)
  • apps/mobile/src/components/agents/read-tool-markdown.ts - previous SUGGESTION (unreachable fallback branch) resolved
Previous Review Summary (commit deda804)

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

Previous review (commit deda804)

Status: 4 Issues Found | Recommendation: Address before merge

Executive Summary

The highest-risk finding is a latent multi-image collapse bug (packages/cloud-agent-sdk/src/chat-processor.ts) where all image attachments on one tool part share a single cache key, silently rendering duplicate images if any tool ever emits more than one image attachment per part.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
packages/cloud-agent-sdk/src/chat-processor.ts 48 Multi-image tool parts collapse to a single cached image (shared part-id key)

SUGGESTION

File Line Issue
apps/mobile/src/components/agents/tool-card-image-cache.ts 7 In-memory dedupe state and on-disk cache grow without bound (no eviction)
apps/mobile/src/components/agents/read-tool-markdown.ts 112 Unreachable fallback branch in line-range computation
apps/mobile/src/components/image-viewer-modal.tsx 87 Pan gesture gated on committed zoom level rather than live scale
Files Reviewed (21 files)
  • apps/mobile/src/components/agents/markdown-viewer-modal.tsx
  • apps/mobile/src/components/agents/mobile-session-manager.ts
  • apps/mobile/src/components/agents/read-markdown-preview.tsx
  • apps/mobile/src/components/agents/read-tool-markdown.test.ts
  • apps/mobile/src/components/agents/read-tool-markdown.ts - 1 issue
  • apps/mobile/src/components/agents/tool-card-attachments.test.ts
  • apps/mobile/src/components/agents/tool-card-attachments.ts
  • apps/mobile/src/components/agents/tool-card-image-attachments.tsx
  • apps/mobile/src/components/agents/tool-card-image-cache.test.ts
  • apps/mobile/src/components/agents/tool-card-image-cache.ts - 1 issue
  • apps/mobile/src/components/agents/tool-card-shell.tsx
  • apps/mobile/src/components/agents/tool-cards/generic-tool-card.tsx
  • apps/mobile/src/components/agents/tool-cards/read-tool-card.tsx
  • apps/mobile/src/components/image-viewer-modal.tsx - 1 issue
  • apps/mobile/src/components/kilo-chat/message-attachment.tsx
  • apps/mobile/src/components/kilo-chat/message-image-preview-modal.tsx (deleted)
  • packages/cloud-agent-sdk/src/chat-processor.test.ts
  • packages/cloud-agent-sdk/src/chat-processor.ts - 1 issue
  • packages/cloud-agent-sdk/src/session-manager.ts
  • packages/cloud-agent-sdk/src/session.ts
  • .kilo_workflow/* (workflow-only, no product code)

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-5 · Input: 38 · Output: 8.1K · Cached: 1.3M

Review guidance: REVIEW.md from base branch main

@iscekic iscekic added the human-ready The PR is ready for human review. label Jul 28, 2026
@iscekic
iscekic requested a review from pandemicsyn July 29, 2026 00:44
@iscekic
iscekic merged commit a28cf41 into main Jul 29, 2026
22 checks passed
@iscekic
iscekic deleted the file-preview-2975 branch July 29, 2026 10:29
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